@opentabs-dev/opentabs-plugin-ynab 0.0.85 → 0.0.86

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 (68) hide show
  1. package/dist/adapter.iife.js +870 -523
  2. package/dist/adapter.iife.js.map +4 -4
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +11 -16
  6. package/dist/index.js.map +1 -1
  7. package/dist/tools/create-transaction.d.ts +2 -2
  8. package/dist/tools/create-transaction.d.ts.map +1 -1
  9. package/dist/tools/create-transaction.js +63 -22
  10. package/dist/tools/create-transaction.js.map +1 -1
  11. package/dist/tools/delete-transaction.d.ts.map +1 -1
  12. package/dist/tools/delete-transaction.js +22 -7
  13. package/dist/tools/delete-transaction.js.map +1 -1
  14. package/dist/tools/get-account.d.ts.map +1 -1
  15. package/dist/tools/get-account.js +3 -3
  16. package/dist/tools/get-account.js.map +1 -1
  17. package/dist/tools/get-month.d.ts +2 -1
  18. package/dist/tools/get-month.d.ts.map +1 -1
  19. package/dist/tools/get-month.js +10 -33
  20. package/dist/tools/get-month.js.map +1 -1
  21. package/dist/tools/get-plan.d.ts.map +1 -1
  22. package/dist/tools/get-plan.js +12 -5
  23. package/dist/tools/get-plan.js.map +1 -1
  24. package/dist/tools/get-transaction.d.ts +2 -2
  25. package/dist/tools/get-transaction.d.ts.map +1 -1
  26. package/dist/tools/get-transaction.js +5 -4
  27. package/dist/tools/get-transaction.js.map +1 -1
  28. package/dist/tools/list-accounts.d.ts.map +1 -1
  29. package/dist/tools/list-accounts.js +4 -4
  30. package/dist/tools/list-accounts.js.map +1 -1
  31. package/dist/tools/list-categories.d.ts.map +1 -1
  32. package/dist/tools/list-categories.js +9 -29
  33. package/dist/tools/list-categories.js.map +1 -1
  34. package/dist/tools/list-months.d.ts +1 -1
  35. package/dist/tools/list-months.d.ts.map +1 -1
  36. package/dist/tools/list-months.js +5 -14
  37. package/dist/tools/list-months.js.map +1 -1
  38. package/dist/tools/list-payees.d.ts.map +1 -1
  39. package/dist/tools/list-payees.js +3 -3
  40. package/dist/tools/list-payees.js.map +1 -1
  41. package/dist/tools/list-scheduled-transactions.d.ts.map +1 -1
  42. package/dist/tools/list-scheduled-transactions.js +7 -5
  43. package/dist/tools/list-scheduled-transactions.js.map +1 -1
  44. package/dist/tools/list-transactions.d.ts +4 -2
  45. package/dist/tools/list-transactions.d.ts.map +1 -1
  46. package/dist/tools/list-transactions.js +40 -8
  47. package/dist/tools/list-transactions.js.map +1 -1
  48. package/dist/tools/move-category-budget.d.ts +24 -0
  49. package/dist/tools/move-category-budget.d.ts.map +1 -0
  50. package/dist/tools/move-category-budget.js +105 -0
  51. package/dist/tools/move-category-budget.js.map +1 -0
  52. package/dist/tools/schemas.d.ts +105 -27
  53. package/dist/tools/schemas.d.ts.map +1 -1
  54. package/dist/tools/schemas.js +176 -27
  55. package/dist/tools/schemas.js.map +1 -1
  56. package/dist/tools/update-category-budget.d.ts.map +1 -1
  57. package/dist/tools/update-category-budget.js +55 -27
  58. package/dist/tools/update-category-budget.js.map +1 -1
  59. package/dist/tools/update-transaction.d.ts +3 -2
  60. package/dist/tools/update-transaction.d.ts.map +1 -1
  61. package/dist/tools/update-transaction.js +84 -31
  62. package/dist/tools/update-transaction.js.map +1 -1
  63. package/dist/tools.json +192 -43
  64. package/dist/ynab-api.d.ts +4 -1
  65. package/dist/ynab-api.d.ts.map +1 -1
  66. package/dist/ynab-api.js +47 -26
  67. package/dist/ynab-api.js.map +1 -1
  68. package/package.json +3 -3
@@ -329,162 +329,6 @@
329
329
  configSchema;
330
330
  };
331
331
 
