@dizzlkheinz/ynab-mcpb 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.chunkhound.json +11 -0
  2. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
  3. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
  4. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
  5. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
  6. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
  7. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
  8. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
  9. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
  10. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
  11. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
  12. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
  13. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
  14. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
  15. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
  16. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
  17. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
  18. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
  19. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
  20. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
  21. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
  22. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
  23. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
  24. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
  25. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
  26. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
  27. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
  28. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
  29. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
  30. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
  31. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
  32. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
  33. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
  34. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
  35. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
  36. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
  37. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
  38. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
  39. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
  40. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
  41. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
  42. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
  43. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
  44. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
  45. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
  46. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
  47. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
  48. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
  49. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
  50. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
  51. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
  52. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
  53. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
  54. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
  55. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
  56. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
  57. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
  58. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
  59. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
  60. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
  61. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
  62. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
  63. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
  64. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
  65. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
  66. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
  67. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
  68. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
  69. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
  70. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
  71. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
  72. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
  73. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
  74. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
  75. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
  76. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
  77. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
  78. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
  79. package/.dxtignore +57 -0
  80. package/.env.example +44 -0
  81. package/.gemini/settings.json +8 -0
  82. package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  83. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  84. package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  85. package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
  86. package/.github/pull_request_template.md +41 -0
  87. package/.github/workflows/ci-tests.yml +41 -0
  88. package/.github/workflows/claude-code-review.yml +57 -0
  89. package/.github/workflows/claude.yml +50 -0
  90. package/.github/workflows/full-integration.yml +22 -0
  91. package/.github/workflows/pr-description-check.yml +88 -0
  92. package/.github/workflows/publish.yml +33 -0
  93. package/.github/workflows/release.yml +89 -0
  94. package/.mcpbignore +58 -0
  95. package/.prettierignore +10 -0
  96. package/.prettierrc.json +10 -0
  97. package/ADOS-2-Module-1-Complete-Manual.md +757 -0
  98. package/AGENTS.md +36 -0
  99. package/CHANGELOG.md +187 -0
  100. package/CLAUDE.md +414 -0
  101. package/CODEREVIEW_RESPONSE.md +128 -0
  102. package/LICENSE +17 -0
  103. package/NUL +1 -0
  104. package/README.md +222 -0
  105. package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
  106. package/TESTING_NOTES.md +217 -0
  107. package/WARP.md +245 -0
  108. package/accountactivity-merged.csv +149 -0
  109. package/bin/ynab-mcp-server.cjs +4 -0
  110. package/bin/ynab-mcp-server.js +8 -0
  111. package/bundle-analysis.html +13110 -0
  112. package/dist/bundle/index.cjs +124 -0
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.js +85 -0
  115. package/dist/server/YNABMCPServer.d.ts +264 -0
  116. package/dist/server/YNABMCPServer.js +845 -0
  117. package/dist/server/budgetResolver.d.ts +15 -0
  118. package/dist/server/budgetResolver.js +99 -0
  119. package/dist/server/cacheManager.d.ts +74 -0
  120. package/dist/server/cacheManager.js +306 -0
  121. package/dist/server/config.d.ts +3 -0
  122. package/dist/server/config.js +19 -0
  123. package/dist/server/deltaCache.d.ts +61 -0
  124. package/dist/server/deltaCache.js +206 -0
  125. package/dist/server/deltaCache.merge.d.ts +9 -0
  126. package/dist/server/deltaCache.merge.js +111 -0
  127. package/dist/server/diagnostics.d.ts +90 -0
  128. package/dist/server/diagnostics.js +163 -0
  129. package/dist/server/errorHandler.d.ts +69 -0
  130. package/dist/server/errorHandler.js +524 -0
  131. package/dist/server/prompts.d.ts +31 -0
  132. package/dist/server/prompts.js +205 -0
  133. package/dist/server/rateLimiter.d.ts +27 -0
  134. package/dist/server/rateLimiter.js +82 -0
  135. package/dist/server/requestLogger.d.ts +62 -0
  136. package/dist/server/requestLogger.js +190 -0
  137. package/dist/server/resources.d.ts +39 -0
  138. package/dist/server/resources.js +85 -0
  139. package/dist/server/responseFormatter.d.ts +14 -0
  140. package/dist/server/responseFormatter.js +42 -0
  141. package/dist/server/securityMiddleware.d.ts +87 -0
  142. package/dist/server/securityMiddleware.js +117 -0
  143. package/dist/server/serverKnowledgeStore.d.ts +11 -0
  144. package/dist/server/serverKnowledgeStore.js +42 -0
  145. package/dist/server/toolRegistry.d.ts +85 -0
  146. package/dist/server/toolRegistry.js +272 -0
  147. package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
  148. package/dist/tools/__tests__/deltaTestUtils.js +26 -0
  149. package/dist/tools/accountTools.d.ts +37 -0
  150. package/dist/tools/accountTools.js +175 -0
  151. package/dist/tools/budgetTools.d.ts +10 -0
  152. package/dist/tools/budgetTools.js +68 -0
  153. package/dist/tools/categoryTools.d.ts +27 -0
  154. package/dist/tools/categoryTools.js +232 -0
  155. package/dist/tools/compareTransactions/formatter.d.ts +71 -0
  156. package/dist/tools/compareTransactions/formatter.js +97 -0
  157. package/dist/tools/compareTransactions/index.d.ts +30 -0
  158. package/dist/tools/compareTransactions/index.js +160 -0
  159. package/dist/tools/compareTransactions/matcher.d.ts +12 -0
  160. package/dist/tools/compareTransactions/matcher.js +140 -0
  161. package/dist/tools/compareTransactions/parser.d.ts +14 -0
  162. package/dist/tools/compareTransactions/parser.js +430 -0
  163. package/dist/tools/compareTransactions/types.d.ts +27 -0
  164. package/dist/tools/compareTransactions/types.js +1 -0
  165. package/dist/tools/compareTransactions.d.ts +1 -0
  166. package/dist/tools/compareTransactions.js +1 -0
  167. package/dist/tools/deltaFetcher.d.ts +22 -0
  168. package/dist/tools/deltaFetcher.js +137 -0
  169. package/dist/tools/deltaSupport.d.ts +20 -0
  170. package/dist/tools/deltaSupport.js +176 -0
  171. package/dist/tools/exportTransactions.d.ts +17 -0
  172. package/dist/tools/exportTransactions.js +191 -0
  173. package/dist/tools/monthTools.d.ts +16 -0
  174. package/dist/tools/monthTools.js +107 -0
  175. package/dist/tools/payeeTools.d.ts +17 -0
  176. package/dist/tools/payeeTools.js +82 -0
  177. package/dist/tools/reconcileAdapter.d.ts +25 -0
  178. package/dist/tools/reconcileAdapter.js +167 -0
  179. package/dist/tools/reconciliation/analyzer.d.ts +3 -0
  180. package/dist/tools/reconciliation/analyzer.js +567 -0
  181. package/dist/tools/reconciliation/executor.d.ts +94 -0
  182. package/dist/tools/reconciliation/executor.js +611 -0
  183. package/dist/tools/reconciliation/index.d.ts +54 -0
  184. package/dist/tools/reconciliation/index.js +249 -0
  185. package/dist/tools/reconciliation/matcher.d.ts +3 -0
  186. package/dist/tools/reconciliation/matcher.js +160 -0
  187. package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
  188. package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
  189. package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
  190. package/dist/tools/reconciliation/recommendationEngine.js +273 -0
  191. package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
  192. package/dist/tools/reconciliation/reportFormatter.js +214 -0
  193. package/dist/tools/reconciliation/types.d.ts +172 -0
  194. package/dist/tools/reconciliation/types.js +7 -0
  195. package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
  196. package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
  197. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
  198. package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
  199. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
  200. package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
  201. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
  202. package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
  203. package/dist/tools/schemas/outputs/index.d.ts +14 -0
  204. package/dist/tools/schemas/outputs/index.js +14 -0
  205. package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
  206. package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
  207. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
  208. package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
  209. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
  210. package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
  211. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
  212. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
  213. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
  214. package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
  215. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
  216. package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
  217. package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
  218. package/dist/tools/schemas/shared/commonOutputs.js +27 -0
  219. package/dist/tools/toolCategories.d.ts +32 -0
  220. package/dist/tools/toolCategories.js +32 -0
  221. package/dist/tools/transactionTools.d.ts +315 -0
  222. package/dist/tools/transactionTools.js +1722 -0
  223. package/dist/tools/utilityTools.d.ts +10 -0
  224. package/dist/tools/utilityTools.js +56 -0
  225. package/dist/types/index.d.ts +20 -0
  226. package/dist/types/index.js +16 -0
  227. package/dist/types/toolAnnotations.d.ts +7 -0
  228. package/dist/types/toolAnnotations.js +1 -0
  229. package/dist/utils/amountUtils.d.ts +3 -0
  230. package/dist/utils/amountUtils.js +10 -0
  231. package/dist/utils/dateUtils.d.ts +9 -0
  232. package/dist/utils/dateUtils.js +43 -0
  233. package/dist/utils/money.d.ts +21 -0
  234. package/dist/utils/money.js +51 -0
  235. package/docs/README.md +72 -0
  236. package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
  237. package/docs/assets/schemas/reconciliation-v2.json +338 -0
  238. package/docs/getting-started/CONFIGURATION.md +175 -0
  239. package/docs/getting-started/INSTALLATION.md +333 -0
  240. package/docs/getting-started/QUICKSTART.md +282 -0
  241. package/docs/guides/ARCHITECTURE.md +650 -0
  242. package/docs/guides/DEPLOYMENT.md +189 -0
  243. package/docs/guides/INTEGRATION_TESTING.md +730 -0
  244. package/docs/guides/TESTING.md +591 -0
  245. package/docs/reconciliation-flow.md +83 -0
  246. package/docs/reference/API.md +1450 -0
  247. package/docs/reference/EXAMPLES.md +946 -0
  248. package/docs/reference/TOOLS.md +348 -0
  249. package/docs/reference/TROUBLESHOOTING.md +481 -0
  250. package/esbuild.config.mjs +68 -0
  251. package/eslint.config.js +49 -0
  252. package/fix-types.sh +17 -0
  253. package/meta.json +12550 -0
  254. package/package.json +105 -0
  255. package/package.json.tmp +105 -0
  256. package/scripts/analyze-bundle.mjs +41 -0
  257. package/scripts/create-pr-description.js +203 -0
  258. package/scripts/generate-mcpb.ps1 +96 -0
  259. package/scripts/run-domain-integration-tests.js +33 -0
  260. package/scripts/run-generate-mcpb.js +29 -0
  261. package/scripts/run-throttled-integration-tests.js +116 -0
  262. package/scripts/test-delta-params.mjs +140 -0
  263. package/scripts/test-recommendations.ts +53 -0
  264. package/scripts/tmpTransaction.ts +48 -0
  265. package/scripts/validate-env.js +122 -0
  266. package/scripts/verify-build.js +105 -0
  267. package/scripts/watch-and-restart.ps1 +50 -0
  268. package/src/__tests__/comprehensive.integration.test.ts +1196 -0
  269. package/src/__tests__/delta.performance.test.ts +80 -0
  270. package/src/__tests__/performance.test.ts +725 -0
  271. package/src/__tests__/setup.ts +449 -0
  272. package/src/__tests__/testRunner.ts +444 -0
  273. package/src/__tests__/testUtils.ts +563 -0
  274. package/src/__tests__/workflows.e2e.test.ts +1675 -0
  275. package/src/index.ts +124 -0
  276. package/src/server/.gitkeep +1 -0
  277. package/src/server/YNABMCPServer.ts +1188 -0
  278. package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
  279. package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
  280. package/src/server/__tests__/budgetResolver.test.ts +425 -0
  281. package/src/server/__tests__/cacheManager.test.ts +880 -0
  282. package/src/server/__tests__/config.test.ts +166 -0
  283. package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
  284. package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
  285. package/src/server/__tests__/deltaCache.test.ts +774 -0
  286. package/src/server/__tests__/diagnostics.test.ts +823 -0
  287. package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
  288. package/src/server/__tests__/errorHandler.test.ts +416 -0
  289. package/src/server/__tests__/prompts.test.ts +354 -0
  290. package/src/server/__tests__/rateLimiter.test.ts +314 -0
  291. package/src/server/__tests__/requestLogger.test.ts +408 -0
  292. package/src/server/__tests__/resources.test.ts +299 -0
  293. package/src/server/__tests__/security.integration.test.ts +426 -0
  294. package/src/server/__tests__/securityMiddleware.test.ts +449 -0
  295. package/src/server/__tests__/server-startup.integration.test.ts +477 -0
  296. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
  297. package/src/server/__tests__/toolRegistry.test.ts +855 -0
  298. package/src/server/budgetResolver.ts +235 -0
  299. package/src/server/cacheManager.ts +503 -0
  300. package/src/server/config.ts +41 -0
  301. package/src/server/deltaCache.merge.ts +149 -0
  302. package/src/server/deltaCache.ts +341 -0
  303. package/src/server/diagnostics.ts +338 -0
  304. package/src/server/errorHandler.ts +756 -0
  305. package/src/server/prompts.ts +291 -0
  306. package/src/server/rateLimiter.ts +156 -0
  307. package/src/server/requestLogger.ts +344 -0
  308. package/src/server/resources.ts +168 -0
  309. package/src/server/responseFormatter.ts +51 -0
  310. package/src/server/securityMiddleware.ts +236 -0
  311. package/src/server/serverKnowledgeStore.ts +91 -0
  312. package/src/server/toolRegistry.ts +489 -0
  313. package/src/tools/.gitkeep +1 -0
  314. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
  315. package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
  316. package/src/tools/__tests__/accountTools.test.ts +653 -0
  317. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
  318. package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
  319. package/src/tools/__tests__/budgetTools.test.ts +423 -0
  320. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
  321. package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
  322. package/src/tools/__tests__/categoryTools.test.ts +622 -0
  323. package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
  324. package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
  325. package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
  326. package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
  327. package/src/tools/__tests__/compareTransactions.test.ts +342 -0
  328. package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
  329. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
  330. package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
  331. package/src/tools/__tests__/deltaSupport.test.ts +188 -0
  332. package/src/tools/__tests__/deltaTestUtils.ts +46 -0
  333. package/src/tools/__tests__/exportTransactions.test.ts +213 -0
  334. package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
  335. package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
  336. package/src/tools/__tests__/monthTools.test.ts +523 -0
  337. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
  338. package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
  339. package/src/tools/__tests__/payeeTools.test.ts +445 -0
  340. package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
  341. package/src/tools/__tests__/transactionTools.test.ts +3521 -0
  342. package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
  343. package/src/tools/__tests__/utilityTools.test.ts +205 -0
  344. package/src/tools/accountTools.ts +283 -0
  345. package/src/tools/budgetTools.ts +112 -0
  346. package/src/tools/categoryTools.ts +366 -0
  347. package/src/tools/compareTransactions/formatter.ts +163 -0
  348. package/src/tools/compareTransactions/index.ts +228 -0
  349. package/src/tools/compareTransactions/matcher.ts +240 -0
  350. package/src/tools/compareTransactions/parser.ts +557 -0
  351. package/src/tools/compareTransactions/types.ts +60 -0
  352. package/src/tools/compareTransactions.ts +3 -0
  353. package/src/tools/deltaFetcher.ts +278 -0
  354. package/src/tools/deltaSupport.ts +293 -0
  355. package/src/tools/exportTransactions.ts +273 -0
  356. package/src/tools/monthTools.ts +164 -0
  357. package/src/tools/payeeTools.ts +140 -0
  358. package/src/tools/reconcileAdapter.ts +312 -0
  359. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
  360. package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
  361. package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
  362. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
  363. package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
  364. package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
  365. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
  366. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
  367. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
  368. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
  369. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
  370. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
  371. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
  372. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
  373. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
  374. package/src/tools/reconciliation/analyzer.ts +824 -0
  375. package/src/tools/reconciliation/executor.ts +880 -0
  376. package/src/tools/reconciliation/index.ts +400 -0
  377. package/src/tools/reconciliation/matcher.ts +269 -0
  378. package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
  379. package/src/tools/reconciliation/recommendationEngine.ts +506 -0
  380. package/src/tools/reconciliation/reportFormatter.ts +363 -0
  381. package/src/tools/reconciliation/types.ts +314 -0
  382. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
  383. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
  384. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
  385. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
  386. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
  387. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
  388. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
  389. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
  390. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
  391. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
  392. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
  393. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
  394. package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
  395. package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
  396. package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
  397. package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
  398. package/src/tools/schemas/outputs/index.ts +270 -0
  399. package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
  400. package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
  401. package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
  402. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
  403. package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
  404. package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
  405. package/src/tools/schemas/shared/commonOutputs.ts +140 -0
  406. package/src/tools/toolCategories.ts +140 -0
  407. package/src/tools/transactionTools.ts +2509 -0
  408. package/src/tools/utilityTools.ts +90 -0
  409. package/src/types/.gitkeep +1 -0
  410. package/src/types/__tests__/index.test.ts +52 -0
  411. package/src/types/index.ts +67 -0
  412. package/src/types/integration-tests.d.ts +35 -0
  413. package/src/types/toolAnnotations.ts +44 -0
  414. package/src/utils/__tests__/dateUtils.test.ts +170 -0
  415. package/src/utils/__tests__/money.test.ts +189 -0
  416. package/src/utils/amountUtils.ts +32 -0
  417. package/src/utils/dateUtils.ts +108 -0
  418. package/src/utils/money.ts +123 -0
  419. package/test-csv-sample.csv +28 -0
  420. package/test-exports/sample_bank_statement.csv +7 -0
  421. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
  422. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
  423. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
  424. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
  425. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
  426. package/test-reconcile-autodetect.js +40 -0
  427. package/test-reconcile-tool.js +152 -0
  428. package/test-reconcile-with-csv.cjs +89 -0
  429. package/test-statement.csv +8 -0
  430. package/test_debug.js +47 -0
  431. package/test_simple.mjs +16 -0
  432. package/tsconfig.json +31 -0
  433. package/tsconfig.prod.json +18 -0
  434. package/vitest-reporters/split-json-reporter.ts +211 -0
  435. package/vitest.config.ts +96 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Payee normalization and similarity matching
