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

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 (41) hide show
  1. package/dist/adapter.iife.js +447 -24
  2. package/dist/adapter.iife.js.map +4 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +12 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/tools/create-category-group.d.ts +11 -0
  7. package/dist/tools/create-category-group.d.ts.map +1 -0
  8. package/dist/tools/create-category-group.js +37 -0
  9. package/dist/tools/create-category-group.js.map +1 -0
  10. package/dist/tools/create-category.d.ts +59 -0
  11. package/dist/tools/create-category.d.ts.map +1 -0
  12. package/dist/tools/create-category.js +63 -0
  13. package/dist/tools/create-category.js.map +1 -0
  14. package/dist/tools/delete-category-group.d.ts +8 -0
  15. package/dist/tools/delete-category-group.d.ts.map +1 -0
  16. package/dist/tools/delete-category-group.js +33 -0
  17. package/dist/tools/delete-category-group.js.map +1 -0
  18. package/dist/tools/delete-category.d.ts +7 -0
  19. package/dist/tools/delete-category.d.ts.map +1 -0
  20. package/dist/tools/delete-category.js +28 -0
  21. package/dist/tools/delete-category.js.map +1 -0
  22. package/dist/tools/move-category-budget.d.ts.map +1 -1
  23. package/dist/tools/move-category-budget.js +13 -18
  24. package/dist/tools/move-category-budget.js.map +1 -1
  25. package/dist/tools/schemas.d.ts +80 -1
  26. package/dist/tools/schemas.d.ts.map +1 -1
  27. package/dist/tools/schemas.js +238 -4
  28. package/dist/tools/schemas.js.map +1 -1
  29. package/dist/tools/snooze-category-goal.d.ts +10 -0
  30. package/dist/tools/snooze-category-goal.d.ts.map +1 -0
  31. package/dist/tools/snooze-category-goal.js +53 -0
  32. package/dist/tools/snooze-category-goal.js.map +1 -0
  33. package/dist/tools/update-category-budget.d.ts.map +1 -1
  34. package/dist/tools/update-category-budget.js +3 -6
  35. package/dist/tools/update-category-budget.js.map +1 -1
  36. package/dist/tools/update-category.d.ts +61 -0
  37. package/dist/tools/update-category.d.ts.map +1 -0
  38. package/dist/tools/update-category.js +46 -0
  39. package/dist/tools/update-category.js.map +1 -0
  40. package/dist/tools.json +774 -1
  41. package/package.json +3 -3
@@ -14270,6 +14270,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14270
14270
  };
14271
14271
  var toMilliunits = (amount) => Math.round(amount * 1e3);
14272
14272
  var notTombstone = (x) => !x.is_tombstone;