332
- // src/ynab-api.ts
333
- var generateDeviceId = () => crypto.randomUUID();
334
- var extractPlanId = () => {
335
- const url2 = getCurrentUrl();
336
- const match = url2.match(/app\.ynab\.com\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
337
- return match?.[1] ?? null;
338
- };
339
- var getAuth = () => {
340
- const cached2 = getAuthCache("ynab");
341
- if (cached2?.sessionToken && cached2.planId) return cached2;
342
- const sessionToken = getMetaContent("session-token");
343
- if (!sessionToken) return null;
344
- const user = getPageGlobal("YNAB_CLIENT_CONSTANTS.USER");
345
- if (!user?.id) return null;
346
- const planId = extractPlanId();
347
- if (!planId) return null;
348
- const deviceId = cached2?.deviceId ?? generateDeviceId();
349
- const auth = {
350
- sessionToken,
351
- deviceId,
352
- userId: user.id,
353
- planId
354
- };
355
- setAuthCache("ynab", auth);
356
- return auth;
357
- };
358
- var isAuthenticated = () => getAuth() !== null;
359
- var waitForAuth = () => waitUntil(() => isAuthenticated(), { interval: 500, timeout: 5e3 }).then(
360
- () => true,
361
- () => false
362
- );
363
- var getPlanId = () => {
364
- const auth = getAuth();
365
- if (!auth) throw ToolError.auth("Not authenticated \u2014 please log in to YNAB.");
366
- return auth.planId;
367
- };
368
- var getHeaders = () => {
369
- const auth = getAuth();
370
- if (!auth) throw ToolError.auth("Not authenticated \u2014 please log in to YNAB.");
371
- const appVersion = getPageGlobal("YNAB_CLIENT_CONSTANTS.YNAB_APP_VERSION");
372
- const headers = {
373
- "X-Session-Token": auth.sessionToken,
374
- "X-YNAB-Device-Id": auth.deviceId,
375
- "X-YNAB-Device-OS": "web",
376
- "X-Requested-With": "XMLHttpRequest",
377
- Accept: "application/json, text/javascript, */*; q=0.01"
378
- };
379
- if (appVersion) headers["X-YNAB-Device-App-Version"] = appVersion;
380
- return headers;
381
- };
382
- var handleApiError = async (response, context) => {
383
- const errorBody = (await response.text().catch(() => "")).substring(0, 512);
384
- if (response.status === 426) {
385
- clearAuthCache("ynab");
386
- throw ToolError.auth(
387
- "YNAB requires an app update (426). The session has been cleared \u2014 please reload the YNAB tab and try again."
388
- );
389
- }
390
- if (response.status === 429) {
391
- const retryAfter = response.headers.get("Retry-After");
392
- const retryMs = retryAfter !== null ? parseRetryAfterMs(retryAfter) : void 0;
393
- throw ToolError.rateLimited(`Rate limited: ${context} \u2014 ${errorBody}`, retryMs);
394
- }
395
- if (response.status === 401 || response.status === 403) {
396
- clearAuthCache("ynab");
397
- throw ToolError.auth(`Auth error (${response.status}): ${errorBody}`);
398
- }
399
- if (response.status === 404) throw ToolError.notFound(`Not found: ${context} \u2014 ${errorBody}`);
400
- if (response.status === 422) throw ToolError.validation(`Validation error: ${context} \u2014 ${errorBody}`);
401
- throw ToolError.internal(`API error (${response.status}): ${context} \u2014 ${errorBody}`);
402
- };
403
- var catalog = async (operationName, requestData = {}) => {
404
- const headers = getHeaders();
405
- headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
406
- let response;
407
- try {
408
- response = await fetch("/api/v1/catalog", {
409
- method: "POST",
410
- headers,
411
- credentials: "include",
412
- body: `operation_name=${encodeURIComponent(operationName)}&request_data=${encodeURIComponent(JSON.stringify(requestData))}`,
413
- signal: AbortSignal.timeout(3e4)
414
- });
415
- } catch (err2) {
416
- if (err2 instanceof DOMException && err2.name === "TimeoutError")
417
- throw ToolError.timeout(`Catalog request timed out: ${operationName}`);
418
- if (err2 instanceof DOMException && err2.name === "AbortError") throw new ToolError("Request was aborted", "aborted");
419
- throw new ToolError(`Network error: ${err2 instanceof Error ? err2.message : String(err2)}`, "network_error", {
420
- category: "internal",
421
- retryable: true
422
- });
423
- }
424
- if (!response.ok) return handleApiError(response, operationName);
425
- const data = await response.json();
426
- if (data.error) {
427
- throw ToolError.internal(`Catalog error (${operationName}): ${data.error.message}`);
428
- }
429
- return data;
430
- };
431
- var BUDGET_SCHEMA_VERSION = 41;
432
- var syncBudget = async (planId) => catalog("syncBudgetData", {
433
- budget_version_id: planId,
434
- sync_type: "delta",
435
- starting_device_knowledge: 0,
436
- ending_device_knowledge: 0,
437
- device_knowledge_of_server: 0,
438
- calculated_entities_included: false,
439
- schema_version: BUDGET_SCHEMA_VERSION,
440
- schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
441
- changed_entities: {}
442
- });
443
- var syncWrite = async (planId, changedEntities) => {
444
- const readResult = await syncBudget(planId);
445
- const serverKnowledge = readResult.current_server_knowledge ?? 0;
446
- return catalog("syncBudgetData", {
447
- budget_version_id: planId,
448
- sync_type: "delta",
449
- starting_device_knowledge: 0,
450
- ending_device_knowledge: 1,
451
- device_knowledge_of_server: serverKnowledge,
452
- calculated_entities_included: false,
453
- schema_version: BUDGET_SCHEMA_VERSION,
454
- schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
455
- changed_entities: changedEntities
456
- });
457
- };
458
- var api = async (endpoint, options = {}) => {
459
- const headers = getHeaders();
460
- let fetchBody;
461
- if (options.body) {
462
- headers["Content-Type"] = "application/json";
463
- fetchBody = JSON.stringify(options.body);
464
- }
465
- let response;
466
- try {
467
- response = await fetch(`/api/v2${endpoint}`, {
468
- method: options.method ?? "GET",
469
- headers,
470
- body: fetchBody,
471
- credentials: "include",
472
- signal: AbortSignal.timeout(3e4)
473
- });
474
- } catch (err2) {
475
- if (err2 instanceof DOMException && err2.name === "TimeoutError")
476
- throw ToolError.timeout(`API request timed out: ${endpoint}`);
477
- if (err2 instanceof DOMException && err2.name === "AbortError") throw new ToolError("Request was aborted", "aborted");
478
- throw new ToolError(`Network error: ${err2 instanceof Error ? err2.message : String(err2)}`, "network_error", {
479
- category: "internal",
480
- retryable: true
481
- });
482
- }
483
- if (!response.ok) return handleApiError(response, endpoint);
484
- if (response.status === 204) return {};
485
- return await response.json();
486
- };
487
-
488
332
  // node_modules/zod/v4/classic/external.js
489
333
  var external_exports = {};
490
334
  __export(external_exports, {
@@ -14253,19 +14097,187 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14253
14097
  // node_modules/zod/v4/classic/external.js
14254
14098
  config(en_default());
14255
14099
 
14256
- // src/tools/schemas.ts
14257
- var formatMilliunits = (milliunits) => {
14258
- const amount = milliunits / 1e3;
14259
- return amount.toFixed(2);
14100
+ // src/ynab-api.ts
14101
+ var NOT_AUTHENTICATED_MESSAGE = "Not authenticated \u2014 please log in to YNAB.";
14102
+ var generateDeviceId = () => crypto.randomUUID();
14103
+ var extractPlanId = () => {
14104
+ const url2 = getCurrentUrl();
14105
+ const match = url2.match(/app\.ynab\.com\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
14106
+ return match?.[1] ?? null;
14260
14107
  };
14261
- var userSchema = external_exports.object({
14262
- id: external_exports.string().describe("User ID"),
14263
- first_name: external_exports.string().describe("First name"),
14264
- email: external_exports.string().describe("Email address")
14108
+ var getAuth = () => {
14109
+ const cached2 = getAuthCache("ynab");
14110
+ if (cached2?.sessionToken && cached2.planId) return cached2;
14111
+ const sessionToken = getMetaContent("session-token");
14112
+ if (!sessionToken) return null;
14113
+ const user = getPageGlobal("YNAB_CLIENT_CONSTANTS.USER");
14114
+ if (!user?.id) return null;
14115
+ const planId = extractPlanId();
14116
+ if (!planId) return null;
14117
+ const deviceId = cached2?.deviceId ?? generateDeviceId();
14118
+ const auth = {
14119
+ sessionToken,
14120
+ deviceId,
14121
+ userId: user.id,
14122
+ planId
14123
+ };
14124
+ setAuthCache("ynab", auth);
14125
+ return auth;
14126
+ };
14127
+ var isAuthenticated = () => getAuth() !== null;
14128
+ var waitForAuth = () => waitUntil(() => isAuthenticated(), { interval: 500, timeout: 5e3 }).then(
14129
+ () => true,
14130
+ () => false
14131
+ );
14132
+ var getPlanId = () => {
14133
+ const auth = getAuth();
14134
+ if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
14135
+ return auth.planId;
14136
+ };
14137
+ var getDeviceId = () => {
14138
+ const auth = getAuth();
14139
+ if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
14140
+ return auth.deviceId;
14141
+ };
14142
+ var getUserId = () => {
14143
+ const auth = getAuth();
14144
+ if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
14145
+ return auth.userId;
14146
+ };
14147
+ var assertAuthenticated = () => {
14148
+ if (!getAuth()) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
14149
+ };
14150
+ var getHeaders = () => {
14151
+ const auth = getAuth();
14152
+ if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
14153
+ const appVersion = getPageGlobal("YNAB_CLIENT_CONSTANTS.YNAB_APP_VERSION");
14154
+ const headers = {
14155
+ "X-Session-Token": auth.sessionToken,
14156
+ "X-YNAB-Device-Id": auth.deviceId,
14157
+ "X-YNAB-Device-OS": "web",
14158
+ "X-Requested-With": "XMLHttpRequest",
14159
+ Accept: "application/json, text/javascript, */*; q=0.01"
14160
+ };
14161
+ if (appVersion) headers["X-YNAB-Device-App-Version"] = appVersion;
14162
+ return headers;
14163
+ };
14164
+ var handleApiError = async (response, context) => {
14165
+ const errorBody = (await response.text().catch(() => "")).substring(0, 512);
14166
+ if (response.status === 426) {
14167
+ clearAuthCache("ynab");
14168
+ throw ToolError.auth(
14169
+ "YNAB requires an app update (426). The session has been cleared \u2014 please reload the YNAB tab and try again."
14170
+ );
14171
+ }
14172
+ if (response.status === 429) {
14173
+ const retryAfter = response.headers.get("Retry-After");
14174
+ const retryMs = retryAfter !== null ? parseRetryAfterMs(retryAfter) : void 0;
14175
+ throw ToolError.rateLimited(`Rate limited: ${context} \u2014 ${errorBody}`, retryMs);
14176
+ }
14177
+ if (response.status === 401 || response.status === 403) {
14178
+ clearAuthCache("ynab");
14179
+ throw ToolError.auth(`Auth error (${response.status}): ${errorBody}`);
14180
+ }
14181
+ if (response.status === 404) throw ToolError.notFound(`Not found: ${context} \u2014 ${errorBody}`);
14182
+ if (response.status === 422) throw ToolError.validation(`Validation error: ${context} \u2014 ${errorBody}`);
14183
+ throw ToolError.internal(`API error (${response.status}): ${context} \u2014 ${errorBody}`);
14184
+ };
14185
+ var handleNetworkError = (err2, context) => {
14186
+ if (err2 instanceof DOMException && err2.name === "TimeoutError")
14187
+ throw ToolError.timeout(`Request timed out: ${context}`);
14188
+ if (err2 instanceof DOMException && err2.name === "AbortError") throw new ToolError("Request was aborted", "aborted");
14189
+ throw new ToolError(`Network error: ${err2 instanceof Error ? err2.message : String(err2)}`, "network_error", {
14190
+ category: "internal",
14191
+ retryable: true
14192
+ });
14193
+ };
14194
+ var catalog = async (operationName, requestData = {}) => {
14195
+ const headers = getHeaders();
14196
+ headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
14197
+ let response;
14198
+ try {
14199
+ response = await fetch("/api/v1/catalog", {
14200
+ method: "POST",
14201
+ headers,
14202
+ credentials: "include",
14203
+ body: `operation_name=${encodeURIComponent(operationName)}&request_data=${encodeURIComponent(JSON.stringify(requestData))}`,
14204
+ signal: AbortSignal.timeout(3e4)
14205
+ });
14206
+ } catch (err2) {
14207
+ return handleNetworkError(err2, operationName);
14208
+ }
14209
+ if (!response.ok) return handleApiError(response, operationName);
14210
+ const data = await response.json();
14211
+ if (data.error) {
14212
+ throw ToolError.internal(`Catalog error (${operationName}): ${data.error.message}`);
14213
+ }
14214
+ return data;
14215
+ };
14216
+ var BUDGET_SCHEMA_VERSION = 41;
14217
+ var syncBudget = async (planId) => catalog("syncBudgetData", {
14218
+ budget_version_id: planId,
14219
+ sync_type: "delta",
14220
+ starting_device_knowledge: 0,
14221
+ ending_device_knowledge: 0,
14222
+ device_knowledge_of_server: 0,
14223
+ calculated_entities_included: false,
14224
+ schema_version: BUDGET_SCHEMA_VERSION,
14225
+ schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
14226
+ changed_entities: {}
14265
14227
  });
14266
- var planSchema = external_exports.object({
14267
- id: external_exports.string().describe("Plan (budget version) ID used in API calls"),
14268
- budget_id: external_exports.string().describe("Underlying budget ID"),
14228
+ var syncWrite = async (planId, changedEntities, serverKnowledge) => {
14229
+ const knowledge = serverKnowledge ?? (await syncBudget(planId)).current_server_knowledge ?? 0;
14230
+ return catalog("syncBudgetData", {
14231
+ budget_version_id: planId,
14232
+ sync_type: "delta",
14233
+ starting_device_knowledge: 0,
14234
+ ending_device_knowledge: 1,
14235
+ device_knowledge_of_server: knowledge,
14236
+ calculated_entities_included: false,
14237
+ schema_version: BUDGET_SCHEMA_VERSION,
14238
+ schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
14239
+ changed_entities: changedEntities
14240
+ });
14241
+ };
14242
+ var api = async (endpoint, options = {}) => {
14243
+ const headers = getHeaders();
14244
+ let fetchBody;
14245
+ if (options.body) {
14246
+ headers["Content-Type"] = "application/json";
14247
+ fetchBody = JSON.stringify(options.body);
14248
+ }
14249
+ let response;
14250
+ try {
14251
+ response = await fetch(`/api/v2${endpoint}`, {
14252
+ method: options.method ?? "GET",
14253
+ headers,
14254
+ body: fetchBody,
14255
+ credentials: "include",
14256
+ signal: AbortSignal.timeout(3e4)
14257
+ });
14258
+ } catch (err2) {
14259
+ return handleNetworkError(err2, endpoint);
14260
+ }
14261
+ if (!response.ok) return handleApiError(response, endpoint);
14262
+ if (response.status === 204) return {};
14263
+ return await response.json();
14264
+ };
14265
+
14266
+ // src/tools/schemas.ts
14267
+ var formatMilliunits = (milliunits) => {
14268
+ const amount = milliunits / 1e3;
14269
+ return amount.toFixed(2);
14270
+ };
14271
+ var toMilliunits = (amount) => Math.round(amount * 1e3);
14272
+ var notTombstone = (x) => !x.is_tombstone;
14273
+ var userSchema = external_exports.object({
14274
+ id: external_exports.string().describe("User ID"),
14275
+ first_name: external_exports.string().describe("First name"),
14276
+ email: external_exports.string().describe("Email address")
14277
+ });
14278
+ var planSchema = external_exports.object({
14279
+ id: external_exports.string().describe("Plan (budget version) ID used in API calls"),
14280
+ budget_id: external_exports.string().describe("Underlying budget ID"),
14269
14281
  name: external_exports.string().describe("Plan name"),
14270
14282
  date_format: external_exports.string().describe("Date format string (e.g. MM/DD/YYYY)"),
14271
14283
  currency_symbol: external_exports.string().describe("Currency symbol (e.g. $)"),
@@ -14317,7 +14329,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14317
14329
  cleared: external_exports.string().describe("Cleared status: cleared, uncleared, or reconciled"),
14318
14330
  approved: external_exports.boolean().describe("Whether the transaction is approved"),
14319
14331
  flag_color: external_exports.string().describe("Flag color or empty string"),
14320
- flag_name: external_exports.string().describe("Custom flag name or empty string"),
14321
14332
  account_id: external_exports.string().describe("Account ID"),
14322
14333
  account_name: external_exports.string().describe("Account name"),
14323
14334
  payee_id: external_exports.string().describe("Payee ID"),
@@ -14325,7 +14336,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14325
14336
  category_id: external_exports.string().describe("Category ID"),
14326
14337
  category_name: external_exports.string().describe("Category name"),
14327
14338
  transfer_account_id: external_exports.string().describe("If a transfer, the destination account ID"),
14328
- import_id: external_exports.string().describe("Import ID for deduplication"),
14339
+ imported_payee: external_exports.string().describe("Bank-imported payee name after YNAB cleansing (empty if manually entered)"),
14340
+ original_imported_payee: external_exports.string().describe("Raw payee string from the bank feed before any cleansing (empty if manually entered)"),
14329
14341
  deleted: external_exports.boolean().describe("Whether the transaction is deleted")
14330
14342
  });
14331
14343
  var subtransactionSchema = external_exports.object({
@@ -14351,7 +14363,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14351
14363
  budgeted_milliunits: external_exports.number().describe("Total budgeted in milliunits"),
14352
14364
  activity_milliunits: external_exports.number().describe("Total activity in milliunits"),
14353
14365
  to_be_budgeted_milliunits: external_exports.number().describe("Ready to Assign in milliunits"),
14354
- age_of_money: external_exports.number().describe("Age of money in days")
14366
+ age_of_money: external_exports.number().nullable().describe("Age of money in days, or null if not yet computed")
14355
14367
  });
14356
14368
  var scheduledTransactionSchema = external_exports.object({
14357
14369
  id: external_exports.string().describe("Scheduled transaction ID"),
@@ -14372,6 +14384,119 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14372
14384
  category_name: external_exports.string().describe("Category name"),
14373
14385
  deleted: external_exports.boolean().describe("Whether the scheduled transaction is deleted")
14374
14386
  });
14387
+ var MONEY_MOVEMENT_SOURCE = {
14388
+ /** RTA ↔ category (in either direction). */
14389
+ ASSIGN: "manual_assign",
14390
+ /** Category-to-category transfer. */
14391
+ MOVEMENT: "manual_movement"
14392
+ };
14393
+ var SUBCATEGORY_BUDGET_PREFIX = "mcb";
14394
+ var MONTHLY_BUDGET_PREFIX = "mb";
14395
+ var toMonthKey = (month) => month.substring(0, 7);
14396
+ var currentMonthKey = () => {
14397
+ const now = /* @__PURE__ */ new Date();
14398
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
14399
+ };
14400
+ var formatSubcategoryBudgetId = (monthKey, categoryId) => `${SUBCATEGORY_BUDGET_PREFIX}/${monthKey}/${categoryId}`;
14401
+ var formatMonthlyBudgetId = (monthKey, planId) => `${MONTHLY_BUDGET_PREFIX}/${monthKey}/${planId}`;
14402
+ var CLEARED_MAP = {
14403
+ cleared: "Cleared",
14404
+ uncleared: "Uncleared",
14405
+ reconciled: "Reconciled"
14406
+ };
14407
+ var FLAG_MAP = {
14408
+ red: "Red",
14409
+ orange: "Orange",
14410
+ yellow: "Yellow",
14411
+ green: "Green",
14412
+ blue: "Blue",
14413
+ purple: "Purple"
14414
+ };
14415
+ var resolvePayee = (existingPayees, payeeName) => {
14416
+ const target = payeeName.toLowerCase();
14417
+ const match = existingPayees.find((p) => notTombstone(p) && p.name?.toLowerCase() === target);
14418
+ if (match?.id) return { payeeId: match.id };
14419
+ const payeeId = crypto.randomUUID();
14420
+ const newPayee = {
14421
+ id: payeeId,
14422
+ is_tombstone: false,
14423
+ entities_account_id: null,
14424
+ enabled: true,
14425
+ auto_fill_subcategory_id: null,
14426
+ auto_fill_memo: null,
14427
+ auto_fill_amount: 0,
14428
+ auto_fill_subcategory_enabled: true,
14429
+ auto_fill_memo_enabled: false,
14430
+ auto_fill_amount_enabled: false,
14431
+ rename_on_import_enabled: true,
14432
+ name: payeeName,
14433
+ internal_name: null
14434
+ };
14435
+ return { payeeId, newPayee };
14436
+ };
14437
+ var buildAccountCalcMap = (entities) => new Map((entities.be_account_calculations ?? []).map((c) => [c.entities_account_id, c]));
14438
+ var buildMonthlyBudgetCalcMap = (calcs) => {
14439
+ const map2 = /* @__PURE__ */ new Map();
14440
+ for (const calc of calcs) {
14441
+ const entityId = calc.entities_monthly_budget_id;
14442
+ if (!entityId) continue;
14443
+ const parts = entityId.split("/");
14444
+ const month = parts[1];
14445
+ if (parts.length >= 2 && month) map2.set(month, calc);
14446
+ }
14447
+ return map2;
14448
+ };
14449
+ var subcategoryCalcKey = (month, categoryId) => `${month}/${categoryId}`;
14450
+ var parseSubcategoryEntityId = (entityId) => {
14451
+ if (!entityId) return null;
14452
+ const parts = entityId.split("/");
14453
+ if (parts.length !== 3) return null;
14454
+ const [, month, categoryId] = parts;
14455
+ if (!month || !categoryId) return null;
14456
+ return { month, categoryId };
14457
+ };
14458
+ var buildSubcategoryCalcMap = (calcs) => {
14459
+ const map2 = /* @__PURE__ */ new Map();
14460
+ for (const calc of calcs) {
14461
+ const parsed = parseSubcategoryEntityId(calc.entities_monthly_subcategory_budget_id);
14462
+ if (parsed) map2.set(subcategoryCalcKey(parsed.month, parsed.categoryId), calc);
14463
+ }
14464
+ return map2;
14465
+ };
14466
+ var buildSubcategoryBudgetMap = (budgets) => {
14467
+ const map2 = /* @__PURE__ */ new Map();
14468
+ for (const budget of budgets) {
14469
+ if (budget.is_tombstone) continue;
14470
+ const parsed = parseSubcategoryEntityId(budget.id);
14471
+ if (parsed) map2.set(subcategoryCalcKey(parsed.month, parsed.categoryId), budget);
14472
+ }
14473
+ return map2;
14474
+ };
14475
+ var mapCategoryForMonth = (c, budgetMap, calcMap, month) => {
14476
+ const key = subcategoryCalcKey(month, c.id ?? "");
14477
+ const budget = budgetMap.get(key);
14478
+ const calc = calcMap.get(key);
14479
+ return mapCategory({
14480
+ ...c,
14481
+ budgeted: budget?.budgeted ?? c.budgeted,
14482
+ activity: (calc?.cash_outflows ?? 0) + (calc?.credit_outflows ?? 0),
14483
+ balance: calc?.balance ?? c.balance,
14484
+ goal_target: calc?.goal_target ?? c.goal_target,
14485
+ goal_percentage_complete: calc?.goal_percentage_complete ?? c.goal_percentage_complete
14486
+ });
14487
+ };
14488
+ var hasId = (x) => !!x.id;
14489
+ var buildLookups = (entities) => ({
14490
+ payees: new Map(
14491
+ (entities.be_payees ?? []).filter(notTombstone).filter(hasId).map((p) => [p.id, p.name ?? ""])
14492
+ ),
14493
+ accounts: new Map(
14494
+ (entities.be_accounts ?? []).filter(notTombstone).filter(hasId).map((a) => [a.id, a.account_name ?? ""])
14495
+ ),
14496
+ categories: new Map(
14497
+ (entities.be_subcategories ?? []).filter(notTombstone).filter(hasId).map((c) => [c.id, c.name ?? ""])
14498
+ )
14499
+ });
14375
14500
  var mapUser = (u) => ({
14376
14501
  id: u.id ?? "",
14377
14502
  first_name: u.first_name ?? "",
@@ -14386,8 +14511,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14386
14511
  const df = JSON.parse(p.date_format);
14387
14512
  dateFormat = df.format ?? "";
14388
14513
  }
14389
- } catch {
14390
- dateFormat = p.date_format ?? "";
14514
+ } catch (err2) {
14515
+ log.warn("mapPlan: failed to parse date_format", { raw: p.date_format, err: err2 });
14391
14516
  }
14392
14517
  try {
14393
14518
  if (p.currency_format) {
@@ -14395,7 +14520,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14395
14520
  currencySymbol = cf.currency_symbol ?? "$";
14396
14521
  currencyIsoCode = cf.iso_code ?? "USD";
14397
14522
  }
14398
- } catch {
14523
+ } catch (err2) {
14524
+ log.warn("mapPlan: failed to parse currency_format", { raw: p.currency_format, err: err2 });
14399
14525
  }
14400
14526
  return {
14401
14527
  id: p.id ?? "",
@@ -14443,36 +14569,36 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14443
14569
  name: p.name ?? "",
14444
14570
  transfer_account_id: p.entities_account_id ?? ""
14445
14571
  });
14446
- var mapTransaction = (t) => ({
14572
+ var mapTransaction = (t, lookups) => ({
14447
14573
  id: t.id ?? "",
14448
14574
  date: t.date ?? "",
14449
14575
  amount: formatMilliunits(t.amount ?? 0),
14450
14576
  amount_milliunits: t.amount ?? 0,
14451
14577
  memo: t.memo ?? "",
14452
- cleared: t.cleared ?? "uncleared",
14453
- approved: t.approved ?? false,
14454
- flag_color: t.flag_color ?? "",
14455
- flag_name: t.flag_name ?? "",
14578
+ cleared: t.cleared?.toLowerCase() ?? "uncleared",
14579
+ approved: t.accepted ?? false,
14580
+ flag_color: t.flag?.toLowerCase() ?? "",
14456
14581
  account_id: t.entities_account_id ?? "",
14457
- account_name: t.account_name ?? "",
14582
+ account_name: lookups?.accounts.get(t.entities_account_id ?? "") ?? "",
14458
14583
  payee_id: t.entities_payee_id ?? "",
14459
- payee_name: t.payee_name ?? "",
14584
+ payee_name: lookups?.payees.get(t.entities_payee_id ?? "") ?? "",
14460
14585
  category_id: t.entities_subcategory_id ?? "",
14461
- category_name: t.category_name ?? "",
14586
+ category_name: lookups?.categories.get(t.entities_subcategory_id ?? "") ?? "",
14462
14587
  transfer_account_id: t.transfer_account_id ?? "",
14463
- import_id: t.import_id ?? "",
14588
+ imported_payee: t.imported_payee ?? "",
14589
+ original_imported_payee: t.original_imported_payee ?? "",
14464
14590
  deleted: t.is_tombstone === true
14465
14591
  });
14466
- var mapSubtransaction = (s) => ({
14592
+ var mapSubtransaction = (s, lookups) => ({
14467
14593
  id: s.id ?? "",
14468
14594
  transaction_id: s.entities_transaction_id ?? "",
14469
14595
  amount: formatMilliunits(s.amount ?? 0),
14470
14596
  amount_milliunits: s.amount ?? 0,
14471
14597
  memo: s.memo ?? "",
14472
14598
  payee_id: s.entities_payee_id ?? "",
14473
- payee_name: s.payee_name ?? "",
14599
+ payee_name: lookups?.payees.get(s.entities_payee_id ?? "") ?? "",
14474
14600
  category_id: s.entities_subcategory_id ?? "",
14475
- category_name: s.category_name ?? "",
14601
+ category_name: lookups?.categories.get(s.entities_subcategory_id ?? "") ?? "",
14476
14602
  transfer_account_id: s.transfer_account_id ?? "",
14477
14603
  deleted: s.is_tombstone === true
14478
14604
  });
@@ -14491,27 +14617,198 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14491
14617
  budgeted_milliunits: budgeted,
14492
14618
  activity_milliunits: activity,
14493
14619
  to_be_budgeted_milliunits: toBeBudgeted,
14494
- age_of_money: calc?.age_of_money ?? 0
14620
+ age_of_money: calc?.age_of_money ?? null
14495
14621
  };
14496
14622
  };
14497
- var mapScheduledTransaction = (s) => ({
14623
+ var mapScheduledTransaction = (s, lookups) => ({
14498
14624
  id: s.id ?? "",
14499
- date_first: s.date_first ?? "",
14500
- date_next: s.date_next ?? "",
14625
+ date_first: s.date ?? "",
14626
+ date_next: s.upcoming_instances?.[0] ?? s.date ?? "",
14501
14627
  frequency: s.frequency ?? "never",
14502
14628
  amount: formatMilliunits(s.amount ?? 0),
14503
14629
  amount_milliunits: s.amount ?? 0,
14504
14630
  memo: s.memo ?? "",
14505
- flag_color: s.flag_color ?? "",
14631
+ flag_color: s.flag?.toLowerCase() ?? "",
14506
14632
  account_id: s.entities_account_id ?? "",
14507
- account_name: s.account_name ?? "",
14633
+ account_name: lookups?.accounts.get(s.entities_account_id ?? "") ?? "",
14508
14634
  payee_id: s.entities_payee_id ?? "",
14509
- payee_name: s.payee_name ?? "",
14635
+ payee_name: lookups?.payees.get(s.entities_payee_id ?? "") ?? "",
14510
14636
  category_id: s.entities_subcategory_id ?? "",
14511
- category_name: s.category_name ?? "",
14637
+ category_name: lookups?.categories.get(s.entities_subcategory_id ?? "") ?? "",
14512
14638
  deleted: s.is_tombstone === true
14513
14639
  });
14514
14640
 
14641
+ // src/tools/create-transaction.ts
14642
+ var createTransaction = defineTool({
14643
+ name: "create_transaction",
14644
+ displayName: "Create Transaction",
14645
+ description: "Create a new transaction in the active YNAB plan. Amount is in currency units (e.g. -42.50 for a $42.50 expense, 1500 for $1500 income). Negative amounts are outflows (expenses), positive amounts are inflows (income).",
14646
+ summary: "Create a new transaction",
14647
+ icon: "plus",
14648
+ group: "Transactions",
14649
+ input: external_exports.object({
14650
+ account_id: external_exports.string().min(1).describe("Account ID to create the transaction in"),
14651
+ date: external_exports.string().min(1).describe("Transaction date in YYYY-MM-DD format"),
14652
+ amount: external_exports.number().describe(
14653
+ "Amount in currency units (negative for expenses, positive for income). E.g. -42.50 for a $42.50 expense."
14654
+ ),
14655
+ payee_name: external_exports.string().optional().describe("Payee name (creates new payee if not found)"),
14656
+ payee_id: external_exports.string().optional().describe("Existing payee ID (takes precedence over payee_name)"),
14657
+ category_id: external_exports.string().optional().describe("Category ID to assign"),
14658
+ memo: external_exports.string().optional().describe("Transaction memo"),
14659
+ cleared: external_exports.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status (default uncleared)"),
14660
+ approved: external_exports.boolean().optional().describe("Whether the transaction is approved (default true)"),
14661
+ flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color")
14662
+ }),
14663
+ output: external_exports.object({
14664
+ transaction: transactionSchema
14665
+ }),
14666
+ handle: async (params) => {
14667
+ const planId = getPlanId();
14668
+ const milliunits = toMilliunits(params.amount);
14669
+ const txId = crypto.randomUUID();
14670
+ const budget = await syncBudget(planId);
14671
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
14672
+ const lookups = buildLookups(budget.changed_entities ?? {});
14673
+ const changedEntities = {};
14674
+ let payeeId = params.payee_id ?? null;
14675
+ if (!payeeId && params.payee_name) {
14676
+ const resolved = resolvePayee(budget.changed_entities?.be_payees ?? [], params.payee_name);
14677
+ payeeId = resolved.payeeId;
14678
+ if (resolved.newPayee) {
14679
+ changedEntities.be_payees = [resolved.newPayee];
14680
+ lookups.payees.set(resolved.payeeId, params.payee_name);
14681
+ }
14682
+ }
14683
+ changedEntities.be_transaction_groups = [
14684
+ {
14685
+ id: txId,
14686
+ be_transaction: {
14687
+ id: txId,
14688
+ is_tombstone: false,
14689
+ entities_account_id: params.account_id,
14690
+ entities_payee_id: payeeId,
14691
+ entities_subcategory_id: params.category_id ?? null,
14692
+ entities_scheduled_transaction_id: null,
14693
+ date: params.date,
14694
+ date_entered_from_schedule: null,
14695
+ amount: milliunits,
14696
+ // cash_amount and credit_amount are server-computed splits the account
14697
+ // type determines. Captured from a credit card account create where
14698
+ // YNAB's UI sent zeros and the server populated them on response —
14699
+ // not yet verified for cash/checking accounts but likely the same
14700
+ // pattern.
14701
+ cash_amount: 0,
14702
+ credit_amount: 0,
14703
+ credit_amount_adjusted: 0,
14704
+ subcategory_credit_amount_preceding: 0,
14705
+ memo: params.memo ?? null,
14706
+ cleared: CLEARED_MAP[params.cleared ?? "uncleared"],
14707
+ // YNAB's wire format calls this "accepted"; the public tool surface uses "approved".
14708
+ accepted: params.approved ?? true,
14709
+ check_number: null,
14710
+ flag: params.flag_color ? FLAG_MAP[params.flag_color] : null,
14711
+ transfer_account_id: null,
14712
+ transfer_transaction_id: null,
14713
+ transfer_subtransaction_id: null,
14714
+ matched_transaction_id: null,
14715
+ ynab_id: null,
14716
+ // Import-related fields are only populated by bank-feed imports, not manual entry.
14717
+ imported_payee: null,
14718
+ imported_date: null,
14719
+ original_imported_payee: null,
14720
+ provider_cleansed_payee: null,
14721
+ source: null,
14722
+ debt_transaction_type: null
14723
+ },
14724
+ be_subtransactions: null
14725
+ }
14726
+ ];
14727
+ const result = await syncWrite(planId, changedEntities, serverKnowledge);
14728
+ const saved = result.changed_entities?.be_transactions?.find((t) => t.id === txId);
14729
+ if (!saved) {
14730
+ throw ToolError.internal("Transaction was created but no data was returned");
14731
+ }
14732
+ return { transaction: mapTransaction(saved, lookups) };
14733
+ }
14734
+ });
14735
+
14736
+ // src/tools/delete-transaction.ts
14737
+ var deleteTransaction = defineTool({
14738
+ name: "delete_transaction",
14739
+ displayName: "Delete Transaction",
14740
+ description: "Delete a transaction from the active YNAB plan. This marks the transaction as deleted (soft delete). Transfer transactions cannot be deleted through this tool \u2014 delete them directly in YNAB.",
14741
+ summary: "Delete a transaction",
14742
+ icon: "trash-2",
14743
+ group: "Transactions",
14744
+ input: external_exports.object({
14745
+ transaction_id: external_exports.string().min(1).describe("Transaction ID to delete"),
14746
+ account_id: external_exports.string().min(1).describe("Account ID the transaction belongs to")
14747
+ }),
14748
+ output: external_exports.object({
14749
+ success: external_exports.boolean().describe("Whether the operation succeeded")
14750
+ }),
14751
+ handle: async (params) => {
14752
+ const planId = getPlanId();
14753
+ const budget = await syncBudget(planId);
14754
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
14755
+ const existing2 = budget.changed_entities?.be_transactions?.find(
14756
+ (t) => t.id === params.transaction_id && !t.is_tombstone
14757
+ );
14758
+ if (!existing2) {
14759
+ throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
14760
+ }
14761
+ if (existing2.transfer_account_id) {
14762
+ throw ToolError.validation("Cannot delete transfer transactions \u2014 delete them in YNAB directly.");
14763
+ }
14764
+ await syncWrite(
14765
+ planId,
14766
+ {
14767
+ be_transaction_groups: [
14768
+ {
14769
+ id: params.transaction_id,
14770
+ be_transaction: {
14771
+ ...existing2,
14772
+ is_tombstone: true
14773
+ },
14774
+ be_subtransactions: null
14775
+ }
14776
+ ]
14777
+ },
14778
+ serverKnowledge
14779
+ );
14780
+ return { success: true };
14781
+ }
14782
+ });
14783
+
14784
+ // src/tools/get-account.ts
14785
+ var getAccount = defineTool({
14786
+ name: "get_account",
14787
+ displayName: "Get Account",
14788
+ description: "Get details for a specific account in the active YNAB plan by its ID. Returns name, type, balances, and on-budget status.",
14789
+ summary: "Get account details by ID",
14790
+ icon: "landmark",
14791
+ group: "Accounts",
14792
+ input: external_exports.object({
14793
+ account_id: external_exports.string().min(1).describe("Account ID to retrieve")
14794
+ }),
14795
+ output: external_exports.object({
14796
+ account: accountSchema
14797
+ }),
14798
+ handle: async (params) => {
14799
+ const planId = getPlanId();
14800
+ const result = await syncBudget(planId);
14801
+ const entities = result.changed_entities;
14802
+ const raw = entities?.be_accounts ?? [];
14803
+ const calcMap = buildAccountCalcMap(entities ?? {});
14804
+ const account = raw.find((a) => a.id === params.account_id && !a.is_tombstone);
14805
+ if (!account) {
14806
+ throw ToolError.notFound(`Account not found: ${params.account_id}`);
14807
+ }
14808
+ return { account: mapAccount(account, calcMap.get(account.id)) };
14809
+ }
14810
+ });
14811
+
14515
14812
  // src/tools/get-current-user.ts
14516
14813
  var getCurrentUser = defineTool({
14517
14814
  name: "get_current_user",
@@ -14528,6 +14825,47 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14528
14825
  }
14529
14826
  });
14530
14827
 
14828
+ // src/tools/get-month.ts
14829
+ var getMonth = defineTool({
14830
+ name: "get_month",
14831
+ displayName: "Get Month",
14832
+ description: "Get budget summary and category details for a specific month. Returns the month overview (income, budgeted, activity, Ready to Assign) plus per-category breakdowns. Month format is YYYY-MM-DD using the first of the month (e.g. 2026-03-01).",
14833
+ summary: "Get budget details for a month",
14834
+ icon: "calendar",
14835
+ group: "Months",
14836
+ input: external_exports.object({
14837
+ month: external_exports.string().min(1).describe("Month in YYYY-MM-DD format (first of month, e.g. 2026-03-01)"),
14838
+ include_hidden: external_exports.boolean().optional().describe("Include hidden categories (default false)")
14839
+ }),
14840
+ output: external_exports.object({
14841
+ month: monthSchema,
14842
+ categories: external_exports.array(categorySchema).describe("Category budgets for this month")
14843
+ }),
14844
+ handle: async (params) => {
14845
+ const planId = getPlanId();
14846
+ const result = await syncBudget(planId);
14847
+ const entities = result.changed_entities;
14848
+ const rawMonths = entities?.be_monthly_budgets ?? [];
14849
+ const monthData = rawMonths.find((m) => m.month === params.month && !m.is_tombstone);
14850
+ if (!monthData) {
14851
+ throw ToolError.notFound(`Month not found: ${params.month}`);
14852
+ }
14853
+ const monthKey = toMonthKey(params.month);
14854
+ const monthCalcMap = buildMonthlyBudgetCalcMap(entities?.be_monthly_budget_calculations ?? []);
14855
+ const monthCalc = monthCalcMap.get(monthKey);
14856
+ const rawCategories = (entities?.be_subcategories ?? []).filter(
14857
+ (c) => notTombstone(c) && (params.include_hidden || c.is_hidden !== true)
14858
+ );
14859
+ const budgetMap = buildSubcategoryBudgetMap(entities?.be_monthly_subcategory_budgets ?? []);
14860
+ const calcMap = buildSubcategoryCalcMap(entities?.be_monthly_subcategory_budget_calculations ?? []);
14861
+ const categories = rawCategories.map((c) => mapCategoryForMonth(c, budgetMap, calcMap, monthKey));
14862
+ return {
14863
+ month: mapMonth(monthData, monthCalc),
14864
+ categories
14865
+ };
14866
+ }
14867
+ });
14868
+
14531
14869
  // src/tools/get-plan.ts
14532
14870
  var getPlan = defineTool({
14533
14871
  name: "get_plan",
@@ -14539,67 +14877,77 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14539
14877
  input: external_exports.object({}),
14540
14878
  output: external_exports.object({ plan: planSchema }),
14541
14879
  handle: async () => {
14542
- const planId = getPlanId();
14880
+ assertAuthenticated();
14543
14881
  const result = await catalog("getInitialUserData", {
14544
- device_info: { id: planId, device_os: "web" }
14882
+ device_info: { id: getDeviceId(), device_os: "web" }
14545
14883
  });
14546
- return { plan: mapPlan(result.budget_version) };
14884
+ const budgetVersion = result.budget_version;
14885
+ if (!budgetVersion) {
14886
+ throw ToolError.notFound("No active plan found");
14887
+ }
14888
+ return { plan: mapPlan(budgetVersion) };
14547
14889
  }
14548
14890
  });
14549
14891
 
14550
- // src/tools/list-accounts.ts
14551
- var listAccounts = defineTool({
14552
- name: "list_accounts",
14553
- displayName: "List Accounts",
14554
- description: "List all accounts in the active YNAB plan. Returns account name, type, balances, and on-budget status. Includes checking, savings, credit cards, and tracking accounts.",
14555
- summary: "List all budget accounts",
14556
- icon: "landmark",
14557
- group: "Accounts",
14892
+ // src/tools/get-transaction.ts
14893
+ var getTransaction = defineTool({
14894
+ name: "get_transaction",
14895
+ displayName: "Get Transaction",
14896
+ description: "Get details for a specific transaction by its ID. Returns full transaction data including any split subtransactions.",
14897
+ summary: "Get transaction details by ID",
14898
+ icon: "receipt",
14899
+ group: "Transactions",
14558
14900
  input: external_exports.object({
14559
- include_closed: external_exports.boolean().optional().describe("Include closed accounts (default false)")
14901
+ transaction_id: external_exports.string().min(1).describe("Transaction ID to retrieve")
14560
14902
  }),
14561
14903
  output: external_exports.object({
14562
- accounts: external_exports.array(accountSchema).describe("List of accounts")
14904
+ transaction: transactionSchema,
14905
+ subtransactions: external_exports.array(subtransactionSchema).describe("Split subtransactions (empty if not a split)")
14563
14906
  }),
14564
14907
  handle: async (params) => {
14565
14908
  const planId = getPlanId();
14566
14909
  const result = await syncBudget(planId);
14567
14910
  const entities = result.changed_entities;
14568
- const raw = entities?.be_accounts ?? [];
14569
- const calcMap = new Map((entities?.be_account_calculations ?? []).map((c) => [c.entities_account_id, c]));
14570
- let accounts = raw.filter((a) => !a.is_tombstone).map((a) => mapAccount(a, calcMap.get(a.id)));
14571
- if (!params.include_closed) {
14572
- accounts = accounts.filter((a) => !a.closed);
14911
+ const raw = entities?.be_transactions ?? [];
14912
+ const tx = raw.find((t) => t.id === params.transaction_id && !t.is_tombstone);
14913
+ if (!tx) {
14914
+ throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
14573
14915
  }
14574
- return { accounts };
14916
+ const lookups = buildLookups(entities ?? {});
14917
+ const allSubs = entities?.be_subtransactions ?? [];
14918
+ const subtransactions = allSubs.filter((s) => s.entities_transaction_id === params.transaction_id && !s.is_tombstone).map((s) => mapSubtransaction(s, lookups));
14919
+ return {
14920
+ transaction: mapTransaction(tx, lookups),
14921
+ subtransactions
14922
+ };
14575
14923
  }
14576
14924
  });
14577
14925
 
14578
- // src/tools/get-account.ts
14579
- var getAccount = defineTool({
14580
- name: "get_account",
14581
- displayName: "Get Account",
14582
- description: "Get details for a specific account in the active YNAB plan by its ID. Returns name, type, balances, and on-budget status.",
14583
- summary: "Get account details by ID",
14926
+ // src/tools/list-accounts.ts
14927
+ var listAccounts = defineTool({
14928
+ name: "list_accounts",
14929
+ displayName: "List Accounts",
14930
+ description: "List all accounts in the active YNAB plan. Returns account name, type, balances, and on-budget status. Includes checking, savings, credit cards, and tracking accounts.",
14931
+ summary: "List all budget accounts",
14584
14932
  icon: "landmark",
14585
14933
  group: "Accounts",
14586
14934
  input: external_exports.object({
14587
- account_id: external_exports.string().min(1).describe("Account ID to retrieve")
14935
+ include_closed: external_exports.boolean().optional().describe("Include closed accounts (default false)")
14588
14936
  }),
14589
14937
  output: external_exports.object({
14590
- account: accountSchema
14938
+ accounts: external_exports.array(accountSchema).describe("List of accounts")
14591
14939
  }),
14592
14940
  handle: async (params) => {
14593
14941
  const planId = getPlanId();
14594
14942
  const result = await syncBudget(planId);
14595
14943
  const entities = result.changed_entities;
14596
14944
  const raw = entities?.be_accounts ?? [];
14597
- const calcMap = new Map((entities?.be_account_calculations ?? []).map((c) => [c.entities_account_id, c]));
14598
- const account = raw.find((a) => a.id === params.account_id && !a.is_tombstone);
14599
- if (!account) {
14600
- throw ToolError.notFound(`Account not found: ${params.account_id}`);
14945
+ const calcMap = buildAccountCalcMap(entities ?? {});
14946
+ let accounts = raw.filter(notTombstone).map((a) => mapAccount(a, calcMap.get(a.id)));
14947
+ if (!params.include_closed) {
14948
+ accounts = accounts.filter((a) => !a.closed);
14601
14949
  }
14602
- return { account: mapAccount(account, calcMap.get(account.id)) };
14950
+ return { accounts };
14603
14951
  }
14604
14952
  });
14605
14953
 
@@ -14607,7 +14955,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14607
14955
  var listCategories = defineTool({
14608
14956
  name: "list_categories",
14609
14957
  displayName: "List Categories",
14610
- description: "List all category groups and categories in the active YNAB plan. Returns budgeted amounts, activity, and available balances for the current month. Excludes hidden and deleted categories by default.",
14958
+ description: "List category groups and categories in the active YNAB plan. Returns budgeted amounts, activity, and available balances for the current month. Hidden and deleted categories are excluded by default \u2014 pass include_hidden=true to also see hidden categories (useful for editing budgets on hidden categories).",
14611
14959
  summary: "List budget categories with balances",
14612
14960
  icon: "tags",
14613
14961
  group: "Categories",
@@ -14622,31 +14970,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14622
14970
  const planId = getPlanId();
14623
14971
  const result = await syncBudget(planId);
14624
14972
  const entities = result.changed_entities;
14625
- const rawGroups = (entities?.be_master_categories ?? []).filter((g) => !g.is_tombstone);
14626
- const rawCategories = (entities?.be_subcategories ?? []).filter((c) => !c.is_tombstone);
14627
- const calcs = entities?.be_monthly_subcategory_budget_calculations ?? [];
14628
- const calcMap = /* @__PURE__ */ new Map();
14629
- for (const calc of calcs) {
14630
- const entityId = calc.entities_monthly_subcategory_budget_id;
14631
- if (entityId) {
14632
- const parts = entityId.split("/");
14633
- const categoryId = parts.length >= 3 ? parts.slice(2).join("/") : entityId;
14634
- calcMap.set(categoryId, calc);
14635
- }
14636
- }
14973
+ const rawGroups = (entities?.be_master_categories ?? []).filter(notTombstone);
14974
+ const rawCategories = (entities?.be_subcategories ?? []).filter(notTombstone);
14975
+ const budgetMap = buildSubcategoryBudgetMap(entities?.be_monthly_subcategory_budgets ?? []);
14976
+ const calcMap = buildSubcategoryCalcMap(entities?.be_monthly_subcategory_budget_calculations ?? []);
14977
+ const currentMonth = currentMonthKey();
14637
14978
  let groups = rawGroups.map(mapCategoryGroup);
14638
- let categories = rawCategories.map((c) => {
14639
- const calc = calcMap.get(c.id ?? "");
14640
- return mapCategory({
14641
- ...c,
14642
- budgeted: calc?.budgeted ?? c.budgeted,
14643
- activity: calc?.activity ?? c.activity,
14644
- balance: calc?.balance ?? c.balance,
14645
- goal_type: calc?.goal_type ?? c.goal_type,
14646
- goal_target: calc?.goal_target ?? c.goal_target,
14647
- goal_percentage_complete: calc?.goal_percentage_complete ?? c.goal_percentage_complete
14648
- });
14649
- });
14979
+ let categories = rawCategories.map((c) => mapCategoryForMonth(c, budgetMap, calcMap, currentMonth));
14650
14980
  if (!params.include_hidden) {
14651
14981
  groups = groups.filter((g) => !g.hidden);
14652
14982
  categories = categories.filter((c) => !c.hidden);
@@ -14655,48 +14985,26 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14655
14985
  }
14656
14986
  });
14657
14987
 
14658
- // src/tools/update-category-budget.ts
14659
- var updateCategoryBudget = defineTool({
14660
- name: "update_category_budget",
14661
- displayName: "Update Category Budget",
14662
- description: "Set the budgeted amount for a category in a specific month. Amount is in currency units (e.g. 500 to budget $500). The month should be in YYYY-MM format (e.g. 2026-03 for March 2026).",
14663
- summary: "Set budgeted amount for a category",
14664
- icon: "pencil",
14665
- group: "Categories",
14666
- input: external_exports.object({
14667
- category_id: external_exports.string().min(1).describe("Category ID to budget"),
14668
- month: external_exports.string().min(1).describe("Month in YYYY-MM format (e.g. 2026-03)"),
14669
- budgeted: external_exports.number().describe("Amount to budget in currency units (e.g. 500 for $500)")
14670
- }),
14988
+ // src/tools/list-months.ts
14989
+ var listMonths = defineTool({
14990
+ name: "list_months",
14991
+ displayName: "List Months",
14992
+ description: "List all budget months in the active YNAB plan. Returns income, budgeted, activity, and Ready to Assign amounts for each month. Sorted from most recent to oldest.",
14993
+ summary: "List budget months with summaries",
14994
+ icon: "calendar",
14995
+ group: "Months",
14996
+ input: external_exports.object({}),
14671
14997
  output: external_exports.object({
14672
- category: categorySchema
14998
+ months: external_exports.array(monthSchema).describe("List of budget months")
14673
14999
  }),
14674
- handle: async (params) => {
15000
+ handle: async () => {
14675
15001
  const planId = getPlanId();
14676
- const milliunits = Math.round(params.budgeted * 1e3);
14677
- const monthKey = params.month.substring(0, 7);
14678
- const budgetId = `mcb/${monthKey}/${params.category_id}`;
14679
- const monthlyBudgetId = `mb/${monthKey}/${planId}`;
14680
- const result = await syncWrite(planId, {
14681
- be_monthly_subcategory_budgets: [
14682
- {
14683
- id: budgetId,
14684
- entities_monthly_budget_id: monthlyBudgetId,
14685
- entities_subcategory_id: params.category_id,
14686
- budgeted: milliunits,
14687
- overspending_handling: "AffectsBuffer",
14688
- is_tombstone: false
14689
- }
14690
- ]
14691
- });
14692
- const budgets = result.changed_entities?.be_monthly_subcategory_budgets;
14693
- const updatedBudget = budgets?.[0]?.budgeted ?? milliunits;
14694
- return {
14695
- category: mapCategory({
14696
- id: params.category_id,
14697
- budgeted: updatedBudget
14698
- })
14699
- };
15002
+ const result = await syncBudget(planId);
15003
+ const entities = result.changed_entities;
15004
+ const rawMonths = entities?.be_monthly_budgets ?? [];
15005
+ const calcMap = buildMonthlyBudgetCalcMap(entities?.be_monthly_budget_calculations ?? []);
15006
+ const months = rawMonths.filter(notTombstone).map((m) => mapMonth(m, calcMap.get((m.month ?? "").substring(0, 7)))).sort((a, b) => b.month.localeCompare(a.month));
15007
+ return { months };
14700
15008
  }
14701
15009
  });
14702
15010
 
@@ -14716,11 +15024,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14716
15024
  const planId = getPlanId();
14717
15025
  const result = await syncBudget(planId);
14718
15026
  const raw = result.changed_entities?.be_payees ?? [];
14719
- const payees = raw.filter((p) => !p.is_tombstone).map(mapPayee);
15027
+ const payees = raw.filter(notTombstone).map(mapPayee);
14720
15028
  return { payees };
14721
15029
  }
14722
15030
  });
14723
15031
 
15032
+ // src/tools/list-scheduled-transactions.ts
15033
+ var listScheduledTransactions = defineTool({
15034
+ name: "list_scheduled_transactions",
15035
+ displayName: "List Scheduled Transactions",
15036
+ description: "List all scheduled (recurring) transactions in the active YNAB plan. Returns frequency, next occurrence date, amount, payee, and category for each.",
15037
+ summary: "List scheduled/recurring transactions",
15038
+ icon: "clock",
15039
+ group: "Transactions",
15040
+ input: external_exports.object({}),
15041
+ output: external_exports.object({
15042
+ scheduled_transactions: external_exports.array(scheduledTransactionSchema).describe("List of scheduled transactions")
15043
+ }),
15044
+ handle: async () => {
15045
+ const planId = getPlanId();
15046
+ const result = await syncBudget(planId);
15047
+ const entities = result.changed_entities;
15048
+ const raw = entities?.be_scheduled_transactions ?? [];
15049
+ const lookups = buildLookups(entities ?? {});
15050
+ const scheduledTransactions = raw.filter(notTombstone).map((s) => mapScheduledTransaction(s, lookups)).sort((a, b) => a.date_next.localeCompare(b.date_next));
15051
+ return { scheduled_transactions: scheduledTransactions };
15052
+ }
15053
+ });
15054
+
14724
15055
  // src/tools/list-transactions.ts
14725
15056
  var listTransactions = defineTool({
14726
15057
  name: "list_transactions",
@@ -14731,7 +15062,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14731
15062
  group: "Transactions",
14732
15063
  input: external_exports.object({
14733
15064
  account_id: external_exports.string().optional().describe("Filter by account ID. Omit to list all transactions."),
14734
- since_date: external_exports.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD). Omit for all transactions.")
15065
+ since_date: external_exports.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD). Omit for all transactions."),
15066
+ until_date: external_exports.string().optional().describe(
15067
+ "Only return transactions on or before this date (YYYY-MM-DD). Combine with since_date for a date range."
15068
+ ),
15069
+ payee_search: external_exports.string().optional().describe(
15070
+ "Case-insensitive substring match against payee_name, imported_payee, and original_imported_payee. Useful for finding all transactions for a merchant without knowing the exact payee ID."
15071
+ )
14735
15072
  }),
14736
15073
  output: external_exports.object({
14737
15074
  transactions: external_exports.array(transactionSchema).describe("List of transactions")
@@ -14739,112 +15076,211 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14739
15076
  handle: async (params) => {
14740
15077
  const planId = getPlanId();
14741
15078
  const result = await syncBudget(planId);
14742
- const raw = result.changed_entities?.be_transactions ?? [];
14743
- let transactions = raw.filter((t) => !t.is_tombstone).map(mapTransaction);
15079
+ const entities = result.changed_entities;
15080
+ const raw = entities?.be_transactions ?? [];
15081
+ const lookups = buildLookups(entities ?? {});
15082
+ let filtered = raw.filter(notTombstone);
14744
15083
  if (params.account_id) {
14745
- transactions = transactions.filter((t) => t.account_id === params.account_id);
15084
+ filtered = filtered.filter((t) => t.entities_account_id === params.account_id);
14746
15085
  }
14747
15086
  if (params.since_date) {
14748
15087
  const sinceDate = params.since_date;
14749
- transactions = transactions.filter((t) => t.date >= sinceDate);
15088
+ filtered = filtered.filter((t) => (t.date ?? "") >= sinceDate);
15089
+ }
15090
+ if (params.until_date) {
15091
+ const untilDate = params.until_date;
15092
+ filtered = filtered.filter((t) => (t.date ?? "") <= untilDate);
15093
+ }
15094
+ if (params.payee_search) {
15095
+ const needle = params.payee_search.toLowerCase();
15096
+ const matchingPayeeIds = /* @__PURE__ */ new Set();
15097
+ for (const [id, name] of lookups.payees) {
15098
+ if (name.toLowerCase().includes(needle)) matchingPayeeIds.add(id);
15099
+ }
15100
+ filtered = filtered.filter((t) => {
15101
+ if (t.entities_payee_id && matchingPayeeIds.has(t.entities_payee_id)) return true;
15102
+ if (t.imported_payee?.toLowerCase().includes(needle)) return true;
15103
+ if (t.original_imported_payee?.toLowerCase().includes(needle)) return true;
15104
+ return false;
15105
+ });
14750
15106
  }
14751
- transactions.sort((a, b) => b.date.localeCompare(a.date));
15107
+ const transactions = filtered.map((t) => mapTransaction(t, lookups)).sort((a, b) => b.date.localeCompare(a.date));
14752
15108
  return { transactions };
14753
15109
  }
14754
15110
  });
14755
15111
 
14756
- // src/tools/get-transaction.ts
14757
- var getTransaction = defineTool({
14758
- name: "get_transaction",
14759
- displayName: "Get Transaction",
14760
- description: "Get details for a specific transaction by its ID. Returns full transaction data including any split subtransactions.",
14761
- summary: "Get transaction details by ID",
14762
- icon: "receipt",
14763
- group: "Transactions",
15112
+ // src/tools/move-category-budget.ts
15113
+ var moveCategoryBudget = defineTool({
15114
+ name: "move_category_budget",
15115
+ displayName: "Move Category Budget",
15116
+ description: "Move budgeted money between categories or to/from Ready to Assign for a specific month. Omit from_category_id to move money out of Ready to Assign; omit to_category_id to move money back to Ready to Assign. Both null is invalid.",
15117
+ summary: "Move money between budget categories",
15118
+ icon: "arrow-left-right",
15119
+ group: "Categories",
14764
15120
  input: external_exports.object({
14765
- transaction_id: external_exports.string().min(1).describe("Transaction ID to retrieve")
15121
+ month: external_exports.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, "Month must be YYYY-MM or YYYY-MM-DD").describe("Month in YYYY-MM format (e.g. 2026-03)"),
15122
+ amount: external_exports.number().positive().describe("Amount to move in currency units (e.g. 50 for $50)"),
15123
+ from_category_id: external_exports.string().optional().describe("Source category ID. Omit to move from Ready to Assign."),
15124
+ to_category_id: external_exports.string().optional().describe("Destination category ID. Omit to move to Ready to Assign.")
15125
+ }).refine((p) => p.from_category_id || p.to_category_id, {
15126
+ message: "At least one of from_category_id or to_category_id must be provided"
15127
+ }).refine((p) => !p.from_category_id || !p.to_category_id || p.from_category_id !== p.to_category_id, {
15128
+ message: "from_category_id and to_category_id must differ"
14766
15129
  }),
14767
15130
  output: external_exports.object({
14768
- transaction: transactionSchema,
14769
- subtransactions: external_exports.array(subtransactionSchema).describe("Split subtransactions (empty if not a split)")
15131
+ categories: external_exports.array(categorySchema).describe("Updated categories (1 if RTA is involved, 2 for category-to-category)")
14770
15132
  }),
14771
15133
  handle: async (params) => {
14772
15134
  const planId = getPlanId();
14773
- const result = await syncBudget(planId);
14774
- const entities = result.changed_entities;
14775
- const raw = entities?.be_transactions ?? [];
14776
- const tx = raw.find((t) => t.id === params.transaction_id && !t.is_tombstone);
14777
- if (!tx) {
14778
- throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
14779
- }
14780
- const allSubs = entities?.be_subtransactions ?? [];
14781
- const subtransactions = allSubs.filter((s) => s.entities_transaction_id === params.transaction_id && !s.is_tombstone).map(mapSubtransaction);
14782
- return {
14783
- transaction: mapTransaction(tx),
14784
- subtransactions
15135
+ const userId = getUserId();
15136
+ const milliunits = toMilliunits(params.amount);
15137
+ const monthKey = toMonthKey(params.month);
15138
+ const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
15139
+ const budget = await syncBudget(planId);
15140
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15141
+ const subcategories = budget.changed_entities?.be_subcategories ?? [];
15142
+ const existingBudgets = budget.changed_entities?.be_monthly_subcategory_budgets ?? [];
15143
+ const findCategory = (id) => {
15144
+ const c = subcategories.find((s) => s.id === id && notTombstone(s));
15145
+ if (!c?.id) throw ToolError.notFound(`Category not found: ${id}`);
15146
+ return { ...c, id: c.id };
15147
+ };
15148
+ const fromCategory = params.from_category_id ? findCategory(params.from_category_id) : null;
15149
+ const toCategory = params.to_category_id ? findCategory(params.to_category_id) : null;
15150
+ const fromBudgetId = fromCategory ? formatSubcategoryBudgetId(monthKey, fromCategory.id) : null;
15151
+ const toBudgetId = toCategory ? formatSubcategoryBudgetId(monthKey, toCategory.id) : null;
15152
+ const buildEntry = (categoryId, budgetId, signedDelta) => {
15153
+ const current = existingBudgets.find((b) => b.id === budgetId && notTombstone(b))?.budgeted ?? 0;
15154
+ return {
15155
+ id: budgetId,
15156
+ is_tombstone: false,
15157
+ entities_monthly_budget_id: monthlyBudgetId,
15158
+ entities_subcategory_id: categoryId,
15159
+ budgeted: current + signedDelta
15160
+ };
14785
15161
  };
15162
+ const budgetEntries = [];
15163
+ if (fromCategory && fromBudgetId) budgetEntries.push(buildEntry(fromCategory.id, fromBudgetId, -milliunits));
15164
+ if (toCategory && toBudgetId) budgetEntries.push(buildEntry(toCategory.id, toBudgetId, milliunits));
15165
+ const source = fromCategory && toCategory ? MONEY_MOVEMENT_SOURCE.MOVEMENT : MONEY_MOVEMENT_SOURCE.ASSIGN;
15166
+ const result = await syncWrite(
15167
+ planId,
15168
+ {
15169
+ be_monthly_subcategory_budgets: budgetEntries,
15170
+ be_money_movements: [
15171
+ {
15172
+ id: crypto.randomUUID(),
15173
+ is_tombstone: false,
15174
+ from_entities_monthly_subcategory_budget_id: fromBudgetId,
15175
+ to_entities_monthly_subcategory_budget_id: toBudgetId,
15176
+ entities_money_movement_group_id: null,
15177
+ amount: milliunits,
15178
+ performed_by_user_id: userId,
15179
+ note: null,
15180
+ source,
15181
+ move_started_at: (/* @__PURE__ */ new Date()).toISOString(),
15182
+ move_accepted_at: null
15183
+ }
15184
+ ]
15185
+ },
15186
+ serverKnowledge
15187
+ );
15188
+ const calcMap = buildSubcategoryCalcMap(result.changed_entities?.be_monthly_subcategory_budget_calculations ?? []);
15189
+ const budgetMap = buildSubcategoryBudgetMap(result.changed_entities?.be_monthly_subcategory_budgets ?? []);
15190
+ for (const e of budgetEntries) {
15191
+ const key = `${monthKey}/${e.entities_subcategory_id}`;
15192
+ if (!budgetMap.has(key)) budgetMap.set(key, e);
15193
+ }
15194
+ const categories = [];
15195
+ if (fromCategory) categories.push(mapCategoryForMonth(fromCategory, budgetMap, calcMap, monthKey));
15196
+ if (toCategory) categories.push(mapCategoryForMonth(toCategory, budgetMap, calcMap, monthKey));
15197
+ return { categories };
14786
15198
  }
14787
15199
  });
14788
15200
 
14789
- // src/tools/create-transaction.ts
14790
- var createTransaction = defineTool({
14791
- name: "create_transaction",
14792
- displayName: "Create Transaction",
14793
- description: "Create a new transaction in the active YNAB plan. Amount is in currency units (e.g. -42.50 for a $42.50 expense, 1500 for $1500 income). Negative amounts are outflows (expenses), positive amounts are inflows (income).",
14794
- summary: "Create a new transaction",
14795
- icon: "plus",
14796
- group: "Transactions",
15201
+ // src/tools/update-category-budget.ts
15202
+ var updateCategoryBudget = defineTool({
15203
+ name: "update_category_budget",
15204
+ displayName: "Update Category Budget",
15205
+ description: "Set the budgeted amount for a category in a specific month. Amount is in currency units (e.g. 500 to budget $500). The month should be in YYYY-MM format (e.g. 2026-03 for March 2026).",
15206
+ summary: "Set budgeted amount for a category",
15207
+ icon: "pencil",
15208
+ group: "Categories",
14797
15209
  input: external_exports.object({
14798
- account_id: external_exports.string().min(1).describe("Account ID to create the transaction in"),
14799
- date: external_exports.string().min(1).describe("Transaction date in YYYY-MM-DD format"),
14800
- amount: external_exports.number().describe(
14801
- "Amount in currency units (negative for expenses, positive for income). E.g. -42.50 for a $42.50 expense."
14802
- ),
14803
- payee_name: external_exports.string().optional().describe("Payee name (creates new payee if not found)"),
14804
- payee_id: external_exports.string().optional().describe("Existing payee ID (takes precedence over payee_name)"),
14805
- category_id: external_exports.string().optional().describe("Category ID to assign"),
14806
- memo: external_exports.string().optional().describe("Transaction memo"),
14807
- cleared: external_exports.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status (default uncleared)"),
14808
- approved: external_exports.boolean().optional().describe("Whether the transaction is approved (default true)"),
14809
- flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color")
15210
+ category_id: external_exports.string().min(1).describe("Category ID to budget"),
15211
+ month: external_exports.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, "Month must be YYYY-MM or YYYY-MM-DD").describe("Month in YYYY-MM format (e.g. 2026-03)"),
15212
+ budgeted: external_exports.number().describe("Amount to budget in currency units (e.g. 500 for $500)")
14810
15213
  }),
14811
15214
  output: external_exports.object({
14812
- transaction: transactionSchema
15215
+ category: categorySchema
14813
15216
  }),
14814
15217
  handle: async (params) => {
14815
15218
  const planId = getPlanId();
14816
- const milliunits = Math.round(params.amount * 1e3);
14817
- const txId = crypto.randomUUID();
14818
- const transaction = {
14819
- id: txId,
14820
- entities_account_id: params.account_id,
14821
- date: params.date,
14822
- amount: milliunits,
14823
- cleared: params.cleared ?? "uncleared",
14824
- approved: params.approved ?? true,
14825
- memo: params.memo ?? null,
14826
- flag_color: params.flag_color ?? null,
14827
- entities_payee_id: params.payee_id ?? null,
14828
- payee_name: params.payee_name ?? null,
14829
- entities_subcategory_id: params.category_id ?? null,
14830
- is_tombstone: false
15219
+ const userId = getUserId();
15220
+ const milliunits = toMilliunits(params.budgeted);
15221
+ const monthKey = toMonthKey(params.month);
15222
+ const budgetId = formatSubcategoryBudgetId(monthKey, params.category_id);
15223
+ const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
15224
+ const budget = await syncBudget(planId);
15225
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15226
+ const category = (budget.changed_entities?.be_subcategories ?? []).find(
15227
+ (c) => c.id === params.category_id && notTombstone(c)
15228
+ );
15229
+ if (!category) {
15230
+ throw ToolError.notFound(`Category not found: ${params.category_id}`);
15231
+ }
15232
+ const existingBudget = (budget.changed_entities?.be_monthly_subcategory_budgets ?? []).find(
15233
+ (b) => b.id === budgetId && notTombstone(b)
15234
+ );
15235
+ const delta = milliunits - (existingBudget?.budgeted ?? 0);
15236
+ const budgetEntry = {
15237
+ id: budgetId,
15238
+ is_tombstone: false,
15239
+ entities_monthly_budget_id: monthlyBudgetId,
15240
+ entities_subcategory_id: params.category_id,
15241
+ budgeted: milliunits
14831
15242
  };
14832
- const result = await syncWrite(planId, {
14833
- be_transactions: [transaction]
14834
- });
14835
- const saved = result.changed_entities?.be_transactions?.[0];
14836
- if (!saved) {
14837
- throw ToolError.internal("Transaction was created but no data was returned");
15243
+ const changedEntities = { be_monthly_subcategory_budgets: [budgetEntry] };
15244
+ if (delta !== 0) {
15245
+ changedEntities.be_money_movements = [
15246
+ {
15247
+ id: crypto.randomUUID(),
15248
+ is_tombstone: false,
15249
+ to_entities_monthly_subcategory_budget_id: delta > 0 ? budgetId : null,
15250
+ from_entities_monthly_subcategory_budget_id: delta < 0 ? budgetId : null,
15251
+ entities_money_movement_group_id: null,
15252
+ amount: Math.abs(delta),
15253
+ performed_by_user_id: userId,
15254
+ note: null,
15255
+ source: MONEY_MOVEMENT_SOURCE.ASSIGN,
15256
+ move_started_at: (/* @__PURE__ */ new Date()).toISOString(),
15257
+ move_accepted_at: null
15258
+ }
15259
+ ];
14838
15260
  }
14839
- return { transaction: mapTransaction(saved) };
15261
+ const result = await syncWrite(planId, changedEntities, serverKnowledge);
15262
+ const calcMap = buildSubcategoryCalcMap(result.changed_entities?.be_monthly_subcategory_budget_calculations ?? []);
15263
+ const budgetMap = buildSubcategoryBudgetMap(result.changed_entities?.be_monthly_subcategory_budgets ?? []);
15264
+ const key = `${monthKey}/${params.category_id}`;
15265
+ if (!budgetMap.has(key)) budgetMap.set(key, budgetEntry);
15266
+ return { category: mapCategoryForMonth(category, budgetMap, calcMap, monthKey) };
14840
15267
  }
14841
15268
  });
14842
15269
 
14843
15270
  // src/tools/update-transaction.ts
15271
+ var resolveFlag = (requested, existing2) => {
15272
+ if (requested === "none") return null;
15273
+ if (requested) return FLAG_MAP[requested];
15274
+ return existing2 ?? null;
15275
+ };
15276
+ var resolveCleared = (requested, existing2) => {
15277
+ if (requested) return CLEARED_MAP[requested];
15278
+ return existing2 ?? "Uncleared";
15279
+ };
14844
15280
  var updateTransaction = defineTool({
14845
15281
  name: "update_transaction",
14846
15282
  displayName: "Update Transaction",
14847
- description: "Update an existing transaction in the active YNAB plan. Only specified fields are changed; omitted fields remain unchanged. Amount is in currency units (negative for expenses, positive for income).",
15283
+ description: "Update an existing transaction in the active YNAB plan. Only specified fields are changed; omitted fields remain unchanged. Amount is in currency units (negative for expenses, positive for income). Transfers and split transactions cannot be updated through this tool \u2014 edit them directly in YNAB.",
14848
15284
  summary: "Update a transaction",
14849
15285
  icon: "pencil",
14850
15286
  group: "Transactions",
@@ -14859,175 +15295,85 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14859
15295
  memo: external_exports.string().optional().describe("New transaction memo"),
14860
15296
  cleared: external_exports.enum(["cleared", "uncleared", "reconciled"]).optional().describe("New cleared status"),
14861
15297
  approved: external_exports.boolean().optional().describe("New approval status"),
14862
- flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("New flag color")
15298
+ flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple", "none"]).optional().describe('New flag color (pass "none" to clear)')
14863
15299
  }),
14864
15300
  output: external_exports.object({
14865
15301
  transaction: transactionSchema
14866
15302
  }),
14867
15303
  handle: async (params) => {
14868
15304
  const planId = getPlanId();
14869
- const transaction = {
14870
- id: params.transaction_id,
14871
- entities_account_id: params.account_id
14872
- };
14873
- if (params.date !== void 0) transaction.date = params.date;
14874
- if (params.amount !== void 0) transaction.amount = Math.round(params.amount * 1e3);
14875
- if (params.payee_name !== void 0) transaction.payee_name = params.payee_name;
14876
- if (params.payee_id !== void 0) transaction.entities_payee_id = params.payee_id;
14877
- if (params.category_id !== void 0) transaction.entities_subcategory_id = params.category_id;
14878
- if (params.memo !== void 0) transaction.memo = params.memo;
14879
- if (params.cleared !== void 0) transaction.cleared = params.cleared;
14880
- if (params.approved !== void 0) transaction.approved = params.approved;
14881
- if (params.flag_color !== void 0) transaction.flag_color = params.flag_color;
14882
- const result = await syncWrite(planId, {
14883
- be_transactions: [transaction]
14884
- });
14885
- const saved = result.changed_entities?.be_transactions?.[0];
14886
- if (!saved) {
14887
- throw ToolError.internal("Transaction was updated but no data was returned");
15305
+ const budget = await syncBudget(planId);
15306
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15307
+ const lookups = buildLookups(budget.changed_entities ?? {});
15308
+ const existing2 = budget.changed_entities?.be_transactions?.find(
15309
+ (t) => t.id === params.transaction_id && !t.is_tombstone
15310
+ );
15311
+ if (!existing2) {
15312
+ throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
14888
15313
  }
14889
- return { transaction: mapTransaction(saved) };
14890
- }
14891
- });
14892
-
14893
- // src/tools/delete-transaction.ts
14894
- var deleteTransaction = defineTool({
14895
- name: "delete_transaction",
14896
- displayName: "Delete Transaction",
14897
- description: "Delete a transaction from the active YNAB plan. This marks the transaction as deleted (soft delete).",
14898
- summary: "Delete a transaction",
14899
- icon: "trash-2",
14900
- group: "Transactions",
14901
- input: external_exports.object({
14902
- transaction_id: external_exports.string().min(1).describe("Transaction ID to delete"),
14903
- account_id: external_exports.string().min(1).describe("Account ID the transaction belongs to")
14904
- }),
14905
- output: external_exports.object({
14906
- success: external_exports.boolean().describe("Whether the operation succeeded")
14907
- }),
14908
- handle: async (params) => {
14909
- const planId = getPlanId();
14910
- await syncWrite(planId, {
14911
- be_transactions: [
14912
- {
15314
+ if (existing2.transfer_account_id) {
15315
+ throw ToolError.validation("Cannot update transfer transactions \u2014 edit them in YNAB directly.");
15316
+ }
15317
+ const hasSubtransactions = (budget.changed_entities?.be_subtransactions ?? []).some(
15318
+ (s) => s.entities_transaction_id === params.transaction_id && !s.is_tombstone
15319
+ );
15320
+ if (hasSubtransactions) {
15321
+ throw ToolError.validation("Cannot update split transactions \u2014 edit them in YNAB directly.");
15322
+ }
15323
+ const changedEntities = {};
15324
+ let payeeId = params.payee_id ?? existing2.entities_payee_id ?? null;
15325
+ if (params.payee_name && !params.payee_id) {
15326
+ const resolved = resolvePayee(budget.changed_entities?.be_payees ?? [], params.payee_name);
15327
+ payeeId = resolved.payeeId;
15328
+ if (resolved.newPayee) {
15329
+ changedEntities.be_payees = [resolved.newPayee];
15330
+ lookups.payees.set(resolved.payeeId, params.payee_name);
15331
+ }
15332
+ }
15333
+ changedEntities.be_transaction_groups = [
15334
+ {
15335
+ id: params.transaction_id,
15336
+ be_transaction: {
14913
15337
  id: params.transaction_id,
15338
+ is_tombstone: false,
14914
15339
  entities_account_id: params.account_id,
14915
- is_tombstone: true
14916
- }
14917
- ]
14918
- });
14919
- return { success: true };
14920
- }
14921
- });
14922
-
14923
- // src/tools/list-scheduled-transactions.ts
14924
- var listScheduledTransactions = defineTool({
14925
- name: "list_scheduled_transactions",
14926
- displayName: "List Scheduled Transactions",
14927
- description: "List all scheduled (recurring) transactions in the active YNAB plan. Returns frequency, next occurrence date, amount, payee, and category for each.",
14928
- summary: "List scheduled/recurring transactions",
14929
- icon: "clock",
14930
- group: "Transactions",
14931
- input: external_exports.object({}),
14932
- output: external_exports.object({
14933
- scheduled_transactions: external_exports.array(scheduledTransactionSchema).describe("List of scheduled transactions")
14934
- }),
14935
- handle: async () => {
14936
- const planId = getPlanId();
14937
- const result = await syncBudget(planId);
14938
- const raw = result.changed_entities?.be_scheduled_transactions ?? [];
14939
- const scheduledTransactions = raw.filter((s) => !s.is_tombstone).map(mapScheduledTransaction).sort((a, b) => a.date_next.localeCompare(b.date_next));
14940
- return { scheduled_transactions: scheduledTransactions };
14941
- }
14942
- });
14943
-
14944
- // src/tools/list-months.ts
14945
- var listMonths = defineTool({
14946
- name: "list_months",
14947
- displayName: "List Months",
14948
- description: "List all budget months in the active YNAB plan. Returns income, budgeted, activity, and Ready to Assign amounts for each month. Sorted from most recent to oldest.",
14949
- summary: "List budget months with summaries",
14950
- icon: "calendar",
14951
- group: "Months",
14952
- input: external_exports.object({}),
14953
- output: external_exports.object({
14954
- months: external_exports.array(monthSchema).describe("List of budget months")
14955
- }),
14956
- handle: async () => {
14957
- const planId = getPlanId();
14958
- const result = await syncBudget(planId);
14959
- const entities = result.changed_entities;
14960
- const rawMonths = entities?.be_monthly_budgets ?? [];
14961
- const rawCalcs = entities?.be_monthly_budget_calculations ?? [];
14962
- const calcMap = /* @__PURE__ */ new Map();
14963
- for (const calc of rawCalcs) {
14964
- const budgetId = calc.entities_monthly_budget_id;
14965
- if (budgetId) {
14966
- const month = budgetId.replace("mb/", "");
14967
- calcMap.set(month, calc);
15340
+ entities_payee_id: payeeId,
15341
+ entities_subcategory_id: params.category_id ?? existing2.entities_subcategory_id ?? null,
15342
+ entities_scheduled_transaction_id: existing2.entities_scheduled_transaction_id ?? null,
15343
+ date: params.date ?? existing2.date ?? "",
15344
+ date_entered_from_schedule: null,
15345
+ amount: params.amount !== void 0 ? toMilliunits(params.amount) : existing2.amount ?? 0,
15346
+ cash_amount: 0,
15347
+ credit_amount: 0,
15348
+ credit_amount_adjusted: 0,
15349
+ subcategory_credit_amount_preceding: 0,
15350
+ memo: params.memo ?? existing2.memo ?? null,
15351
+ cleared: resolveCleared(params.cleared, existing2.cleared),
15352
+ // YNAB's wire format calls this "accepted"; the public tool surface uses "approved".
15353
+ accepted: params.approved ?? existing2.accepted ?? false,
15354
+ check_number: null,
15355
+ flag: resolveFlag(params.flag_color, existing2.flag),
15356
+ transfer_account_id: existing2.transfer_account_id ?? null,
15357
+ transfer_transaction_id: null,
15358
+ transfer_subtransaction_id: null,
15359
+ matched_transaction_id: null,
15360
+ ynab_id: existing2.ynab_id ?? null,
15361
+ imported_payee: existing2.imported_payee ?? null,
15362
+ imported_date: null,
15363
+ original_imported_payee: existing2.original_imported_payee ?? null,
15364
+ provider_cleansed_payee: null,
15365
+ source: existing2.source ?? null,
15366
+ debt_transaction_type: null
15367
+ },
15368
+ be_subtransactions: null
14968
15369
  }
15370
+ ];
15371
+ const result = await syncWrite(planId, changedEntities, serverKnowledge);
15372
+ const saved = result.changed_entities?.be_transactions?.find((t) => t.id === params.transaction_id);
15373
+ if (!saved) {
15374
+ throw ToolError.internal("Transaction was updated but no data was returned");
14969
15375
  }
14970
- const months = rawMonths.filter((m) => !m.is_tombstone).map((m) => mapMonth(m, calcMap.get(m.month ?? ""))).sort((a, b) => b.month.localeCompare(a.month));
14971
- return { months };
14972
- }
14973
- });
14974
-
14975
- // src/tools/get-month.ts
14976
- var getMonth = defineTool({
14977
- name: "get_month",
14978
- displayName: "Get Month",
14979
- description: "Get budget summary and category details for a specific month. Returns the month overview (income, budgeted, activity, Ready to Assign) plus per-category breakdowns. Month format is YYYY-MM-DD using the first of the month (e.g. 2026-03-01).",
14980
- summary: "Get budget details for a month",
14981
- icon: "calendar",
14982
- group: "Months",
14983
- input: external_exports.object({
14984
- month: external_exports.string().min(1).describe("Month in YYYY-MM-DD format (first of month, e.g. 2026-03-01)")
14985
- }),
14986
- output: external_exports.object({
14987
- month: monthSchema,
14988
- categories: external_exports.array(categorySchema).describe("Category budgets for this month")
14989
- }),
14990
- handle: async (params) => {
14991
- const planId = getPlanId();
14992
- const result = await syncBudget(planId);
14993
- const entities = result.changed_entities;
14994
- const rawMonths = entities?.be_monthly_budgets ?? [];
14995
- const monthData = rawMonths.find((m) => m.month === params.month && !m.is_tombstone);
14996
- if (!monthData) {
14997
- throw ToolError.notFound(`Month not found: ${params.month}`);
14998
- }
14999
- const monthlyCalcs = entities?.be_monthly_budget_calculations ?? [];
15000
- const monthCalc = monthlyCalcs.find((c) => {
15001
- const budgetId = c.entities_monthly_budget_id;
15002
- return budgetId && budgetId.replace("mb/", "") === params.month;
15003
- });
15004
- const rawCategories = (entities?.be_subcategories ?? []).filter((c) => !c.is_tombstone && c.is_hidden !== true);
15005
- const subcatCalcs = entities?.be_monthly_subcategory_budget_calculations ?? [];
15006
- const calcMap = /* @__PURE__ */ new Map();
15007
- for (const calc of subcatCalcs) {
15008
- const entityId = calc.entities_monthly_subcategory_budget_id;
15009
- if (entityId) {
15010
- const parts = entityId.split("/");
15011
- const categoryId = parts.length >= 3 ? parts.slice(2).join("/") : entityId;
15012
- calcMap.set(categoryId, calc);
15013
- }
15014
- }
15015
- const categories = rawCategories.map((c) => {
15016
- const calc = calcMap.get(c.id ?? "");
15017
- return mapCategory({
15018
- ...c,
15019
- budgeted: calc?.budgeted ?? c.budgeted,
15020
- activity: calc?.activity ?? c.activity,
15021
- balance: calc?.balance ?? c.balance,
15022
- goal_type: calc?.goal_type ?? c.goal_type,
15023
- goal_target: calc?.goal_target ?? c.goal_target,
15024
- goal_percentage_complete: calc?.goal_percentage_complete ?? c.goal_percentage_complete
15025
- });
15026
- });
15027
- return {
15028
- month: mapMonth(monthData, monthCalc),
15029
- categories
15030
- };
15376
+ return { transaction: mapTransaction(saved, lookups) };
15031
15377
  }
15032
15378
  });
15033
15379
 
@@ -15049,6 +15395,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15049
15395
  // Categories
15050
15396
  listCategories,
15051
15397
  updateCategoryBudget,
15398
+ moveCategoryBudget,
15052
15399
  // Payees
15053
15400
  listPayees,
15054
15401
  // Transactions
@@ -15069,7 +15416,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15069
15416
  };
15070
15417
  var src_default = new YnabPlugin();
15071
15418
 
15072
- // dist/_adapter_entry_331af839-0591-41a5-afc4-b832c8b79610.ts
15419
+ // dist/_adapter_entry_fb321989-7b03-4647-9b5e-64822784cb35.ts
15073
15420
  if (!globalThis.__openTabs) {
15074
15421
  globalThis.__openTabs = {};
15075
15422
  } else {
@@ -15285,5 +15632,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15285
15632
  };
15286
15633
  delete src_default.onDeactivate;
15287
15634
  }
15288
- })();(function(){var o=(globalThis).__openTabs;if(o&&o.adapters&&o.adapters["ynab"]){var a=o.adapters["ynab"];a.__adapterHash="0559e5ad8503b3f1094920c7c7fb557b8f9e39f79aa5dd2919a91ca6b0f29c1e";if(a.tools&&Array.isArray(a.tools)){for(var i=0;i<a.tools.length;i++){Object.freeze(a.tools[i]);}Object.freeze(a.tools);}Object.freeze(a);Object.defineProperty(o.adapters,"ynab",{value:a,writable:false,configurable:false,enumerable:true});Object.defineProperty(o,"adapters",{value:o.adapters,writable:false,configurable:false});}})();
15635
+ })();(function(){var o=(globalThis).__openTabs;if(o&&o.adapters&&o.adapters["ynab"]){var a=o.adapters["ynab"];a.__adapterHash="dddcb110d2a66a4da7a57c142d0df2274bcc27f65c27b3a8d89c81d827caa8de";if(a.tools&&Array.isArray(a.tools)){for(var i=0;i<a.tools.length;i++){Object.freeze(a.tools[i]);}Object.freeze(a.tools);}Object.freeze(a);Object.defineProperty(o.adapters,"ynab",{value:a,writable:false,configurable:false,enumerable:true});Object.defineProperty(o,"adapters",{value:o.adapters,writable:false,configurable:false});}})();
15289
15636
  //# sourceMappingURL=adapter.iife.js.map