3
+ * Implements two-tier matching: normalized string comparison + fuzzy matching
4
+ */
5
+
6
+ /**
7
+ * Normalizes a payee string for comparison
8
+ * - Lowercase
9
+ * - Remove spaces, punctuation, special characters
10
+ * - Keep only alphanumeric characters
11
+ *
12
+ * @example
13
+ * normalizePayee("SHELL #1234 OAKVILLE ON") => "shell1234oakvilleon"
14
+ * normalizePayee("AMZN MKTP CA*123456789") => "amznmktpca123456789"
15
+ */
16
+ export function normalizePayee(payee: string | null | undefined): string {
17
+ if (!payee) return '';
18
+
19
+ return payee.toLowerCase().replace(/[^a-z0-9]/g, ''); // Remove all non-alphanumeric
20
+ }
21
+
22
+ /**
23
+ * Tier 1: Fast normalized string comparison
24
+ * Returns true if normalized strings are identical
25
+ *
26
+ * This catches 80%+ of matches quickly
27
+ */
28
+ export function normalizedMatch(
29
+ payee1: string | null | undefined,
30
+ payee2: string | null | undefined,
31
+ ): boolean {
32
+ const norm1 = normalizePayee(payee1);
33
+ const norm2 = normalizePayee(payee2);
34
+
35
+ if (!norm1 || !norm2) return false;
36
+
37
+ return norm1 === norm2;
38
+ }
39
+
40
+ /**
41
+ * Calculates Levenshtein distance between two strings
42
+ * Used for fuzzy matching when normalized comparison fails
43
+ */
44
+ function levenshteinDistance(str1: string, str2: string): number {
45
+ const len1 = str1.length;
46
+ const len2 = str2.length;
47
+
48
+ // Create distance matrix
49
+ const matrix: number[][] = Array(len1 + 1)
50
+ .fill(null)
51
+ .map(() => Array(len2 + 1).fill(0));
52
+
53
+ // Initialize first row and column
54
+ for (let i = 0; i <= len1; i++) matrix[i]![0] = i;
55
+ for (let j = 0; j <= len2; j++) matrix[0]![j] = j;
56
+
57
+ // Fill the matrix
58
+ for (let i = 1; i <= len1; i++) {
59
+ for (let j = 1; j <= len2; j++) {
60
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
61
+ matrix[i]![j] = Math.min(
62
+ matrix[i - 1]![j]! + 1, // deletion
63
+ matrix[i]![j - 1]! + 1, // insertion
64
+ matrix[i - 1]![j - 1]! + cost, // substitution
65
+ );
66
+ }
67
+ }
68
+
69
+ return matrix[len1]![len2]!;
70
+ }
71
+
72
+ /**
73
+ * Tier 2: Fuzzy matching using Levenshtein distance
74
+ * Returns similarity score 0-100
75
+ *
76
+ * Only used when Tier 1 (normalized matching) fails
77
+ */
78
+ export function fuzzyMatch(
79
+ payee1: string | null | undefined,
80
+ payee2: string | null | undefined,
81
+ ): number {
82
+ const norm1 = normalizePayee(payee1);
83
+ const norm2 = normalizePayee(payee2);
84
+
85
+ if (!norm1 || !norm2) return 0;
86
+ if (norm1 === norm2) return 100; // Perfect match
87
+
88
+ const distance = levenshteinDistance(norm1, norm2);
89
+ const maxLen = Math.max(norm1.length, norm2.length);
90
+
91
+ if (maxLen === 0) return 0;
92
+
93
+ // Convert to similarity percentage
94
+ const similarity = (1 - distance / maxLen) * 100;
95
+ return Math.max(0, Math.min(100, similarity));
96
+ }
97
+
98
+ /**
99
+ * Token-based similarity for better handling of word order differences
100
+ * Splits normalized payees into tokens and compares overlap
101
+ *
102
+ * @example
103
+ * "amazon prime video" vs "prime amazon" => higher similarity
104
+ */
105
+ export function tokenBasedSimilarity(
106
+ payee1: string | null | undefined,
107
+ payee2: string | null | undefined,
108
+ ): number {
109
+ const norm1 = normalizePayee(payee1);
110
+ const norm2 = normalizePayee(payee2);
111
+
112
+ if (!norm1 || !norm2) return 0;
113
+
114
+ // Split into tokens (any sequence of letters or digits)
115
+ const tokens1 = norm1.match(/[a-z]+|[0-9]+/g) || [];
116
+ const tokens2 = norm2.match(/[a-z]+|[0-9]+/g) || [];
117
+
118
+ if (tokens1.length === 0 || tokens2.length === 0) return 0;
119
+
120
+ // Count matching tokens
121
+ const set1 = new Set(tokens1);
122
+ const set2 = new Set(tokens2);
123
+
124
+ let matches = 0;
125
+ for (const token of set1) {
126
+ if (set2.has(token)) matches++;
127
+ }
128
+
129
+ // Jaccard similarity
130
+ const union = new Set([...set1, ...set2]).size;
131
+ return (matches / union) * 100;
132
+ }
133
+
134
+ /**
135
+ * Combined payee similarity using multiple strategies
136
+ * Returns the best similarity score from:
137
+ * - Normalized exact match (100 if matches)
138
+ * - Fuzzy match (Levenshtein distance)
139
+ * - Token-based match
140
+ */
141
+ export function payeeSimilarity(
142
+ payee1: string | null | undefined,
143
+ payee2: string | null | undefined,
144
+ ): number {
145
+ // Tier 1: Normalized exact match
146
+ if (normalizedMatch(payee1, payee2)) return 100;
147
+
148
+ // Tier 2: Fuzzy and token-based matching
149
+ const fuzzyScore = fuzzyMatch(payee1, payee2);
150
+ const tokenScore = tokenBasedSimilarity(payee1, payee2);
151
+
152
+ // Return the best score
153
+ return Math.max(fuzzyScore, tokenScore);
154
+ }
155
+
156
+ /**
157
+ * Check if payee contains a common substring
158
+ * Useful for matching "AMAZON.COM" to "Amazon Prime"
159
+ */
160
+ export function payeeContains(payee: string | null | undefined, substring: string): boolean {
161
+ const norm = normalizePayee(payee);
162
+ const normSub = normalizePayee(substring);
163
+
164
+ if (!norm || !normSub) return false;
165
+
166
+ return norm.includes(normSub);
167
+ }
@@ -0,0 +1,506 @@
1
+ import { randomUUID } from 'crypto';
2
+ import type {
3
+ ActionableRecommendation,
4
+ CreateTransactionRecommendation,
5
+ UpdateClearedRecommendation,
6
+ ReviewDuplicateRecommendation,
7
+ ManualReviewRecommendation,
8
+ RecommendationContext,
9
+ ReconciliationInsight,
10
+ TransactionMatch,
11
+ BankTransaction,
12
+ YNABTransaction,
13
+ } from './types.js';
14
+ import { toMoneyValueFromDecimal, fromMilli, toMilli } from '../../utils/money.js';
15
+
16
+ const RECOMMENDATION_VERSION = '1.0';
17
+
18
+ /**
19
+ * Confidence scores for different recommendation types
20
+ */
21
+ const CONFIDENCE = {
22
+ CREATE_EXACT_MATCH: 0.95,
23
+ NEAR_MATCH_REVIEW: 0.7,
24
+ REPEAT_AMOUNT: 0.75,
25
+ ANOMALY_REVIEW: 0.5,
26
+ UNMATCHED_BANK: 0.8,
27
+ UPDATE_CLEARED: 0.6,
28
+ } as const;
29
+
30
+ /**
31
+ * Priority order for sorting recommendations
32
+ */
33
+ const PRIORITY_ORDER = { high: 3, medium: 2, low: 1 } as const;
34
+
35
+ /**
36
+ * Generate actionable recommendations from reconciliation analysis.
37
+ *
38
+ * This function processes reconciliation analysis results and generates specific,
39
+ * executable recommendations for resolving discrepancies. It analyzes insights,
40
+ * unmatched transactions, and suggested matches to create prioritized actions.
41
+ *
42
+ * @param context - The recommendation context containing analysis results, IDs, and config
43
+ * @param context.account_id - The YNAB account ID for transaction operations
44
+ * @param context.budget_id - The YNAB budget ID (reserved for future category suggestions)
45
+ * @param context.analysis - The complete reconciliation analysis results
46
+ * @param context.matching_config - The matching configuration used during analysis
47
+ * @returns Array of actionable recommendations sorted by priority and confidence
48
+ *
49
+ * @example
50
+ * const recommendations = generateRecommendations({
51
+ * account_id: 'abc123',
52
+ * budget_id: 'budget-456',
53
+ * analysis: reconciliationAnalysis,
54
+ * matching_config: defaultConfig
55
+ * });
56
+ * // Returns recommendations like create_transaction, update_cleared, etc.
57
+ */
58
+ export function generateRecommendations(
59
+ context: RecommendationContext,
60
+ ): ActionableRecommendation[] {
61
+ const recommendations: ActionableRecommendation[] = [];
62
+
63
+ // Process insights from analyzer
64
+ for (const insight of context.analysis.insights) {
65
+ const recs = processInsight(insight, context);
66
+ recommendations.push(...recs);
67
+ }
68
+
69
+ // Process unmatched transactions
70
+ const unmatchedRecs = processUnmatchedTransactions(context);
71
+ recommendations.push(...unmatchedRecs);
72
+
73
+ // Sort by priority and confidence
74
+ return sortRecommendations(recommendations);
75
+ }
76
+
77
+ /**
78
+ * Process a single insight into recommendations
79
+ */
80
+ function processInsight(
81
+ insight: ReconciliationInsight,
82
+ context: RecommendationContext,
83
+ ): ActionableRecommendation[] {
84
+ switch (insight.type) {
85
+ case 'near_match':
86
+ return [createNearMatchRecommendation(insight, context)];
87
+
88
+ case 'repeat_amount':
89
+ return createRepeatAmountRecommendations(insight, context);
90
+
91
+ case 'anomaly':
92
+ return [createManualReviewRecommendation(insight, context)];
93
+
94
+ default:
95
+ return [];
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Create recommendation for suggested match with intelligent routing.
101
+ *
102
+ * This function handles three distinct scenarios based on the match characteristics:
103
+ *
104
+ * 1. **Potential Duplicate** (has YNAB transaction + confidence score):
105
+ * Returns review_duplicate recommendation for manual verification
106
+ *
107
+ * 2. **Combination Match** (multiple YNAB transactions matching one bank transaction):
108
+ * Returns manual_review recommendation to investigate complex matching scenario
109
+ *
110
+ * 3. **Missing Transaction** (no matching YNAB transaction):
111
+ * Returns create_transaction recommendation with complete parameters
112
+ *
113
+ * @param match - The transaction match containing bank transaction and optional YNAB candidates
114
+ * @param context - The recommendation context for account/budget IDs and analysis data
115
+ * @returns Appropriate recommendation type based on match characteristics
116
+ *
117
+ * @example
118
+ * // Scenario 1: Potential duplicate detected
119
+ * const dupRec = createSuggestedMatchRecommendation(
120
+ * { bank_transaction, ynab_transaction, confidence: 'high', confidence_score: 85 },
121
+ * context
122
+ * ); // Returns: review_duplicate
123
+ *
124
+ * @example
125
+ * // Scenario 2: Combination match (2+ YNAB transactions)
126
+ * const combRec = createSuggestedMatchRecommendation(
127
+ * { bank_transaction, candidates: [txn1, txn2], match_reason: 'combination_match' },
128
+ * context
129
+ * ); // Returns: manual_review
130
+ *
131
+ * @example
132
+ * // Scenario 3: Create missing transaction
133
+ * const createRec = createSuggestedMatchRecommendation(
134
+ * { bank_transaction, confidence: 'none' },
135
+ * context
136
+ * ); // Returns: create_transaction
137
+ */
138
+ function createSuggestedMatchRecommendation(
139
+ match: TransactionMatch,
140
+ context: RecommendationContext,
141
+ ): CreateTransactionRecommendation | ReviewDuplicateRecommendation | ManualReviewRecommendation {
142
+ const bankTxn = match.bank_transaction;
143
+
144
+ // If there's a suggested YNAB transaction, review as possible duplicate
145
+ if (match.ynab_transaction && match.confidence !== 'none') {
146
+ return {
147
+ id: randomUUID(),
148
+ action_type: 'review_duplicate',
149
+ priority: 'high',
150
+ confidence: Math.max(0, Math.min(1, match.confidence_score / 100)),
151
+ message: `Review possible match: ${bankTxn.payee}`,
152
+ reason: match.match_reason,
153
+ estimated_impact: toMoneyValueFromDecimal(
154
+ 0,
155
+ context.analysis.balance_info.current_cleared.currency,
156
+ ),
157
+ account_id: context.account_id,
158
+ metadata: {
159
+ version: RECOMMENDATION_VERSION,
160
+ created_at: new Date().toISOString(),
161
+ },
162
+ parameters: {
163
+ candidate_ids: [match.ynab_transaction.id],
164
+ bank_transaction: bankTxn,
165
+ suggested_match_id: match.ynab_transaction.id,
166
+ },
167
+ };
168
+ }
169
+
170
+ // Check for combination matches (multiple YNAB transactions that together match the bank transaction)
171
+ const isCombinationMatch =
172
+ match.match_reason === 'combination_match' || (match.candidates?.length ?? 0) > 1;
173
+
174
+ if (isCombinationMatch) {
175
+ return createCombinationReviewRecommendation(match, context);
176
+ }
177
+
178
+ // Otherwise suggest creating new transaction
179
+ const parameters: CreateTransactionRecommendation['parameters'] = {
180
+ account_id: context.account_id,
181
+ date: bankTxn.date,
182
+ amount: toMilli(bankTxn.amount), // Convert dollars to milliunits for create_transaction
183
+ payee_name: bankTxn.payee,
184
+ cleared: 'cleared',
185
+ approved: true,
186
+ };
187
+
188
+ if (bankTxn.memo) {
189
+ parameters.memo = bankTxn.memo;
190
+ }
191
+
192
+ return {
193
+ id: randomUUID(),
194
+ action_type: 'create_transaction',
195
+ priority: 'high',
196
+ confidence: CONFIDENCE.CREATE_EXACT_MATCH,
197
+ message: `Create transaction for ${bankTxn.payee}`,
198
+ reason: `This transaction exactly matches your discrepancy`,
199
+ estimated_impact: toMoneyValueFromDecimal(
200
+ bankTxn.amount,
201
+ context.analysis.balance_info.current_cleared.currency,
202
+ ),
203
+ account_id: context.account_id,
204
+ metadata: {
205
+ version: RECOMMENDATION_VERSION,
206
+ created_at: new Date().toISOString(),
207
+ },
208
+ parameters,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Create recommendation for combination match (multiple YNAB transactions matching one bank transaction)
214
+ */
215
+ function createCombinationReviewRecommendation(
216
+ match: TransactionMatch,
217
+ context: RecommendationContext,
218
+ ): ManualReviewRecommendation {
219
+ const bankTxn = match.bank_transaction;
220
+ const candidateIds = match.candidates?.map((candidate) => candidate.ynab_transaction.id) ?? [];
221
+
222
+ // Calculate total amount from candidates for context (convert from milliunits to decimal)
223
+ const candidateTotalAmount =
224
+ match.candidates?.reduce((sum, candidate) => {
225
+ const amount = candidate.ynab_transaction.amount;
226
+ if (!Number.isFinite(amount)) {
227
+ console.warn(`Invalid candidate amount: ${amount}`);
228
+ return sum;
229
+ }
230
+ return sum + fromMilli(amount);
231
+ }, 0) ?? 0;
232
+
233
+ return {
234
+ id: randomUUID(),
235
+ action_type: 'manual_review',
236
+ priority: 'medium',
237
+ confidence: CONFIDENCE.NEAR_MATCH_REVIEW,
238
+ message: `Review combination match: ${bankTxn.payee}`,
239
+ reason:
240
+ match.recommendation ??
241
+ 'Multiple YNAB transactions appear to match this bank transaction. Review before creating anything new.',
242
+ estimated_impact: toMoneyValueFromDecimal(
243
+ 0,
244
+ context.analysis.balance_info.current_cleared.currency,
245
+ ),
246
+ account_id: context.account_id,
247
+ metadata: {
248
+ version: RECOMMENDATION_VERSION,
249
+ created_at: new Date().toISOString(),
250
+ bank_transaction_amount: toMoneyValueFromDecimal(
251
+ bankTxn.amount,
252
+ context.analysis.balance_info.current_cleared.currency,
253
+ ),
254
+ candidate_total_amount: toMoneyValueFromDecimal(
255
+ candidateTotalAmount,
256
+ context.analysis.balance_info.current_cleared.currency,
257
+ ),
258
+ candidate_count: match.candidates?.length ?? 0,
259
+ },
260
+ parameters: {
261
+ issue_type: 'complex_match',
262
+ related_transactions: [
263
+ {
264
+ source: 'bank',
265
+ id: bankTxn.id,
266
+ description: bankTxn.payee,
267
+ },
268
+ ...candidateIds.map((id) => ({
269
+ source: 'ynab' as const,
270
+ id,
271
+ description:
272
+ match.candidates?.find((c) => c.ynab_transaction.id === id)?.ynab_transaction
273
+ .payee_name ?? 'Unknown',
274
+ })),
275
+ ],
276
+ },
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Create recommendation for near match insight (possible duplicate)
282
+ */
283
+ function createNearMatchRecommendation(
284
+ insight: ReconciliationInsight,
285
+ context: RecommendationContext,
286
+ ): ManualReviewRecommendation {
287
+ return {
288
+ id: randomUUID(),
289
+ action_type: 'manual_review',
290
+ priority: 'medium',
291
+ confidence: CONFIDENCE.NEAR_MATCH_REVIEW,
292
+ message: `Review: ${insight.title}`,
293
+ reason: insight.description,
294
+ estimated_impact: toMoneyValueFromDecimal(
295
+ 0,
296
+ context.analysis.balance_info.current_cleared.currency,
297
+ ),
298
+ account_id: context.account_id,
299
+ source_insight_id: insight.id,
300
+ metadata: {
301
+ version: RECOMMENDATION_VERSION,
302
+ created_at: new Date().toISOString(),
303
+ current_discrepancy: context.analysis.balance_info.discrepancy,
304
+ insight_severity: insight.severity,
305
+ },
306
+ parameters: {
307
+ issue_type: 'complex_match',
308
+ },
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Create recommendations for repeat amount pattern
314
+ */
315
+ function createRepeatAmountRecommendations(
316
+ insight: ReconciliationInsight,
317
+ context: RecommendationContext,
318
+ ): ManualReviewRecommendation[] {
319
+ // For repeat amounts, suggest manual review since we need to identify the specific transactions
320
+ return [
321
+ {
322
+ id: randomUUID(),
323
+ action_type: 'manual_review',
324
+ priority: 'medium',
325
+ confidence: CONFIDENCE.REPEAT_AMOUNT,
326
+ message: `Review recurring pattern: ${insight.title}`,
327
+ reason: insight.description,
328
+ estimated_impact: toMoneyValueFromDecimal(
329
+ 0,
330
+ context.analysis.balance_info.current_cleared.currency,
331
+ ),
332
+ account_id: context.account_id,
333
+ source_insight_id: insight.id,
334
+ metadata: {
335
+ version: RECOMMENDATION_VERSION,
336
+ created_at: new Date().toISOString(),
337
+ current_discrepancy: context.analysis.balance_info.discrepancy,
338
+ insight_severity: insight.severity,
339
+ },
340
+ parameters: {
341
+ issue_type: 'complex_match',
342
+ },
343
+ },
344
+ ];
345
+ }
346
+
347
+ /**
348
+ * Create manual review recommendation (fallback)
349
+ */
350
+ function createManualReviewRecommendation(
351
+ insight: ReconciliationInsight,
352
+ context: RecommendationContext,
353
+ ): ManualReviewRecommendation {
354
+ return {
355
+ id: randomUUID(),
356
+ action_type: 'manual_review',
357
+ priority: 'low',
358
+ confidence: CONFIDENCE.ANOMALY_REVIEW,
359
+ message: `Review: ${insight.title}`,
360
+ reason: insight.description,
361
+ estimated_impact: toMoneyValueFromDecimal(
362
+ 0,
363
+ context.analysis.balance_info.current_cleared.currency,
364
+ ),
365
+ account_id: context.account_id,
366
+ source_insight_id: insight.id,
367
+ metadata: {
368
+ version: RECOMMENDATION_VERSION,
369
+ created_at: new Date().toISOString(),
370
+ current_discrepancy: context.analysis.balance_info.discrepancy,
371
+ insight_severity: insight.severity,
372
+ },
373
+ parameters: {
374
+ issue_type: insight.severity === 'critical' ? 'large_discrepancy' : 'unknown',
375
+ },
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Process unmatched transactions into recommendations
381
+ */
382
+ function processUnmatchedTransactions(context: RecommendationContext): ActionableRecommendation[] {
383
+ const recommendations: ActionableRecommendation[] = [];
384
+
385
+ // Unmatched bank transactions → create_transaction
386
+ for (const bankTxn of context.analysis.unmatched_bank) {
387
+ recommendations.push(createUnmatchedBankRecommendation(bankTxn, context));
388
+ }
389
+
390
+ // Suggested matches → review as potential duplicates or auto-match
391
+ for (const match of context.analysis.suggested_matches) {
392
+ recommendations.push(createSuggestedMatchRecommendation(match, context));
393
+ }
394
+
395
+ // Unmatched YNAB uncleared → update_cleared (lower priority)
396
+ for (const ynabTxn of context.analysis.unmatched_ynab) {
397
+ if (ynabTxn.cleared === 'uncleared') {
398
+ recommendations.push(createUpdateClearedRecommendation(ynabTxn, context));
399
+ }
400
+ }
401
+
402
+ return recommendations;
403
+ }
404
+
405
+ /**
406
+ * Create a create_transaction recommendation for an unmatched bank transaction.
407
+ *
408
+ * Generates a recommendation to create a new YNAB transaction for a bank statement
409
+ * entry that has no corresponding transaction in YNAB. The recommendation includes
410
+ * complete parameters ready for execution via the create_transaction MCP tool.
411
+ *
412
+ * @param txn - The unmatched bank transaction
413
+ * @param context - The recommendation context for account ID and currency
414
+ * @returns create_transaction recommendation with medium priority and 0.8 confidence
415
+ */
416
+ function createUnmatchedBankRecommendation(
417
+ txn: BankTransaction,
418
+ context: RecommendationContext,
419
+ ): CreateTransactionRecommendation {
420
+ const parameters: CreateTransactionRecommendation['parameters'] = {
421
+ account_id: context.account_id,
422
+ date: txn.date,
423
+ amount: toMilli(txn.amount), // Convert dollars to milliunits for create_transaction
424
+ payee_name: txn.payee,
425
+ cleared: 'cleared',
426
+ approved: true,
427
+ };
428
+
429
+ if (txn.memo) {
430
+ parameters.memo = txn.memo;
431
+ }
432
+
433
+ return {
434
+ id: randomUUID(),
435
+ action_type: 'create_transaction',
436
+ priority: 'medium',
437
+ confidence: CONFIDENCE.UNMATCHED_BANK,
438
+ message: `Create missing transaction: ${txn.payee}`,
439
+ reason: 'Transaction appears on bank statement but not in YNAB',
440
+ estimated_impact: toMoneyValueFromDecimal(
441
+ txn.amount,
442
+ context.analysis.balance_info.current_cleared.currency,
443
+ ),
444
+ account_id: context.account_id,
445
+ metadata: {
446
+ version: RECOMMENDATION_VERSION,
447
+ created_at: new Date().toISOString(),
448
+ },
449
+ parameters,
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Create an update_cleared recommendation for an unmatched uncleared YNAB transaction.
455
+ *
456
+ * Generates a recommendation to mark an existing YNAB transaction as cleared. This is
457
+ * used when a transaction exists in YNAB but is still marked as "uncleared" and may
458
+ * correspond to a bank statement entry. This is a low-priority suggestion since the
459
+ * transaction already exists and only needs status update.
460
+ *
461
+ * @param txn - The unmatched YNAB transaction (must have cleared status of 'uncleared')
462
+ * @param context - The recommendation context for account ID and currency
463
+ * @returns update_cleared recommendation with low priority and 0.6 confidence
464
+ */
465
+ function createUpdateClearedRecommendation(
466
+ txn: YNABTransaction,
467
+ context: RecommendationContext,
468
+ ): UpdateClearedRecommendation {
469
+ return {
470
+ id: randomUUID(),
471
+ action_type: 'update_cleared',
472
+ priority: 'low',
473
+ confidence: CONFIDENCE.UPDATE_CLEARED,
474
+ message: `Mark transaction as cleared: ${txn.payee_name || 'Unknown'}`,
475
+ reason: 'Transaction exists in YNAB but not yet cleared',
476
+ estimated_impact: toMoneyValueFromDecimal(
477
+ 0,
478
+ context.analysis.balance_info.current_cleared.currency,
479
+ ),
480
+ account_id: context.account_id,
481
+ metadata: {
482
+ version: RECOMMENDATION_VERSION,
483
+ created_at: new Date().toISOString(),
484
+ },
485
+ parameters: {
486
+ transaction_id: txn.id,
487
+ cleared: 'cleared',
488
+ },
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Sort recommendations by priority and confidence
494
+ */
495
+ function sortRecommendations(
496
+ recommendations: ActionableRecommendation[],
497
+ ): ActionableRecommendation[] {
498
+ return recommendations.sort((a, b) => {
499
+ // Sort by priority first
500
+ const priorityDiff = PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority];
501
+ if (priorityDiff !== 0) return priorityDiff;
502
+
503
+ // Then by confidence
504
+ return b.confidence - a.confidence;
505
+ });
506
+ }