14273
+ var findCategory = (entities, id) => {
14274
+ const c = (entities?.be_subcategories ?? []).find((s) => s.id === id && notTombstone(s));
14275
+ if (!c) throw ToolError.notFound(`Category not found: ${id}`);
14276
+ return c;
14277
+ };
14278
+ var findCategoryGroup = (entities, id) => {
14279
+ const g = (entities?.be_master_categories ?? []).find((m) => m.id === id && notTombstone(m));
14280
+ if (!g) throw ToolError.notFound(`Category group not found: ${id}`);
14281
+ return g;
14282
+ };
14283
+ var assertCategoryGroupDeletable = (group) => {
14284
+ if (group.deletable !== true) {
14285
+ throw ToolError.validation(`Category group "${group.name}" is not deletable.`);
14286
+ }
14287
+ };
14288
+ var assertCategoryDeletable = (category) => {
14289
+ if (category.entities_account_id != null || category.type !== CATEGORY_TYPE_DEFAULT) {
14290
+ throw ToolError.validation(`Category "${category.name}" is system-managed and cannot be deleted.`);
14291
+ }
14292
+ };
14293
+ var nextTopSortableIndex = (rows, step = 10) => {
14294
+ let min = 0;
14295
+ for (const r of rows) {
14296
+ if (typeof r.sortable_index === "number" && r.sortable_index < min) min = r.sortable_index;
14297
+ }
14298
+ return min - step;
14299
+ };
14273
14300
  var userSchema = external_exports.object({
14274
14301
  id: external_exports.string().describe("User ID"),
14275
14302
  first_name: external_exports.string().describe("First name"),
@@ -14390,6 +14417,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14390
14417
  /** Category-to-category transfer. */
14391
14418
  MOVEMENT: "manual_movement"
14392
14419
  };
14420
+ var GOAL_TYPE = {
14421
+ /** "Set aside" or "Refill" — `goal_needs_whole_amount` differentiates. */
14422
+ NEED: "NEED",
14423
+ /** "Have a balance of" — no date, no cadence. */
14424
+ TARGET_BALANCE: "TB",
14425
+ /** "Have a balance of by date" — one-shot with `goal_target_date`. */
14426
+ TARGET_BY_DATE: "TBD",
14427
+ /** Debt payment goals on debt-account categories. */
14428
+ DEBT: "DEBT",
14429
+ /** Legacy "Monthly Funding". The modern UI no longer creates these but they
14430
+ still exist on older categories and the API still honors them. */
14431
+ MONTHLY_FUNDING: "MF"
14432
+ };
14433
+ var CATEGORY_TYPE_DEFAULT = "DFT";
14393
14434
  var SUBCATEGORY_BUDGET_PREFIX = "mcb";
14394
14435
  var MONTHLY_BUDGET_PREFIX = "mb";
14395
14436
  var toMonthKey = (month) => month.substring(0, 7);
@@ -14412,6 +14453,128 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14412
14453
  blue: "Blue",
14413
14454
  purple: "Purple"
14414
14455
  };
