@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,90 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import * as ynab from 'ynab';
3
+ import { z } from 'zod/v4';
4
+ import { withToolErrorHandling } from '../types/index.js';
5
+ import { responseFormatter } from '../server/responseFormatter.js';
6
+
7
+ /**
8
+ * Schema for ynab:convert_amount tool parameters
9
+ */
10
+ export const ConvertAmountSchema = z
11
+ .object({
12
+ amount: z.number().finite(),
13
+ to_milliunits: z.boolean(),
14
+ })
15
+ .strict();
16
+
17
+ export type ConvertAmountParams = z.infer<typeof ConvertAmountSchema>;
18
+
19
+ /**
20
+ * Handles the ynab:get_user tool call
21
+ * Gets information about the authenticated user
22
+ */
23
+ export async function handleGetUser(ynabAPI: ynab.API): Promise<CallToolResult> {
24
+ return await withToolErrorHandling(
25
+ async () => {
26
+ const response = await ynabAPI.user.getUser();
27
+ const userInfo = response.data.user;
28
+
29
+ const user = {
30
+ id: userInfo.id,
31
+ };
32
+
33
+ return {
34
+ content: [
35
+ {
36
+ type: 'text',
37
+ text: responseFormatter.format({ user }),
38
+ },
39
+ ],
40
+ };
41
+ },
42
+ 'ynab:get_user',
43
+ 'getting user information',
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Handles the ynab:convert_amount tool call
49
+ * Converts between dollars and milliunits with integer arithmetic for precision
50
+ */
51
+ export async function handleConvertAmount(params: ConvertAmountParams): Promise<CallToolResult> {
52
+ return await withToolErrorHandling(
53
+ async () => {
54
+ const { amount, to_milliunits } = params;
55
+
56
+ let result: number;
57
+ let description: string;
58
+
59
+ if (to_milliunits) {
60
+ // Convert from dollars to milliunits
61
+ // Use integer arithmetic to avoid floating-point precision issues
62
+ result = Math.round(amount * 1000);
63
+ description = `$${amount.toFixed(2)} = ${result} milliunits`;
64
+ } else {
65
+ // Convert from milliunits to dollars
66
+ // Assume input amount is in milliunits
67
+ result = amount / 1000;
68
+ description = `${amount} milliunits = $${result.toFixed(2)}`;
69
+ }
70
+
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: responseFormatter.format({
76
+ conversion: {
77
+ original_amount: amount,
78
+ converted_amount: result,
79
+ to_milliunits,
80
+ description,
81
+ },
82
+ }),
83
+ },
84
+ ],
85
+ };
86
+ },
87
+ 'ynab:convert_amount',
88
+ 'converting amount',
89
+ );
90
+ }
@@ -0,0 +1 @@
1
+ # This file ensures the types directory is tracked by git
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AuthenticationError, ConfigurationError } from '../index';
3
+
4
+ describe('Error Classes', () => {
5
+ describe('AuthenticationError', () => {
6
+ it('should create error with correct name and message', () => {
7
+ const message = 'Invalid access token';
8
+ const error = new AuthenticationError(message);
9
+
10
+ expect(error).toBeInstanceOf(Error);
11
+ expect(error).toBeInstanceOf(AuthenticationError);
12
+ expect(error.name).toBe('AuthenticationError');
13
+ expect(error.message).toBe(message);
14
+ });
15
+
16
+ it('should be throwable and catchable', () => {
17
+ const message = 'Token expired';
18
+
19
+ expect(() => {
20
+ throw new AuthenticationError(message);
21
+ }).toThrow(AuthenticationError);
22
+
23
+ expect(() => {
24
+ throw new AuthenticationError(message);
25
+ }).toThrow(message);
26
+ });
27
+ });
28
+
29
+ describe('ConfigurationError', () => {
30
+ it('should create error with correct name and message', () => {
31
+ const message = 'Missing environment variable';
32
+ const error = new ConfigurationError(message);
33
+
34
+ expect(error).toBeInstanceOf(Error);
35
+ expect(error).toBeInstanceOf(ConfigurationError);
36
+ expect(error.name).toBe('ConfigurationError');
37
+ expect(error.message).toBe(message);
38
+ });
39
+
40
+ it('should be throwable and catchable', () => {
41
+ const message = 'Invalid configuration';
42
+
43
+ expect(() => {
44
+ throw new ConfigurationError(message);
45
+ }).toThrow(ConfigurationError);
46
+
47
+ expect(() => {
48
+ throw new ConfigurationError(message);
49
+ }).toThrow(message);
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Type definitions for YNAB MCP Server
3
+ */
4
+
5
+ export interface AuthenticationConfig {
6
+ accessToken: string;
7
+ validateToken(): Promise<boolean>;
8
+ }
9
+
10
+ export interface ServerConfig {
11
+ accessToken: string;
12
+ defaultBudgetId?: string;
13
+ }
14
+
15
+ export class AuthenticationError extends Error {
16
+ constructor(message: string) {
17
+ super(message);
18
+ this.name = 'AuthenticationError';
19
+ }
20
+ }
21
+
22
+ export class ConfigurationError extends Error {
23
+ constructor(message: string) {
24
+ super(message);
25
+ this.name = 'ConfigurationError';
26
+ }
27
+ }
28
+
29
+ // Re-export error handling types for convenience
30
+ export {
31
+ ErrorHandler,
32
+ YNABAPIError,
33
+ ValidationError,
34
+ YNABErrorCode,
35
+ SecurityErrorCode,
36
+ type ErrorResponse,
37
+ handleToolError,
38
+ withToolErrorHandling,
39
+ } from '../server/errorHandler.js';
40
+
41
+ // Re-export security modules
42
+ export {
43
+ RateLimiter,
44
+ RateLimitError,
45
+ globalRateLimiter,
46
+ type RateLimitConfig,
47
+ type RateLimitInfo,
48
+ } from '../server/rateLimiter.js';
49
+
50
+ export {
51
+ RequestLogger,
52
+ globalRequestLogger,
53
+ type LogEntry,
54
+ type LoggerConfig,
55
+ } from '../server/requestLogger.js';
56
+
57
+ export {
58
+ SecurityMiddleware,
59
+ withSecurityWrapper,
60
+ type SecurityContext,
61
+ } from '../server/securityMiddleware.js';
62
+
63
+ // Re-export tool annotation types
64
+ export type { MCPToolAnnotations } from './toolAnnotations.js';
65
+
66
+ // Re-export tool registry types for convenience
67
+ export type { ToolDefinition } from '../server/toolRegistry.js';
@@ -0,0 +1,35 @@
1
+ type IntegrationTestTier = 'core' | 'domain' | 'full';
2
+ type IntegrationTestDomain =
3
+ | 'budgets'
4
+ | 'accounts'
5
+ | 'transactions'
6
+ | 'categories'
7
+ | 'payees'
8
+ | 'months'
9
+ | 'delta'
10
+ | 'reconciliation'
11
+ | 'utility'
12
+ | 'security'
13
+ | 'server'
14
+ | 'workflows'
15
+ | (string & {});
16
+
17
+ export interface IntegrationTestMeta {
18
+ tier: IntegrationTestTier;
19
+ domain: IntegrationTestDomain;
20
+ }
21
+
22
+ declare module '@vitest/runner' {
23
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
24
+ interface TaskMeta extends Partial<IntegrationTestMeta> {}
25
+
26
+ interface TestOptions {
27
+ meta?: IntegrationTestMeta;
28
+ }
29
+ }
30
+
31
+ declare module 'vitest' {
32
+ interface TestOptions {
33
+ meta?: IntegrationTestMeta;
34
+ }
35
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Type-safe MCP tool annotations interface
3
+ *
4
+ * These annotations are advisory hints per the MCP specification to help AI clients
5
+ * understand tool behavior. They do not enforce behavior at runtime.
6
+ *
7
+ * @see https://blog.marcnuri.com/mcp-tool-annotations-introduction
8
+ */
9
+ export interface MCPToolAnnotations {
10
+ /**
11
+ * Human-readable title for UI display
12
+ *
13
+ * @example "YNAB: Delete Transaction"
14
+ */
15
+ title?: string;
16
+
17
+ /**
18
+ * Indicates tool only reads data without modifications
19
+ *
20
+ * @example true for list_budgets, get_account
21
+ */
22
+ readOnlyHint?: boolean;
23
+
24
+ /**
25
+ * For non-read-only tools, indicates irreversible operations
26
+ *
27
+ * @example true for delete_transaction
28
+ */
29
+ destructiveHint?: boolean;
30
+
31
+ /**
32
+ * Indicates repeated identical calls have same effect
33
+ *
34
+ * @example true for set_default_budget
35
+ */
36
+ idempotentHint?: boolean;
37
+
38
+ /**
39
+ * Indicates tool interacts with external systems like YNAB API
40
+ *
41
+ * @example true for all YNAB tools that call the YNAB API
42
+ */
43
+ openWorldHint?: boolean;
44
+ }
@@ -0,0 +1,170 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ formatYNABMonth,
4
+ formatISODate,
5
+ getCurrentMonth,
6
+ getToday,
7
+ getHistoricalMonths,
8
+ subtractMonths,
9
+ isValidISODate,
10
+ isValidYNABMonth,
11
+ yearMonthToYNABMonth,
12
+ } from '../dateUtils.js';
13
+
14
+ describe('dateUtils', () => {
15
+ describe('formatYNABMonth', () => {
16
+ it('should format date as YYYY-MM-01', () => {
17
+ const date = new Date('2024-03-15T10:30:00.000Z');
18
+ expect(formatYNABMonth(date)).toBe('2024-03-01');
19
+ });
20
+
21
+ it('should handle December correctly', () => {
22
+ const date = new Date('2024-12-31T23:59:59.999Z');
23
+ expect(formatYNABMonth(date)).toBe('2024-12-01');
24
+ });
25
+ });
26
+
27
+ describe('formatISODate', () => {
28
+ it('should format date as YYYY-MM-DD', () => {
29
+ const date = new Date('2024-03-15T10:30:00.000Z');
30
+ expect(formatISODate(date)).toBe('2024-03-15');
31
+ });
32
+ });
33
+
34
+ describe('getCurrentMonth', () => {
35
+ it('should return current month in YYYY-MM-01 format', () => {
36
+ const result = getCurrentMonth();
37
+ expect(result).toMatch(/^\d{4}-\d{2}-01$/);
38
+ });
39
+ });
40
+
41
+ describe('getToday', () => {
42
+ it('should return today in YYYY-MM-DD format', () => {
43
+ const result = getToday();
44
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
45
+ });
46
+ });
47
+
48
+ describe('getHistoricalMonths', () => {
49
+ it('should generate correct number of historical months', () => {
50
+ const baseDate = new Date('2024-03-15');
51
+ const months = getHistoricalMonths(3, baseDate);
52
+
53
+ expect(months).toHaveLength(3);
54
+ expect(months[0]).toBe('2024-03-01'); // Current month (i=0)
55
+ expect(months[1]).toBe('2024-02-01'); // 1 month back
56
+ expect(months[2]).toBe('2024-01-01'); // 2 months back
57
+ });
58
+
59
+ it('should handle year boundary correctly', () => {
60
+ const baseDate = new Date('2024-01-15');
61
+ const months = getHistoricalMonths(3, baseDate);
62
+
63
+ expect(months).toHaveLength(3);
64
+ expect(months[0]).toBe('2024-01-01');
65
+ expect(months[1]).toBe('2023-12-01');
66
+ expect(months[2]).toBe('2023-11-01');
67
+ });
68
+
69
+ it('should use current date when no base date provided', () => {
70
+ const months = getHistoricalMonths(2);
71
+ expect(months).toHaveLength(2);
72
+ expect(months[0]).toMatch(/^\d{4}-\d{2}-01$/);
73
+ expect(months[1]).toMatch(/^\d{4}-\d{2}-01$/);
74
+ });
75
+
76
+ it('should generate 6 months correctly (bug test case)', () => {
77
+ const baseDate = new Date('2025-08-15');
78
+ const months = getHistoricalMonths(6, baseDate);
79
+
80
+ expect(months).toEqual([
81
+ '2025-08-01',
82
+ '2025-07-01',
83
+ '2025-06-01',
84
+ '2025-05-01',
85
+ '2025-04-01',
86
+ '2025-03-01',
87
+ ]);
88
+
89
+ // Ensure no duplicates (the original bug)
90
+ const uniqueMonths = new Set(months);
91
+ expect(uniqueMonths.size).toBe(6);
92
+ });
93
+ });
94
+
95
+ describe('subtractMonths', () => {
96
+ it('should subtract months correctly', () => {
97
+ const baseDate = new Date('2024-03-15');
98
+ const result = subtractMonths(baseDate, 2);
99
+
100
+ expect(result.getFullYear()).toBe(2024);
101
+ expect(result.getMonth()).toBe(0); // January (0-indexed)
102
+ // Note: When subtracting months, if the target month doesn't have enough days,
103
+ // JavaScript adjusts the date (e.g., March 15 - 2 months might become January 14)
104
+ expect(result.getDate()).toBeGreaterThanOrEqual(14);
105
+ expect(result.getDate()).toBeLessThanOrEqual(15);
106
+ });
107
+
108
+ it('should handle year boundary', () => {
109
+ const baseDate = new Date('2024-01-15');
110
+ const result = subtractMonths(baseDate, 2);
111
+
112
+ expect(result.getFullYear()).toBe(2023);
113
+ expect(result.getMonth()).toBe(10); // November (0-indexed)
114
+ });
115
+ });
116
+
117
+ describe('isValidISODate', () => {
118
+ it('should validate correct ISO dates', () => {
119
+ expect(isValidISODate('2024-03-15')).toBe(true);
120
+ expect(isValidISODate('2024-12-31')).toBe(true);
121
+ expect(isValidISODate('2024-02-29')).toBe(true); // Leap year
122
+ });
123
+
124
+ it('should reject invalid formats', () => {
125
+ expect(isValidISODate('03/15/2024')).toBe(false);
126
+ expect(isValidISODate('2024-3-15')).toBe(false);
127
+ expect(isValidISODate('2024-03-5')).toBe(false);
128
+ expect(isValidISODate('24-03-15')).toBe(false);
129
+ expect(isValidISODate('not-a-date')).toBe(false);
130
+ });
131
+
132
+ it('should reject invalid dates with correct format', () => {
133
+ expect(isValidISODate('2024-02-30')).toBe(false); // Invalid date
134
+ expect(isValidISODate('2024-13-01')).toBe(false); // Invalid month
135
+ expect(isValidISODate('2023-02-29')).toBe(false); // Not a leap year
136
+ });
137
+ });
138
+
139
+ describe('isValidYNABMonth', () => {
140
+ it('should validate correct YNAB month format', () => {
141
+ expect(isValidYNABMonth('2024-03-01')).toBe(true);
142
+ expect(isValidYNABMonth('2024-12-01')).toBe(true);
143
+ });
144
+
145
+ it('should reject invalid formats', () => {
146
+ expect(isValidYNABMonth('2024-03-15')).toBe(false);
147
+ expect(isValidYNABMonth('2024-03')).toBe(false);
148
+ expect(isValidYNABMonth('03-01-2024')).toBe(false);
149
+ expect(isValidYNABMonth('not-a-month')).toBe(false);
150
+ });
151
+
152
+ it('should reject invalid months with correct format', () => {
153
+ expect(isValidYNABMonth('2024-13-01')).toBe(false); // Invalid month
154
+ expect(isValidYNABMonth('2024-00-01')).toBe(false); // Invalid month
155
+ });
156
+ });
157
+
158
+ describe('yearMonthToYNABMonth', () => {
159
+ it('should convert YYYY-MM to YYYY-MM-01', () => {
160
+ expect(yearMonthToYNABMonth('2024-03')).toBe('2024-03-01');
161
+ expect(yearMonthToYNABMonth('2024-12')).toBe('2024-12-01');
162
+ });
163
+
164
+ it('should throw error for invalid format', () => {
165
+ expect(() => yearMonthToYNABMonth('2024-3')).toThrow('Invalid year-month format');
166
+ expect(() => yearMonthToYNABMonth('24-03')).toThrow('Invalid year-month format');
167
+ expect(() => yearMonthToYNABMonth('not-valid')).toThrow('Invalid year-month format');
168
+ });
169
+ });
170
+ });
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type * as ynab from 'ynab';
3
+ import {
4
+ toMilli,
5
+ fromMilli,
6
+ assertMilli,
7
+ addMilli,
8
+ inWindow,
9
+ moneyDirection,
10
+ formatMoney,
11
+ toMoneyValue,
12
+ toMoneyValueFromDecimal,
13
+ getDecimalDigits,
14
+ getCurrencyCode,
15
+ } from '../money.js';
16
+
17
+ describe('money utilities', () => {
18
+ describe('toMilli', () => {
19
+ it('converts dollars to milliunits correctly', () => {
20
+ expect(toMilli(1.23)).toBe(1230);
21
+ expect(toMilli(-5.67)).toBe(-5670);
22
+ expect(toMilli('10.50')).toBe(10500);
23
+ });
24
+
25
+ it('throws for invalid amounts', () => {
26
+ expect(() => toMilli(Number.MAX_SAFE_INTEGER)).toThrow('Invalid/unsafe amount');
27
+ expect(() => toMilli('invalid')).toThrow('Invalid/unsafe amount');
28
+ });
29
+ });
30
+
31
+ describe('fromMilli', () => {
32
+ it('converts milliunits with 2 decimal digits (USD) by default', () => {
33
+ expect(fromMilli(1230)).toBe(1.23);
34
+ expect(fromMilli(-5670)).toBe(-5.67);
35
+ expect(fromMilli(0)).toBe(0);
36
+ });
37
+
38
+ it('converts milliunits with 0 decimal digits (JPY)', () => {
39
+ expect(fromMilli(123000, 0)).toBe(123);
40
+ expect(fromMilli(1000, 0)).toBe(1);
41
+ });
42
+
43
+ it('converts milliunits with 3 decimal digits (BHD)', () => {
44
+ expect(fromMilli(1234, 3)).toBe(1.234);
45
+ expect(fromMilli(500, 3)).toBe(0.5);
46
+ });
47
+
48
+ it('converts milliunits with explicit 2 decimal digits', () => {
49
+ expect(fromMilli(1230, 2)).toBe(1.23);
50
+ });
51
+ });
52
+
53
+ describe('assertMilli', () => {
54
+ it('passes for safe integers', () => {
55
+ expect(() => assertMilli(1000)).not.toThrow();
56
+ expect(() => assertMilli(-500)).not.toThrow();
57
+ expect(() => assertMilli(0)).not.toThrow();
58
+ });
59
+
60
+ it('throws for non-safe integers', () => {
61
+ expect(() => assertMilli(1.5)).toThrow('Expected safe integer milliunits');
62
+ expect(() => assertMilli(Number.MAX_SAFE_INTEGER + 1)).toThrow(
63
+ 'Expected safe integer milliunits',
64
+ );
65
+ });
66
+ });
67
+
68
+ describe('addMilli', () => {
69
+ it('adds milliunits correctly', () => {
70
+ expect(addMilli(1000, 2000)).toBe(3000);
71
+ expect(addMilli(-500, 300)).toBe(-200);
72
+ });
73
+
74
+ it('throws on overflow', () => {
75
+ expect(() => addMilli(Number.MAX_SAFE_INTEGER, 1)).toThrow('Milliunit sum overflow');
76
+ });
77
+ });
78
+
79
+ describe('inWindow', () => {
80
+ it('checks date windows correctly', () => {
81
+ expect(inWindow('2024-01-15', '2024-01-01', '2024-01-31')).toBe(true);
82
+ expect(inWindow('2023-12-31', '2024-01-01', '2024-01-31')).toBe(false);
83
+ expect(inWindow('2024-02-01', '2024-01-01', '2024-01-31')).toBe(false);
84
+ });
85
+
86
+ it('handles optional bounds', () => {
87
+ expect(inWindow('2024-01-15', undefined, '2024-01-31')).toBe(true);
88
+ expect(inWindow('2024-01-15', '2024-01-01', undefined)).toBe(true);
89
+ expect(inWindow('2024-01-15', undefined, undefined)).toBe(true);
90
+ });
91
+ });
92
+
93
+ describe('currency format helpers', () => {
94
+ it('extracts decimal digits from currency format', () => {
95
+ const usdFormat: ynab.CurrencyFormat = {
96
+ iso_code: 'USD',
97
+ example_format: '$1,234.56',
98
+ decimal_digits: 2,
99
+ decimal_separator: '.',
100
+ symbol_first: true,
101
+ group_separator: ',',
102
+ currency_symbol: '$',
103
+ display_symbol: true,
104
+ };
105
+ expect(getDecimalDigits(usdFormat)).toBe(2);
106
+
107
+ const jpyFormat: ynab.CurrencyFormat = {
108
+ iso_code: 'JPY',
109
+ example_format: '¥1,234',
110
+ decimal_digits: 0,
111
+ decimal_separator: '.',
112
+ symbol_first: true,
113
+ group_separator: ',',
114
+ currency_symbol: '¥',
115
+ display_symbol: true,
116
+ };
117
+ expect(getDecimalDigits(jpyFormat)).toBe(0);
118
+ });
119
+
120
+ it('returns default decimal digits for null/undefined', () => {
121
+ expect(getDecimalDigits(null)).toBe(2);
122
+ expect(getDecimalDigits(undefined)).toBe(2);
123
+ });
124
+
125
+ it('extracts currency code from currency format', () => {
126
+ const eurFormat: ynab.CurrencyFormat = {
127
+ iso_code: 'EUR',
128
+ example_format: '€1.234,56',
129
+ decimal_digits: 2,
130
+ decimal_separator: ',',
131
+ symbol_first: false,
132
+ group_separator: '.',
133
+ currency_symbol: '€',
134
+ display_symbol: true,
135
+ };
136
+ expect(getCurrencyCode(eurFormat)).toBe('EUR');
137
+ });
138
+
139
+ it('returns default currency for null/undefined', () => {
140
+ expect(getCurrencyCode(null)).toBe('USD');
141
+ expect(getCurrencyCode(undefined)).toBe('USD');
142
+ });
143
+ });
144
+
145
+ describe('moneyValue helpers', () => {
146
+ it('derives direction correctly', () => {
147
+ expect(moneyDirection(0)).toBe('balanced');
148
+ expect(moneyDirection(1500)).toBe('credit');
149
+ expect(moneyDirection(-2500)).toBe('debit');
150
+ });
151
+
152
+ it('formats milliunits into currency strings with default 2 decimals', () => {
153
+ expect(formatMoney(1234)).toBe('$1.23');
154
+ expect(formatMoney(-9870)).toBe('-$9.87');
155
+ });
156
+
157
+ it('formats milliunits with custom currency format', () => {
158
+ expect(formatMoney(123000, 'JPY', 0)).toBe('¥123');
159
+ expect(formatMoney(1234, 'USD', 2)).toBe('$1.23');
160
+ expect(formatMoney(1234, 'EUR', 3)).toBe('€1.234'); // 3 decimal digits requested
161
+ });
162
+
163
+ it('creates money values from milliunits with default 2 decimals', () => {
164
+ const value = toMoneyValue(22220);
165
+ expect(value.value).toBe(22.22);
166
+ expect(value.value_display).toBe('$22.22');
167
+ expect(value.direction).toBe('credit');
168
+ });
169
+
170
+ it('creates currency-aware money values with decimal digits', () => {
171
+ const valueJPY = toMoneyValue(123000, 'JPY', 0);
172
+ expect(valueJPY.value).toBe(123);
173
+ expect(valueJPY.value_display).toBe('¥123');
174
+ expect(valueJPY.currency).toBe('JPY');
175
+ expect(valueJPY.direction).toBe('credit');
176
+
177
+ const valueUSD = toMoneyValue(1234, 'USD', 2);
178
+ expect(valueUSD.value).toBe(1.23);
179
+ expect(valueUSD.value_display).toBe('$1.23');
180
+ });
181
+
182
+ it('creates money values from decimal amounts', () => {
183
+ const value = toMoneyValueFromDecimal(-45.67);
184
+ expect(value.value_milliunits).toBe(-45670);
185
+ expect(value.value_display).toBe('-$45.67');
186
+ expect(value.direction).toBe('debit');
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Utility functions for converting between YNAB milliunits and dollars
3
+ */
4
+
5
+ /**
6
+ * Converts an amount from milliunits to dollars
7
+ * @param milliunits - Amount in milliunits (1000 milliunits = $1.00)
8
+ * @returns Amount in dollars as a number with 2 decimal places
9
+ */
10
+ export function milliunitsToAmount(milliunits: number): number {
11
+ return Math.round(milliunits) / 1000;
12
+ }
13
+
14
+ /**
15
+ * Converts an amount from dollars to milliunits
16
+ * @param amount - Amount in dollars
17
+ * @returns Amount in milliunits
18
+ */
19
+ export function amountToMilliunits(amount: number): number {
20
+ return Math.round(amount * 1000);
21
+ }
22
+
23
+ /**
24
+ * Formats an amount from milliunits to a currency string
25
+ * @param milliunits - Amount in milliunits
26
+ * @param currencySymbol - Currency symbol (default: '$')
27
+ * @returns Formatted currency string
28
+ */
29
+ export function formatAmount(milliunits: number, currencySymbol = '$'): string {
30
+ const amount = milliunitsToAmount(milliunits);
31
+ return `${currencySymbol}${amount.toFixed(2)}`;
32
+ }