14456
+ var cadenceWireValue = {
14457
+ weekly: 2,
14458
+ monthly: 1,
14459
+ yearly: 13
14460
+ };
14461
+ var isValidCalendarDate = (s) => {
14462
+ const parts = s.split("-").map(Number);
14463
+ const year = parts[0] ?? 0;
14464
+ const month = parts[1] ?? 0;
14465
+ const day = parts[2] ?? 0;
14466
+ if (month < 1 || month > 12) return false;
14467
+ const maxDay = new Date(year, month, 0).getDate();
14468
+ return day >= 1 && day <= maxDay;
14469
+ };
14470
+ var needGoalShape = {
14471
+ target: external_exports.number().positive().describe("Goal amount in currency units (e.g. 50 for $50)"),
14472
+ cadence: external_exports.enum(["weekly", "monthly", "yearly"]).optional().describe("How often the goal recurs. Defaults to monthly."),
14473
+ every: external_exports.number().int().min(1).optional().describe("Multiplier on cadence (e.g. cadence=monthly + every=5 means every 5 months). Defaults to 1."),
14474
+ day: external_exports.number().int().min(0).max(31).optional().describe("Day-of-week (0=Sunday, 6=Saturday) for weekly cadence, or day-of-month (1-31) for monthly cadence."),
14475
+ start_date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD").refine(isValidCalendarDate, "Date must be a valid calendar date").optional().describe("First occurrence date (YYYY-MM-DD). Required for yearly cadence; optional for others.")
14476
+ };
14477
+ var yearlyNeedRefine = (data, ctx) => {
14478
+ if (data.cadence === "yearly" && !data.start_date) {
14479
+ ctx.addIssue({
14480
+ code: external_exports.ZodIssueCode.custom,
14481
+ message: 'start_date is required when cadence is "yearly"',
14482
+ path: ["start_date"]
14483
+ });
14484
+ }
14485
+ };
14486
+ var needCadenceDayRefine = (data, ctx) => {
14487
+ if (data.day === void 0) return;
14488
+ const cadence = data.cadence ?? "monthly";
14489
+ if (cadence === "weekly" && (data.day < 0 || data.day > 6)) {
14490
+ ctx.addIssue({
14491
+ code: external_exports.ZodIssueCode.custom,
14492
+ message: "For weekly cadence, day must be 0\u20136 (0=Sunday, 6=Saturday)",
14493
+ path: ["day"]
14494
+ });
14495
+ } else if (cadence === "monthly" && (data.day < 1 || data.day > 31)) {
14496
+ ctx.addIssue({
14497
+ code: external_exports.ZodIssueCode.custom,
14498
+ message: "For monthly cadence, day must be 1\u201331",
14499
+ path: ["day"]
14500
+ });
14501
+ } else if (cadence === "yearly") {
14502
+ ctx.addIssue({
14503
+ code: external_exports.ZodIssueCode.custom,
14504
+ message: "For yearly cadence, use start_date to set the recurrence anchor instead of day",
14505
+ path: ["day"]
14506
+ });
14507
+ }
14508
+ };
14509
+ var goalSpecSchema = external_exports.discriminatedUnion("type", [
14510
+ external_exports.object({ type: external_exports.literal("set_aside"), ...needGoalShape }).strict().superRefine(yearlyNeedRefine).superRefine(needCadenceDayRefine),
14511
+ external_exports.object({ type: external_exports.literal("refill"), ...needGoalShape }).strict().superRefine(yearlyNeedRefine).superRefine(needCadenceDayRefine),
14512
+ external_exports.object({
14513
+ type: external_exports.literal("target_balance"),
14514
+ target: external_exports.number().positive().describe("Balance to maintain in currency units")
14515
+ }).strict(),
14516
+ external_exports.object({
14517
+ type: external_exports.literal("target_by_date"),
14518
+ target: external_exports.number().positive().describe("Target balance to have by the given date in currency units"),
14519
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD").refine(isValidCalendarDate, "Date must be a valid calendar date").describe("Target date YYYY-MM-DD")
14520
+ }).strict(),
14521
+ external_exports.object({
14522
+ type: external_exports.literal("debt"),
14523
+ target: external_exports.number().positive().describe("Monthly payment amount in currency units"),
14524
+ day: external_exports.number().int().min(1).max(31).optional().describe("Day of month the payment is due (1-31). Defaults to 1.")
14525
+ }).strict(),
14526
+ external_exports.object({ type: external_exports.literal("none") }).strict()
14527
+ ]);
14528
+ var NO_GOAL_FIELDS = {
14529
+ goal_type: null,
14530
+ goal_created_on: null,
14531
+ goal_needs_whole_amount: null,
14532
+ goal_target_amount: 0,
14533
+ goal_target_date: null,
14534
+ goal_cadence: null,
14535
+ goal_cadence_frequency: null,
14536
+ goal_day: null
14537
+ };
14538
+ var buildGoalFields = (goal) => {
14539
+ if (!goal || goal.type === "none") return { ...NO_GOAL_FIELDS };
14540
+ const now = /* @__PURE__ */ new Date();
14541
+ const createdOn = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
14542
+ const base = { ...NO_GOAL_FIELDS, goal_created_on: createdOn };
14543
+ switch (goal.type) {
14544
+ case "set_aside":
14545
+ case "refill": {
14546
+ const cadence = goal.cadence ?? "monthly";
14547
+ return {
14548
+ ...base,
14549
+ goal_type: GOAL_TYPE.NEED,
14550
+ goal_needs_whole_amount: goal.type === "set_aside",
14551
+ goal_target_amount: toMilliunits(goal.target),
14552
+ goal_cadence: cadenceWireValue[cadence],
14553
+ goal_cadence_frequency: goal.every ?? 1,
14554
+ goal_day: cadence === "yearly" ? null : goal.day ?? null,
14555
+ goal_target_date: goal.start_date ?? null
14556
+ };
14557
+ }
14558
+ case "target_balance":
14559
+ return { ...base, goal_type: GOAL_TYPE.TARGET_BALANCE, goal_target_amount: toMilliunits(goal.target) };
14560
+ case "target_by_date":
14561
+ return {
14562
+ ...base,
14563
+ goal_type: GOAL_TYPE.TARGET_BY_DATE,
14564
+ goal_target_amount: toMilliunits(goal.target),
14565
+ goal_target_date: goal.date
14566
+ };
14567
+ case "debt":
14568
+ return {
14569
+ ...base,
14570
+ goal_type: GOAL_TYPE.DEBT,
14571
+ goal_target_amount: toMilliunits(goal.target),
14572
+ goal_cadence: cadenceWireValue.monthly,
14573
+ goal_cadence_frequency: 1,
14574
+ goal_day: goal.day ?? 1
14575
+ };
14576
+ }
14577
+ };
14415
14578
  var resolvePayee = (existingPayees, payeeName) => {
14416
14579
  const target = payeeName.toLowerCase();
14417
14580
  const match = existingPayees.find((p) => notTombstone(p) && p.name?.toLowerCase() === target);
@@ -14434,7 +14597,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14434
14597
  };
14435
14598
  return { payeeId, newPayee };
14436
14599
  };
14437
- var buildAccountCalcMap = (entities) => new Map((entities.be_account_calculations ?? []).map((c) => [c.entities_account_id, c]));
14600
+ var buildAccountCalcMap = (entities) => new Map(
14601
+ (entities.be_account_calculations ?? []).filter((c) => c.entities_account_id).map((c) => [c.entities_account_id, c])
14602
+ );
14438
14603
  var buildMonthlyBudgetCalcMap = (calcs) => {
14439
14604
  const map2 = /* @__PURE__ */ new Map();
14440
14605
  for (const calc of calcs) {
@@ -14481,7 +14646,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14481
14646
  budgeted: budget?.budgeted ?? c.budgeted,
14482
14647
  activity: (calc?.cash_outflows ?? 0) + (calc?.credit_outflows ?? 0),
14483
14648
  balance: calc?.balance ?? c.balance,
14484
- goal_target: calc?.goal_target ?? c.goal_target,
14485
14649
  goal_percentage_complete: calc?.goal_percentage_complete ?? c.goal_percentage_complete
14486
14650
  });
14487
14651
  };
@@ -14561,7 +14725,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14561
14725
  activity_milliunits: c.activity ?? 0,
14562
14726
  balance_milliunits: c.balance ?? 0,
14563
14727
  goal_type: c.goal_type ?? "",
14564
- goal_target: formatMilliunits(c.goal_target ?? 0),
14728
+ // The "goal target" the user configured is stored on the category itself:
14729
+ // - MF (legacy Monthly Funding): the static target lives in `monthly_funding`
14730
+ // - All modern goal types (NEED, TB, TBD, DEBT): use `goal_target_amount`
14731
+ // The calc's `goal_target` is YNAB's dynamically-computed "needed this month",
14732
+ // which for refill goals returns max(0, target - current_balance) — surprising
14733
+ // and not what users mean when they ask for the goal target.
14734
+ goal_target: formatMilliunits(
14735
+ (c.goal_type === GOAL_TYPE.MONTHLY_FUNDING ? c.monthly_funding : c.goal_target_amount) ?? 0
14736
+ ),
14565
14737
  goal_percentage_complete: c.goal_percentage_complete ?? 0
14566
14738
  });
14567
14739
  var mapPayee = (p) => ({
@@ -14638,6 +14810,105 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14638
14810
  deleted: s.is_tombstone === true
14639
14811
  });
14640
14812
 
14813
+ // src/tools/create-category.ts
14814
+ var createCategory = defineTool({
14815
+ name: "create_category",
14816
+ displayName: "Create Category",
14817
+ description: 'Create a new category in an existing category group. Optionally set an initial goal: "set_aside" (set aside X per cadence), "refill" (refill the balance up to X per cadence), "target_balance" (have a balance of X), or "target_by_date" (have a balance of X by a specific date). NEED-style goals (set_aside, refill) accept weekly/monthly/yearly cadence. Debt goals are not supported for new categories \u2014 they only apply to existing debt-account categories.',
14818
+ summary: "Create a new budget category",
14819
+ icon: "plus",
14820
+ group: "Categories",
14821
+ input: external_exports.object({
14822
+ group_id: external_exports.string().min(1).describe("Category group ID to create the category in"),
14823
+ name: external_exports.string().min(1).describe("Name of the new category"),
14824
+ goal: goalSpecSchema.optional().describe("Optional initial goal for the category"),
14825
+ note: external_exports.string().optional().describe("Optional note for the category")
14826
+ }),
14827
+ output: external_exports.object({
14828
+ category: categorySchema
14829
+ }),
14830
+ handle: async (params) => {
14831
+ if (params.goal?.type === "debt") {
14832
+ throw ToolError.validation("Debt goals can only be set on debt-account categories.");
14833
+ }
14834
+ const planId = getPlanId();
14835
+ const categoryId = crypto.randomUUID();
14836
+ const budget = await syncBudget(planId);
14837
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
14838
+ assertCategoryGroupDeletable(findCategoryGroup(budget.changed_entities, params.group_id));
14839
+ const childCategories = (budget.changed_entities?.be_subcategories ?? []).filter(
14840
+ (c) => c.entities_master_category_id === params.group_id
14841
+ );
14842
+ const monthKey = currentMonthKey();
14843
+ const categoryEntry = {
14844
+ id: categoryId,
14845
+ is_tombstone: false,
14846
+ entities_master_category_id: params.group_id,
14847
+ entities_account_id: null,
14848
+ internal_name: null,
14849
+ sortable_index: nextTopSortableIndex(childCategories, 5),
14850
+ name: params.name,
14851
+ type: CATEGORY_TYPE_DEFAULT,
14852
+ note: params.note ?? null,
14853
+ monthly_funding: 0,
14854
+ is_hidden: false,
14855
+ pinned_index: null,
14856
+ pinned_goal_index: null,
14857
+ ...buildGoalFields(params.goal)
14858
+ };
14859
+ const budgetEntry = {
14860
+ id: formatSubcategoryBudgetId(monthKey, categoryId),
14861
+ is_tombstone: false,
14862
+ entities_monthly_budget_id: formatMonthlyBudgetId(monthKey, planId),
14863
+ entities_subcategory_id: categoryId,
14864
+ budgeted: 0
14865
+ };
14866
+ await syncWrite(
14867
+ planId,
14868
+ {
14869
+ be_subcategories: [categoryEntry],
14870
+ be_monthly_subcategory_budgets: [budgetEntry]
14871
+ },
14872
+ serverKnowledge
14873
+ );
14874
+ return { category: mapCategory(categoryEntry) };
14875
+ }
14876
+ });
14877
+
14878
+ // src/tools/create-category-group.ts
14879
+ var createCategoryGroup = defineTool({
14880
+ name: "create_category_group",
14881
+ displayName: "Create Category Group",
14882
+ description: "Create a new category group in the active YNAB plan.",
14883
+ summary: "Create a category group",
14884
+ icon: "folder-plus",
14885
+ group: "Categories",
14886
+ input: external_exports.object({
14887
+ name: external_exports.string().min(1).describe("Name of the new category group")
14888
+ }),
14889
+ output: external_exports.object({
14890
+ group: categoryGroupSchema
14891
+ }),
14892
+ handle: async (params) => {
14893
+ const planId = getPlanId();
14894
+ const groupId = crypto.randomUUID();
14895
+ const budget = await syncBudget(planId);
14896
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
14897
+ const groupEntry = {
14898
+ id: groupId,
14899
+ is_tombstone: false,
14900
+ internal_name: "",
14901
+ deletable: true,
14902
+ sortable_index: nextTopSortableIndex(budget.changed_entities?.be_master_categories ?? []),
14903
+ name: params.name,
14904
+ note: "",
14905
+ is_hidden: false
14906
+ };
14907
+ await syncWrite(planId, { be_master_categories: [groupEntry] }, serverKnowledge);
14908
+ return { group: mapCategoryGroup(groupEntry) };
14909
+ }
14910
+ });
14911
+
14641
14912
  // src/tools/create-transaction.ts
14642
14913
  var createTransaction = defineTool({
14643
14914
  name: "create_transaction",
@@ -14733,6 +15004,71 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14733
15004
  }
14734
15005
  });
14735
15006
 
15007
+ // src/tools/delete-category.ts
15008
+ var deleteCategory = defineTool({
15009
+ name: "delete_category",
15010
+ displayName: "Delete Category",
15011
+ description: "Delete a category from the active YNAB plan. This is a soft delete (tombstone). Existing transactions assigned to this category remain in place but the category will no longer appear in budget views.",
15012
+ summary: "Delete a category",
15013
+ icon: "trash-2",
15014
+ group: "Categories",
15015
+ input: external_exports.object({
15016
+ category_id: external_exports.string().min(1).describe("Category ID to delete")
15017
+ }),
15018
+ output: external_exports.object({
15019
+ success: external_exports.boolean()
15020
+ }),
15021
+ handle: async (params) => {
15022
+ const planId = getPlanId();
15023
+ const budget = await syncBudget(planId);
15024
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15025
+ const existing2 = findCategory(budget.changed_entities, params.category_id);
15026
+ assertCategoryDeletable(existing2);
15027
+ await syncWrite(
15028
+ planId,
15029
+ { be_subcategories: [{ ...existing2, is_tombstone: true }] },
15030
+ serverKnowledge
15031
+ );
15032
+ return { success: true };
15033
+ }
15034
+ });
15035
+
15036
+ // src/tools/delete-category-group.ts
15037
+ var deleteCategoryGroup = defineTool({
15038
+ name: "delete_category_group",
15039
+ displayName: "Delete Category Group",
15040
+ description: "Delete a category group and all of its child categories from the active YNAB plan. This is a soft delete (tombstone). Internal/non-deletable groups (Credit Card Payments, Hidden Categories, Internal Master Category) cannot be deleted.",
15041
+ summary: "Delete a category group and its children",
15042
+ icon: "folder-x",
15043
+ group: "Categories",
15044
+ input: external_exports.object({
15045
+ group_id: external_exports.string().min(1).describe("Category group ID to delete")
15046
+ }),
15047
+ output: external_exports.object({
15048
+ success: external_exports.boolean(),
15049
+ deleted_category_count: external_exports.number().describe("Number of child categories that were also tombstoned")
15050
+ }),
15051
+ handle: async (params) => {
15052
+ const planId = getPlanId();
15053
+ const budget = await syncBudget(planId);
15054
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15055
+ const group = findCategoryGroup(budget.changed_entities, params.group_id);
15056
+ assertCategoryGroupDeletable(group);
15057
+ const childCategories = (budget.changed_entities?.be_subcategories ?? []).filter(
15058
+ (c) => c.entities_master_category_id === params.group_id && notTombstone(c)
15059
+ );
15060
+ await syncWrite(
15061
+ planId,
15062
+ {
15063
+ be_master_categories: [{ ...group, is_tombstone: true }],
15064
+ be_subcategories: childCategories.map((c) => ({ ...c, is_tombstone: true }))
15065
+ },
15066
+ serverKnowledge
15067
+ );
15068
+ return { success: true, deleted_category_count: childCategories.length };
15069
+ }
15070
+ });
15071
+
14736
15072
  // src/tools/delete-transaction.ts
14737
15073
  var deleteTransaction = defineTool({
14738
15074
  name: "delete_transaction",
@@ -15138,17 +15474,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15138
15474
  const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
15139
15475
  const budget = await syncBudget(planId);
15140
15476
  const serverKnowledge = budget.current_server_knowledge ?? 0;
15141
- const subcategories = budget.changed_entities?.be_subcategories ?? [];
15142
15477
  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;
15478
+ const fromCategoryId = params.from_category_id;
15479
+ const toCategoryId = params.to_category_id;
15480
+ const fromCategory = fromCategoryId ? findCategory(budget.changed_entities, fromCategoryId) : null;
15481
+ const toCategory = toCategoryId ? findCategory(budget.changed_entities, toCategoryId) : null;
15482
+ const fromBudgetId = fromCategoryId ? formatSubcategoryBudgetId(monthKey, fromCategoryId) : null;
15483
+ const toBudgetId = toCategoryId ? formatSubcategoryBudgetId(monthKey, toCategoryId) : null;
15152
15484
  const buildEntry = (categoryId, budgetId, signedDelta) => {
15153
15485
  const current = existingBudgets.find((b) => b.id === budgetId && notTombstone(b))?.budgeted ?? 0;
15154
15486
  return {
@@ -15160,9 +15492,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15160
15492
  };
15161
15493
  };
15162
15494
  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;
15495
+ if (fromCategoryId && fromBudgetId) budgetEntries.push(buildEntry(fromCategoryId, fromBudgetId, -milliunits));
15496
+ if (toCategoryId && toBudgetId) budgetEntries.push(buildEntry(toCategoryId, toBudgetId, milliunits));
15497
+ const source = fromCategoryId && toCategoryId ? MONEY_MOVEMENT_SOURCE.MOVEMENT : MONEY_MOVEMENT_SOURCE.ASSIGN;
15166
15498
  const result = await syncWrite(
15167
15499
  planId,
15168
15500
  {
@@ -15198,6 +15530,96 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15198
15530
  }
15199
15531
  });
15200
15532
 
15533
+ // src/tools/snooze-category-goal.ts
15534
+ var snoozeCategoryGoal = defineTool({
15535
+ name: "snooze_category_goal",
15536
+ displayName: "Snooze Category Goal",
15537
+ description: "Snooze a category goal for a specific month so it does not appear as needing funding for that month. Pass snooze=false to un-snooze.",
15538
+ summary: "Snooze a category goal for a month",
15539
+ icon: "bell-off",
15540
+ group: "Categories",
15541
+ input: external_exports.object({
15542
+ category_id: external_exports.string().min(1).describe("Category ID whose goal to snooze"),
15543
+ 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-04)"),
15544
+ snooze: external_exports.boolean().optional().describe("true to snooze (default), false to un-snooze")
15545
+ }),
15546
+ output: external_exports.object({
15547
+ success: external_exports.boolean(),
15548
+ snoozed_at: external_exports.string().nullable().describe("ISO timestamp the goal was snoozed at, or null if un-snoozed")
15549
+ }),
15550
+ handle: async (params) => {
15551
+ const planId = getPlanId();
15552
+ const monthKey = toMonthKey(params.month);
15553
+ const budgetId = formatSubcategoryBudgetId(monthKey, params.category_id);
15554
+ const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
15555
+ const shouldSnooze = params.snooze ?? true;
15556
+ const budget = await syncBudget(planId);
15557
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15558
+ const category = findCategory(budget.changed_entities, params.category_id);
15559
+ if (!category.goal_type) {
15560
+ throw ToolError.validation(`Category "${category.name}" has no goal to snooze.`);
15561
+ }
15562
+ const existing2 = (budget.changed_entities?.be_monthly_subcategory_budgets ?? []).find(
15563
+ (b) => b.id === budgetId && notTombstone(b)
15564
+ );
15565
+ const snoozedAt = shouldSnooze ? (/* @__PURE__ */ new Date()).toISOString() : null;
15566
+ const budgetEntry = {
15567
+ ...existing2 ?? {},
15568
+ id: budgetId,
15569
+ is_tombstone: false,
15570
+ entities_monthly_budget_id: monthlyBudgetId,
15571
+ entities_subcategory_id: params.category_id,
15572
+ budgeted: existing2?.budgeted ?? 0,
15573
+ goal_snoozed_at: snoozedAt
15574
+ };
15575
+ await syncWrite(planId, { be_monthly_subcategory_budgets: [budgetEntry] }, serverKnowledge);
15576
+ return { success: true, snoozed_at: snoozedAt };
15577
+ }
15578
+ });
15579
+
15580
+ // src/tools/update-category.ts
15581
+ var updateCategory = defineTool({
15582
+ name: "update_category",
15583
+ displayName: "Update Category",
15584
+ description: 'Rename a category, change its group, set/clear its goal, or hide/unhide it. Only specified fields change; omitted fields remain unchanged. Goal types: "set_aside" / "refill" (recurring NEED with optional cadence), "target_balance" (have $X), "target_by_date" (have $X by date), "debt" (recurring debt payment), or "none" to clear an existing goal.',
15585
+ summary: "Update a category",
15586
+ icon: "pencil",
15587
+ group: "Categories",
15588
+ input: external_exports.object({
15589
+ category_id: external_exports.string().min(1).describe("Category ID to update"),
15590
+ name: external_exports.string().min(1).optional().describe("New name"),
15591
+ group_id: external_exports.string().min(1).optional().describe("New parent category group ID (to move the category)"),
15592
+ goal: goalSpecSchema.optional().describe('New goal definition. Pass { type: "none" } to clear the goal.'),
15593
+ hidden: external_exports.boolean().optional().describe("Hide or unhide the category"),
15594
+ note: external_exports.string().optional().describe("New note (pass empty string to clear)")
15595
+ }),
15596
+ output: external_exports.object({
15597
+ category: categorySchema
15598
+ }),
15599
+ handle: async (params) => {
15600
+ const planId = getPlanId();
15601
+ const budget = await syncBudget(planId);
15602
+ const serverKnowledge = budget.current_server_knowledge ?? 0;
15603
+ const existing2 = findCategory(budget.changed_entities, params.category_id);
15604
+ if (params.group_id) {
15605
+ assertCategoryGroupDeletable(findCategoryGroup(budget.changed_entities, params.group_id));
15606
+ }
15607
+ if (params.goal?.type === "debt" && existing2.entities_account_id == null) {
15608
+ throw ToolError.validation("Debt goals can only be set on debt-account categories.");
15609
+ }
15610
+ const updated = {
15611
+ ...existing2,
15612
+ name: params.name ?? existing2.name,
15613
+ entities_master_category_id: params.group_id ?? existing2.entities_master_category_id,
15614
+ is_hidden: params.hidden ?? existing2.is_hidden,
15615
+ note: params.note ?? existing2.note,
15616
+ ...params.goal !== void 0 ? buildGoalFields(params.goal) : {}
15617
+ };
15618
+ await syncWrite(planId, { be_subcategories: [updated] }, serverKnowledge);
15619
+ return { category: mapCategory(updated) };
15620
+ }
15621
+ });
15622
+
15201
15623
  // src/tools/update-category-budget.ts
15202
15624
  var updateCategoryBudget = defineTool({
15203
15625
  name: "update_category_budget",
@@ -15223,12 +15645,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15223
15645
  const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
15224
15646
  const budget = await syncBudget(planId);
15225
15647
  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
- }
15648
+ const category = findCategory(budget.changed_entities, params.category_id);
15232
15649
  const existingBudget = (budget.changed_entities?.be_monthly_subcategory_budgets ?? []).find(
15233
15650
  (b) => b.id === budgetId && notTombstone(b)
15234
15651
  );
@@ -15394,8 +15811,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15394
15811
  getAccount,
15395
15812
  // Categories
15396
15813
  listCategories,
15814
+ createCategory,
15815
+ updateCategory,
15816
+ deleteCategory,
15817
+ createCategoryGroup,
15818
+ deleteCategoryGroup,
15397
15819
  updateCategoryBudget,
15398
15820
  moveCategoryBudget,
15821
+ snoozeCategoryGoal,
15399
15822
  // Payees
15400
15823
  listPayees,
15401
15824
  // Transactions
@@ -15416,7 +15839,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15416
15839
  };
15417
15840
  var src_default = new YnabPlugin();
15418
15841
 
15419
- // dist/_adapter_entry_fb321989-7b03-4647-9b5e-64822784cb35.ts
15842
+ // dist/_adapter_entry_0fc84288-9dca-44bb-92da-639aeea53a88.ts
15420
15843
  if (!globalThis.__openTabs) {
15421
15844
  globalThis.__openTabs = {};
15422
15845
  } else {
@@ -15632,5 +16055,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
15632
16055
  };
15633
16056
  delete src_default.onDeactivate;
15634
16057
  }
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});}})();
16058
+ })();(function(){var o=(globalThis).__openTabs;if(o&&o.adapters&&o.adapters["ynab"]){var a=o.adapters["ynab"];a.__adapterHash="3a2d11d4883f6b89713b9ed3995d0c919e24e6ae5699ae84dc071e130223def6";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});}})();
15636
16059
  //# sourceMappingURL=adapter.iife.js.map