@stephendolan/ynab-cli 1.2.5 → 1.2.6

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.
package/dist/cli.js CHANGED
@@ -3,9 +3,6 @@
3
3
  // src/cli.ts
4
4
  import { Command as Command11 } from "commander";
5
5
 
6
- // src/lib/utils.ts
7
- import { format, parseISO } from "date-fns";
8
-
9
6
  // src/lib/errors.ts
10
7
  var YnabCliError = class extends Error {
11
8
  constructor(message, statusCode) {
@@ -162,7 +159,10 @@ function parseStatusFilter(value) {
162
159
  const validStatuses = ["cleared", "uncleared", "reconciled"];
163
160
  for (const status of statuses) {
164
161
  if (!validStatuses.includes(status)) {
165
- throw new YnabCliError(`Invalid status '${status}'. Must be one of: ${validStatuses.join(", ")}`, 400);
162
+ throw new YnabCliError(
163
+ `Invalid status '${status}'. Must be one of: ${validStatuses.join(", ")}`,
164
+ 400
165
+ );
166
166
  }
167
167
  }
168
168
  return statuses;
@@ -196,11 +196,11 @@ function applyFieldSelection(items, fields) {
196
196
  return items.map((item) => {
197
197
  const filtered = {};
198
198
  const itemRecord = item;
199
- fieldList.forEach((field) => {
199
+ for (const field of fieldList) {
200
200
  if (field in itemRecord) {
201
201
  filtered[field] = itemRecord[field];
202
202
  }
203
- });
203
+ }
204
204
  return filtered;
205
205
  });
206
206
  }
@@ -475,14 +475,6 @@ var YnabClient = class {
475
475
  return response.data.account;
476
476
  });
477
477
  }
478
- async createAccount(accountData, budgetId) {
479
- return this.withErrorHandling(async () => {
480
- const api = await this.getApi();
481
- const id = await this.getBudgetId(budgetId);
482
- const response = await api.accounts.createAccount(id, accountData);
483
- return response.data.account;
484
- });
485
- }
486
478
  async getCategories(budgetId, lastKnowledgeOfServer) {
487
479
  return this.withErrorHandling(async () => {
488
480
  const api = await this.getApi();
@@ -502,22 +494,6 @@ var YnabClient = class {
502
494
  return response.data.category;
503
495
  });
504
496
  }
505
- async updateCategory(categoryId, data, budgetId) {
506
- return this.withErrorHandling(async () => {
507
- const api = await this.getApi();
508
- const id = await this.getBudgetId(budgetId);
509
- const response = await api.categories.updateCategory(id, categoryId, data);
510
- return response.data.category;
511
- });
512
- }
513
- async getMonthCategory(month, categoryId, budgetId) {
514
- return this.withErrorHandling(async () => {
515
- const api = await this.getApi();
516
- const id = await this.getBudgetId(budgetId);
517
- const response = await api.categories.getMonthCategoryById(id, month, categoryId);
518
- return response.data.category;
519
- });
520
- }
521
497
  async updateMonthCategory(month, categoryId, data, budgetId) {
522
498
  return this.withErrorHandling(async () => {
523
499
  const api = await this.getApi();
@@ -553,22 +529,6 @@ var YnabClient = class {
553
529
  return response.data.payee;
554
530
  });
555
531
  }
556
- async getPayeeLocations(budgetId) {
557
- return this.withErrorHandling(async () => {
558
- const api = await this.getApi();
559
- const id = await this.getBudgetId(budgetId);
560
- const response = await api.payeeLocations.getPayeeLocations(id);
561
- return response.data.payee_locations;
562
- });
563
- }
564
- async getPayeeLocation(payeeLocationId, budgetId) {
565
- return this.withErrorHandling(async () => {
566
- const api = await this.getApi();
567
- const id = await this.getBudgetId(budgetId);
568
- const response = await api.payeeLocations.getPayeeLocationById(id, payeeLocationId);
569
- return response.data.payee_location;
570
- });
571
- }
572
532
  async getPayeeLocationsByPayee(payeeId, budgetId) {
573
533
  return this.withErrorHandling(async () => {
574
534
  const api = await this.getApi();
@@ -679,34 +639,14 @@ var YnabClient = class {
679
639
  return response.data.transaction;
680
640
  });
681
641
  }
682
- async createTransactions(transactionsData, budgetId) {
683
- return this.withErrorHandling(async () => {
684
- const api = await this.getApi();
685
- const id = await this.getBudgetId(budgetId);
686
- const response = await api.transactions.createTransactions(id, transactionsData);
687
- return response.data;
688
- });
689
- }
690
642
  async updateTransaction(transactionId, transactionData, budgetId) {
691
643
  return this.withErrorHandling(async () => {
692
644
  const api = await this.getApi();
693
645
  const id = await this.getBudgetId(budgetId);
694
- const response = await api.transactions.updateTransaction(
695
- id,
696
- transactionId,
697
- transactionData
698
- );
646
+ const response = await api.transactions.updateTransaction(id, transactionId, transactionData);
699
647
  return response.data.transaction;
700
648
  });
701
649
  }
702
- async updateTransactions(transactionsData, budgetId) {
703
- return this.withErrorHandling(async () => {
704
- const api = await this.getApi();
705
- const id = await this.getBudgetId(budgetId);
706
- const response = await api.transactions.updateTransactions(id, transactionsData);
707
- return response.data;
708
- });
709
- }
710
650
  async deleteTransaction(transactionId, budgetId) {
711
651
  return this.withErrorHandling(async () => {
712
652
  const api = await this.getApi();
@@ -748,26 +688,6 @@ var YnabClient = class {
748
688
  return response.data.scheduled_transaction;
749
689
  });
750
690
  }
751
- async createScheduledTransaction(data, budgetId) {
752
- return this.withErrorHandling(async () => {
753
- const api = await this.getApi();
754
- const id = await this.getBudgetId(budgetId);
755
- const response = await api.scheduledTransactions.createScheduledTransaction(id, data);
756
- return response.data.scheduled_transaction;
757
- });
758
- }
759
- async updateScheduledTransaction(scheduledTransactionId, data, budgetId) {
760
- return this.withErrorHandling(async () => {
761
- const api = await this.getApi();
762
- const id = await this.getBudgetId(budgetId);
763
- const response = await api.scheduledTransactions.updateScheduledTransaction(
764
- id,
765
- scheduledTransactionId,
766
- data
767
- );
768
- return response.data.scheduled_transaction;
769
- });
770
- }
771
691
  async deleteScheduledTransaction(scheduledTransactionId, budgetId) {
772
692
  return this.withErrorHandling(async () => {
773
693
  const api = await this.getApi();
@@ -846,40 +766,46 @@ function buildUpdateObject(options, mapping) {
846
766
  // src/commands/auth.ts
847
767
  function createAuthCommand() {
848
768
  const cmd = new Command("auth").description("Authentication management");
849
- cmd.command("login").description("Configure access token").action(withErrorHandling(async () => {
850
- const token = await promptForAccessToken();
851
- await auth.setAccessToken(token);
852
- client.clearApi();
853
- try {
854
- const user = await client.getUser();
855
- outputJson({
856
- message: "Successfully authenticated",
857
- user: { id: user?.id }
858
- });
859
- } catch (error) {
860
- await auth.deleteAccessToken();
769
+ cmd.command("login").description("Configure access token").action(
770
+ withErrorHandling(async () => {
771
+ const token = await promptForAccessToken();
772
+ await auth.setAccessToken(token);
861
773
  client.clearApi();
862
- throw error;
863
- }
864
- }));
865
- cmd.command("status").description("Check authentication status").action(withErrorHandling(async () => {
866
- const isAuthenticated = await auth.isAuthenticated();
867
- if (!isAuthenticated) {
868
- outputJson({ authenticated: false, message: "Not authenticated" });
869
- return;
870
- }
871
- try {
872
- const user = await client.getUser();
873
- outputJson({ authenticated: true, user: { id: user?.id } });
874
- } catch (error) {
875
- outputJson({ authenticated: false, message: "Token exists but is invalid" });
876
- }
877
- }));
878
- cmd.command("logout").description("Remove stored credentials").action(withErrorHandling(async () => {
879
- await auth.logout();
880
- client.clearApi();
881
- outputJson({ message: "Successfully logged out" });
882
- }));
774
+ try {
775
+ const user = await client.getUser();
776
+ outputJson({
777
+ message: "Successfully authenticated",
778
+ user: { id: user?.id }
779
+ });
780
+ } catch (error) {
781
+ await auth.deleteAccessToken();
782
+ client.clearApi();
783
+ throw error;
784
+ }
785
+ })
786
+ );
787
+ cmd.command("status").description("Check authentication status").action(
788
+ withErrorHandling(async () => {
789
+ const isAuthenticated = await auth.isAuthenticated();
790
+ if (!isAuthenticated) {
791
+ outputJson({ authenticated: false, message: "Not authenticated" });
792
+ return;
793
+ }
794
+ try {
795
+ const user = await client.getUser();
796
+ outputJson({ authenticated: true, user: { id: user?.id } });
797
+ } catch {
798
+ outputJson({ authenticated: false, message: "Token exists but is invalid" });
799
+ }
800
+ })
801
+ );
802
+ cmd.command("logout").description("Remove stored credentials").action(
803
+ withErrorHandling(async () => {
804
+ await auth.logout();
805
+ client.clearApi();
806
+ outputJson({ message: "Successfully logged out" });
807
+ })
808
+ );
883
809
  return cmd;
884
810
  }
885
811
 
@@ -887,10 +813,12 @@ function createAuthCommand() {
887
813
  import { Command as Command2 } from "commander";
888
814
  function createUserCommand() {
889
815
  const cmd = new Command2("user").description("User information");
890
- cmd.command("info").description("Get authenticated user information").action(withErrorHandling(async () => {
891
- const user = await client.getUser();
892
- outputJson(user);
893
- }));
816
+ cmd.command("info").description("Get authenticated user information").action(
817
+ withErrorHandling(async () => {
818
+ const user = await client.getUser();
819
+ outputJson(user);
820
+ })
821
+ );
894
822
  return cmd;
895
823
  }
896
824
 
@@ -898,30 +826,38 @@ function createUserCommand() {
898
826
  import { Command as Command3 } from "commander";
899
827
  function createBudgetsCommand() {
900
828
  const cmd = new Command3("budgets").description("Budget operations");
901
- cmd.command("list").description("List all budgets").option("--include-accounts", "Include accounts in response").action(withErrorHandling(async (options) => {
902
- const result = await client.getBudgets(options.includeAccounts);
903
- outputJson(result?.budgets);
904
- }));
905
- cmd.command("view").description("View budget details (uses default if no id provided)").argument("[id]", "Budget ID").action(withErrorHandling(async (id) => {
906
- const result = await client.getBudget(id);
907
- outputJson(result?.budget);
908
- }));
909
- cmd.command("settings").description("View budget settings").argument("[id]", "Budget ID").action(withErrorHandling(async (id) => {
910
- const settings = await client.getBudgetSettings(id);
911
- outputJson(settings);
912
- }));
913
- cmd.command("set-default").description("Set default budget for commands").argument("<id>", "Budget ID").action(withErrorHandling(async (id) => {
914
- const result = await client.getBudgets();
915
- const budget = result?.budgets.find((b) => b.id === id);
916
- if (!budget) {
917
- throw new YnabCliError(`Budget with ID ${id} not found`, 404);
918
- }
919
- config.setDefaultBudget(id);
920
- outputJson({
921
- message: "Default budget set",
922
- budget: { id: budget.id, name: budget.name }
923
- });
924
- }));
829
+ cmd.command("list").description("List all budgets").option("--include-accounts", "Include accounts in response").action(
830
+ withErrorHandling(async (options) => {
831
+ const result = await client.getBudgets(options.includeAccounts);
832
+ outputJson(result?.budgets);
833
+ })
834
+ );
835
+ cmd.command("view").description("View budget details (uses default if no id provided)").argument("[id]", "Budget ID").action(
836
+ withErrorHandling(async (id) => {
837
+ const result = await client.getBudget(id);
838
+ outputJson(result?.budget);
839
+ })
840
+ );
841
+ cmd.command("settings").description("View budget settings").argument("[id]", "Budget ID").action(
842
+ withErrorHandling(async (id) => {
843
+ const settings = await client.getBudgetSettings(id);
844
+ outputJson(settings);
845
+ })
846
+ );
847
+ cmd.command("set-default").description("Set default budget for commands").argument("<id>", "Budget ID").action(
848
+ withErrorHandling(async (id) => {
849
+ const result = await client.getBudgets();
850
+ const budget = result?.budgets.find((b) => b.id === id);
851
+ if (!budget) {
852
+ throw new YnabCliError(`Budget with ID ${id} not found`, 404);
853
+ }
854
+ config.setDefaultBudget(id);
855
+ outputJson({
856
+ message: "Default budget set",
857
+ budget: { id: budget.id, name: budget.name }
858
+ });
859
+ })
860
+ );
925
861
  return cmd;
926
862
  }
927
863
 
@@ -929,22 +865,30 @@ function createBudgetsCommand() {
929
865
  import { Command as Command4 } from "commander";
930
866
  function createAccountsCommand() {
931
867
  const cmd = new Command4("accounts").description("Account operations");
932
- cmd.command("list").description("List all accounts").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (options) => {
933
- const result = await client.getAccounts(options.budget);
934
- outputJson(result?.accounts);
935
- }));
936
- cmd.command("view").description("View account details").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
937
- const account = await client.getAccount(id, options.budget);
938
- outputJson(account);
939
- }));
940
- cmd.command("transactions").description("List transactions for account").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").action(withErrorHandling(async (id, options) => {
941
- const result = await client.getTransactionsByAccount(id, {
942
- budgetId: options.budget,
943
- sinceDate: options.since,
944
- type: options.type
945
- });
946
- outputJson(result?.transactions);
947
- }));
868
+ cmd.command("list").description("List all accounts").option("-b, --budget <id>", "Budget ID").action(
869
+ withErrorHandling(async (options) => {
870
+ const result = await client.getAccounts(options.budget);
871
+ outputJson(result?.accounts);
872
+ })
873
+ );
874
+ cmd.command("view").description("View account details").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").action(
875
+ withErrorHandling(async (id, options) => {
876
+ const account = await client.getAccount(id, options.budget);
877
+ outputJson(account);
878
+ })
879
+ );
880
+ cmd.command("transactions").description("List transactions for account").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").action(
881
+ withErrorHandling(
882
+ async (id, options) => {
883
+ const result = await client.getTransactionsByAccount(id, {
884
+ budgetId: options.budget,
885
+ sinceDate: options.since,
886
+ type: options.type
887
+ });
888
+ outputJson(result?.transactions);
889
+ }
890
+ )
891
+ );
948
892
  return cmd;
949
893
  }
950
894
 
@@ -952,36 +896,50 @@ function createAccountsCommand() {
952
896
  import { Command as Command5 } from "commander";
953
897
  function createCategoriesCommand() {
954
898
  const cmd = new Command5("categories").description("Category operations");
955
- cmd.command("list").description("List all categories").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
956
- const result = await client.getCategories(options.budget, options.lastKnowledge);
957
- outputJson(result?.category_groups);
958
- }));
959
- cmd.command("view").description("View category details").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
960
- const category = await client.getCategory(id, options.budget);
961
- outputJson(category);
962
- }));
963
- cmd.command("budget").description("Set category budgeted amount for a month (overrides existing amount)").argument("<id>", "Category ID").requiredOption("--month <month>", "Month in YYYY-MM-DD format (e.g., 2025-07-01)").requiredOption("--amount <amount>", "Total budgeted amount to set (e.g., 100.50)", parseFloat).option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
964
- if (isNaN(options.amount)) {
965
- throw new YnabCliError("Amount must be a valid number", 400);
966
- }
967
- const milliunits = amountToMilliunits(options.amount);
968
- const category = await client.updateMonthCategory(
969
- options.month,
970
- id,
971
- { category: { budgeted: milliunits } },
972
- options.budget
973
- );
974
- outputJson(category);
975
- }));
976
- cmd.command("transactions").description("List transactions for category").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (id, options) => {
977
- const result = await client.getTransactionsByCategory(id, {
978
- budgetId: options.budget,
979
- sinceDate: options.since,
980
- type: options.type,
981
- lastKnowledgeOfServer: options.lastKnowledge
982
- });
983
- outputJson(result?.transactions);
984
- }));
899
+ cmd.command("list").description("List all categories").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
900
+ withErrorHandling(
901
+ async (options) => {
902
+ const result = await client.getCategories(options.budget, options.lastKnowledge);
903
+ outputJson(result?.category_groups);
904
+ }
905
+ )
906
+ );
907
+ cmd.command("view").description("View category details").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").action(
908
+ withErrorHandling(async (id, options) => {
909
+ const category = await client.getCategory(id, options.budget);
910
+ outputJson(category);
911
+ })
912
+ );
913
+ cmd.command("budget").description("Set category budgeted amount for a month (overrides existing amount)").argument("<id>", "Category ID").requiredOption("--month <month>", "Month in YYYY-MM-DD format (e.g., 2025-07-01)").requiredOption("--amount <amount>", "Total budgeted amount to set (e.g., 100.50)", parseFloat).option("-b, --budget <id>", "Budget ID").action(
914
+ withErrorHandling(
915
+ async (id, options) => {
916
+ if (isNaN(options.amount)) {
917
+ throw new YnabCliError("Amount must be a valid number", 400);
918
+ }
919
+ const milliunits = amountToMilliunits(options.amount);
920
+ const category = await client.updateMonthCategory(
921
+ options.month,
922
+ id,
923
+ { category: { budgeted: milliunits } },
924
+ options.budget
925
+ );
926
+ outputJson(category);
927
+ }
928
+ )
929
+ );
930
+ cmd.command("transactions").description("List transactions for category").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
931
+ withErrorHandling(
932
+ async (id, options) => {
933
+ const result = await client.getTransactionsByCategory(id, {
934
+ budgetId: options.budget,
935
+ sinceDate: options.since,
936
+ type: options.type,
937
+ lastKnowledgeOfServer: options.lastKnowledge
938
+ });
939
+ outputJson(result?.transactions);
940
+ }
941
+ )
942
+ );
985
943
  return cmd;
986
944
  }
987
945
 
@@ -1033,161 +991,207 @@ function buildTransactionData(options) {
1033
991
  }
1034
992
  function createTransactionsCommand() {
1035
993
  const cmd = new Command6("transactions").description("Transaction operations");
1036
- cmd.command("list").description("List transactions").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Filter by account ID").option("--category <id>", "Filter by category ID").option("--payee <id>", "Filter by payee ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--until <date>", "Filter transactions until date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--approved <value>", "Filter by approval status: true or false").option("--status <statuses>", "Filter by cleared status: cleared, uncleared, reconciled (comma-separated for multiple)").option("--min-amount <amount>", "Minimum amount in currency units (e.g., 10.50)", parseFloat).option("--max-amount <amount>", "Maximum amount in currency units (e.g., 100.00)", parseFloat).option("--fields <fields>", "Comma-separated list of fields to include (e.g., id,date,amount,memo)").action(withErrorHandling(async (options) => {
1037
- const params = {
1038
- budgetId: options.budget,
1039
- sinceDate: options.since,
1040
- type: options.type
1041
- };
1042
- const result = options.account ? await client.getTransactionsByAccount(options.account, params) : options.category ? await client.getTransactionsByCategory(options.category, params) : options.payee ? await client.getTransactionsByPayee(options.payee, params) : await client.getTransactions(params);
1043
- const transactions = result?.transactions || [];
1044
- const filtered = applyTransactionFilters(transactions, {
1045
- until: options.until,
1046
- approved: options.approved,
1047
- status: options.status,
1048
- minAmount: options.minAmount,
1049
- maxAmount: options.maxAmount
1050
- });
1051
- const selected = applyFieldSelection(filtered, options.fields);
1052
- outputJson(selected);
1053
- }));
1054
- cmd.command("view").description("View single transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1055
- const transaction = await client.getTransaction(id, options.budget);
1056
- outputJson(transaction);
1057
- }));
1058
- cmd.command("create").description("Create transaction").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units (e.g., 10.50)", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status (cleared, uncleared, reconciled)").option("--approved", "Mark as approved").action(withErrorHandling(async (options) => {
1059
- const shouldPrompt = isInteractive() && !options.amount;
1060
- if (shouldPrompt && !options.account) {
1061
- throw new YnabCliError("--account is required. Interactive mode cannot auto-select an account.", 400);
1062
- }
1063
- const transactionData = shouldPrompt ? { ...await promptForTransaction(), account_id: options.account } : buildTransactionData(options);
1064
- const transaction = await client.createTransaction(
1065
- { transaction: transactionData },
1066
- options.budget
1067
- );
1068
- outputJson(transaction);
1069
- }));
1070
- cmd.command("update").description("Update transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status").option("--approved", "Mark as approved").action(withErrorHandling(async (id, options) => {
1071
- const transactionData = buildUpdateObject(options, {
1072
- account: "account_id",
1073
- date: "date",
1074
- payeeName: "payee_name",
1075
- payeeId: "payee_id",
1076
- categoryId: "category_id",
1077
- memo: "memo",
1078
- cleared: "cleared",
1079
- approved: "approved"
1080
- });
1081
- if (options.amount !== void 0) {
1082
- transactionData.amount = amountToMilliunits(options.amount);
1083
- }
1084
- const transaction = await client.updateTransaction(
1085
- id,
1086
- { transaction: transactionData },
1087
- options.budget
1088
- );
1089
- outputJson(transaction);
1090
- }));
1091
- cmd.command("delete").description("Delete transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(withErrorHandling(async (id, options) => {
1092
- if (!await confirmDelete("transaction", options.yes)) {
1093
- return;
1094
- }
1095
- const transaction = await client.deleteTransaction(id, options.budget);
1096
- outputJson({ message: "Transaction deleted", transaction });
1097
- }));
1098
- cmd.command("import").description("Import transactions").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (options) => {
1099
- const transactionIds = await client.importTransactions(options.budget);
1100
- outputJson({ transaction_ids: transactionIds });
1101
- }));
1102
- cmd.command("split").description("Split transaction into multiple categories. Amounts should be in dollars (e.g., 10.50).").argument("<id>", "Transaction ID").requiredOption("--splits <json>", 'JSON array of splits with dollar amounts: [{"amount": -21.40, "category_id": "xxx", "memo": "..."}]').option("-b, --budget <id>", "Budget ID").option("-f, --force", "Force update of already-split transactions by deleting and recreating").action(withErrorHandling(async (id, options) => {
1103
- let parsedSplits;
1104
- try {
1105
- parsedSplits = JSON.parse(options.splits);
1106
- } catch (error) {
1107
- throw new YnabCliError("Invalid JSON in --splits parameter", 400);
1108
- }
1109
- const splits = validateJson(parsedSplits, TransactionSplitSchema, "transaction splits");
1110
- const splitsInMilliunits = splits.map((split) => ({
1111
- ...split,
1112
- amount: amountToMilliunits(split.amount)
1113
- }));
1114
- const existingTransaction = await client.getTransaction(id, options.budget);
1115
- const isAlreadySplit = existingTransaction.subtransactions && existingTransaction.subtransactions.length > 0;
1116
- if (isAlreadySplit && !options.force) {
1117
- throw new YnabCliError(
1118
- "Transaction is already split. YNAB API does not support updating split transactions. Use --force to delete and recreate the transaction with new splits.",
1119
- 400
1120
- );
1121
- }
1122
- if (isAlreadySplit) {
1123
- await client.deleteTransaction(id, options.budget);
1124
- const recreatedTransaction = await client.createTransaction(
1125
- {
1126
- transaction: {
1127
- account_id: existingTransaction.account_id,
1128
- date: existingTransaction.date,
1129
- amount: existingTransaction.amount,
1130
- payee_id: existingTransaction.payee_id,
1131
- payee_name: existingTransaction.payee_name,
1132
- category_id: null,
1133
- memo: existingTransaction.memo,
1134
- cleared: existingTransaction.cleared,
1135
- approved: existingTransaction.approved,
1136
- flag_color: existingTransaction.flag_color,
1137
- subtransactions: splitsInMilliunits
1138
- }
1139
- },
1140
- options.budget
1141
- );
1142
- outputJson(recreatedTransaction);
1143
- } else {
1144
- const transaction = await client.updateTransaction(
1145
- id,
1146
- {
1147
- transaction: {
1148
- category_id: null,
1149
- subtransactions: splitsInMilliunits
1150
- }
1151
- },
1152
- options.budget
1153
- );
994
+ cmd.command("list").description("List transactions").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Filter by account ID").option("--category <id>", "Filter by category ID").option("--payee <id>", "Filter by payee ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--until <date>", "Filter transactions until date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--approved <value>", "Filter by approval status: true or false").option(
995
+ "--status <statuses>",
996
+ "Filter by cleared status: cleared, uncleared, reconciled (comma-separated for multiple)"
997
+ ).option("--min-amount <amount>", "Minimum amount in currency units (e.g., 10.50)", parseFloat).option("--max-amount <amount>", "Maximum amount in currency units (e.g., 100.00)", parseFloat).option(
998
+ "--fields <fields>",
999
+ "Comma-separated list of fields to include (e.g., id,date,amount,memo)"
1000
+ ).action(
1001
+ withErrorHandling(
1002
+ async (options) => {
1003
+ const params = {
1004
+ budgetId: options.budget,
1005
+ sinceDate: options.since,
1006
+ type: options.type
1007
+ };
1008
+ const result = options.account ? await client.getTransactionsByAccount(options.account, params) : options.category ? await client.getTransactionsByCategory(options.category, params) : options.payee ? await client.getTransactionsByPayee(options.payee, params) : await client.getTransactions(params);
1009
+ const transactions = result?.transactions || [];
1010
+ const filtered = applyTransactionFilters(transactions, {
1011
+ until: options.until,
1012
+ approved: options.approved,
1013
+ status: options.status,
1014
+ minAmount: options.minAmount,
1015
+ maxAmount: options.maxAmount
1016
+ });
1017
+ const selected = applyFieldSelection(filtered, options.fields);
1018
+ outputJson(selected);
1019
+ }
1020
+ )
1021
+ );
1022
+ cmd.command("view").description("View single transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").action(
1023
+ withErrorHandling(async (id, options) => {
1024
+ const transaction = await client.getTransaction(id, options.budget);
1154
1025
  outputJson(transaction);
1155
- }
1156
- }));
1157
- cmd.command("search").description("Search transactions").option("-b, --budget <id>", "Budget ID").option("--memo <text>", "Search in memo field").option("--payee-name <name>", "Search in payee name").option("--amount <amount>", "Search for exact amount in currency units", parseFloat).option("--since <date>", "Search transactions since date (YYYY-MM-DD)").option("--until <date>", "Search transactions until date (YYYY-MM-DD)").option("--approved <value>", "Filter by approval status: true or false").option("--status <statuses>", "Filter by cleared status: cleared, uncleared, reconciled (comma-separated)").option("--fields <fields>", "Comma-separated list of fields to include").action(withErrorHandling(async (options) => {
1158
- if (!options.memo && !options.payeeName && options.amount === void 0) {
1159
- throw new YnabCliError("At least one search criteria required: --memo, --payee-name, or --amount", 400);
1160
- }
1161
- const params = {
1162
- budgetId: options.budget,
1163
- sinceDate: options.since
1164
- };
1165
- const result = await client.getTransactions(params);
1166
- let transactions = result?.transactions || [];
1167
- if (options.memo) {
1168
- const searchTerm = options.memo.toLowerCase();
1169
- transactions = transactions.filter(
1170
- (t) => t.memo?.toLowerCase().includes(searchTerm)
1171
- );
1172
- }
1173
- if (options.payeeName) {
1174
- const searchTerm = options.payeeName.toLowerCase();
1175
- transactions = transactions.filter(
1176
- (t) => t.payee_name?.toLowerCase().includes(searchTerm)
1177
- );
1178
- }
1179
- if (options.amount !== void 0) {
1180
- const amountMilliunits = amountToMilliunits(options.amount);
1181
- transactions = transactions.filter((t) => t.amount === amountMilliunits);
1182
- }
1183
- transactions = applyTransactionFilters(transactions, {
1184
- until: options.until,
1185
- approved: options.approved,
1186
- status: options.status
1187
- });
1188
- const filteredTransactions = applyFieldSelection(transactions, options.fields);
1189
- outputJson(filteredTransactions);
1190
- }));
1026
+ })
1027
+ );
1028
+ cmd.command("create").description("Create transaction").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units (e.g., 10.50)", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status (cleared, uncleared, reconciled)").option("--approved", "Mark as approved").action(
1029
+ withErrorHandling(
1030
+ async (options) => {
1031
+ const shouldPrompt = isInteractive() && !options.amount;
1032
+ if (shouldPrompt && !options.account) {
1033
+ throw new YnabCliError(
1034
+ "--account is required. Interactive mode cannot auto-select an account.",
1035
+ 400
1036
+ );
1037
+ }
1038
+ const transactionData = shouldPrompt ? { ...await promptForTransaction(), account_id: options.account } : buildTransactionData(options);
1039
+ const transaction = await client.createTransaction(
1040
+ { transaction: transactionData },
1041
+ options.budget
1042
+ );
1043
+ outputJson(transaction);
1044
+ }
1045
+ )
1046
+ );
1047
+ cmd.command("update").description("Update transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status").option("--approved", "Mark as approved").action(
1048
+ withErrorHandling(
1049
+ async (id, options) => {
1050
+ const transactionData = buildUpdateObject(options, {
1051
+ account: "account_id",
1052
+ date: "date",
1053
+ payeeName: "payee_name",
1054
+ payeeId: "payee_id",
1055
+ categoryId: "category_id",
1056
+ memo: "memo",
1057
+ cleared: "cleared",
1058
+ approved: "approved"
1059
+ });
1060
+ if (options.amount !== void 0) {
1061
+ transactionData.amount = amountToMilliunits(options.amount);
1062
+ }
1063
+ const transaction = await client.updateTransaction(
1064
+ id,
1065
+ { transaction: transactionData },
1066
+ options.budget
1067
+ );
1068
+ outputJson(transaction);
1069
+ }
1070
+ )
1071
+ );
1072
+ cmd.command("delete").description("Delete transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(
1073
+ withErrorHandling(
1074
+ async (id, options) => {
1075
+ if (!await confirmDelete("transaction", options.yes)) {
1076
+ return;
1077
+ }
1078
+ const transaction = await client.deleteTransaction(id, options.budget);
1079
+ outputJson({ message: "Transaction deleted", transaction });
1080
+ }
1081
+ )
1082
+ );
1083
+ cmd.command("import").description("Import transactions").option("-b, --budget <id>", "Budget ID").action(
1084
+ withErrorHandling(async (options) => {
1085
+ const transactionIds = await client.importTransactions(options.budget);
1086
+ outputJson({ transaction_ids: transactionIds });
1087
+ })
1088
+ );
1089
+ cmd.command("split").description(
1090
+ "Split transaction into multiple categories. Amounts should be in dollars (e.g., 10.50)."
1091
+ ).argument("<id>", "Transaction ID").requiredOption(
1092
+ "--splits <json>",
1093
+ 'JSON array of splits with dollar amounts: [{"amount": -21.40, "category_id": "xxx", "memo": "..."}]'
1094
+ ).option("-b, --budget <id>", "Budget ID").option("-f, --force", "Force update of already-split transactions by deleting and recreating").action(
1095
+ withErrorHandling(
1096
+ async (id, options) => {
1097
+ let parsedSplits;
1098
+ try {
1099
+ parsedSplits = JSON.parse(options.splits);
1100
+ } catch {
1101
+ throw new YnabCliError("Invalid JSON in --splits parameter", 400);
1102
+ }
1103
+ const splits = validateJson(parsedSplits, TransactionSplitSchema, "transaction splits");
1104
+ const splitsInMilliunits = splits.map((split) => ({
1105
+ ...split,
1106
+ amount: amountToMilliunits(split.amount)
1107
+ }));
1108
+ const existingTransaction = await client.getTransaction(id, options.budget);
1109
+ const isAlreadySplit = existingTransaction.subtransactions && existingTransaction.subtransactions.length > 0;
1110
+ if (isAlreadySplit && !options.force) {
1111
+ throw new YnabCliError(
1112
+ "Transaction is already split. YNAB API does not support updating split transactions. Use --force to delete and recreate the transaction with new splits.",
1113
+ 400
1114
+ );
1115
+ }
1116
+ if (isAlreadySplit) {
1117
+ await client.deleteTransaction(id, options.budget);
1118
+ const recreatedTransaction = await client.createTransaction(
1119
+ {
1120
+ transaction: {
1121
+ account_id: existingTransaction.account_id,
1122
+ date: existingTransaction.date,
1123
+ amount: existingTransaction.amount,
1124
+ payee_id: existingTransaction.payee_id,
1125
+ payee_name: existingTransaction.payee_name,
1126
+ category_id: null,
1127
+ memo: existingTransaction.memo,
1128
+ cleared: existingTransaction.cleared,
1129
+ approved: existingTransaction.approved,
1130
+ flag_color: existingTransaction.flag_color,
1131
+ subtransactions: splitsInMilliunits
1132
+ }
1133
+ },
1134
+ options.budget
1135
+ );
1136
+ outputJson(recreatedTransaction);
1137
+ } else {
1138
+ const transaction = await client.updateTransaction(
1139
+ id,
1140
+ {
1141
+ transaction: {
1142
+ category_id: null,
1143
+ subtransactions: splitsInMilliunits
1144
+ }
1145
+ },
1146
+ options.budget
1147
+ );
1148
+ outputJson(transaction);
1149
+ }
1150
+ }
1151
+ )
1152
+ );
1153
+ cmd.command("search").description("Search transactions").option("-b, --budget <id>", "Budget ID").option("--memo <text>", "Search in memo field").option("--payee-name <name>", "Search in payee name").option("--amount <amount>", "Search for exact amount in currency units", parseFloat).option("--since <date>", "Search transactions since date (YYYY-MM-DD)").option("--until <date>", "Search transactions until date (YYYY-MM-DD)").option("--approved <value>", "Filter by approval status: true or false").option(
1154
+ "--status <statuses>",
1155
+ "Filter by cleared status: cleared, uncleared, reconciled (comma-separated)"
1156
+ ).option("--fields <fields>", "Comma-separated list of fields to include").action(
1157
+ withErrorHandling(
1158
+ async (options) => {
1159
+ if (!options.memo && !options.payeeName && options.amount === void 0) {
1160
+ throw new YnabCliError(
1161
+ "At least one search criteria required: --memo, --payee-name, or --amount",
1162
+ 400
1163
+ );
1164
+ }
1165
+ const params = {
1166
+ budgetId: options.budget,
1167
+ sinceDate: options.since
1168
+ };
1169
+ const result = await client.getTransactions(params);
1170
+ let transactions = result?.transactions || [];
1171
+ if (options.memo) {
1172
+ const searchTerm = options.memo.toLowerCase();
1173
+ transactions = transactions.filter((t) => t.memo?.toLowerCase().includes(searchTerm));
1174
+ }
1175
+ if (options.payeeName) {
1176
+ const searchTerm = options.payeeName.toLowerCase();
1177
+ transactions = transactions.filter(
1178
+ (t) => t.payee_name?.toLowerCase().includes(searchTerm)
1179
+ );
1180
+ }
1181
+ if (options.amount !== void 0) {
1182
+ const amountMilliunits = amountToMilliunits(options.amount);
1183
+ transactions = transactions.filter((t) => t.amount === amountMilliunits);
1184
+ }
1185
+ transactions = applyTransactionFilters(transactions, {
1186
+ until: options.until,
1187
+ approved: options.approved,
1188
+ status: options.status
1189
+ });
1190
+ const filteredTransactions = applyFieldSelection(transactions, options.fields);
1191
+ outputJson(filteredTransactions);
1192
+ }
1193
+ )
1194
+ );
1191
1195
  return cmd;
1192
1196
  }
1193
1197
 
@@ -1195,24 +1199,37 @@ function createTransactionsCommand() {
1195
1199
  import { Command as Command7 } from "commander";
1196
1200
  function createScheduledCommand() {
1197
1201
  const cmd = new Command7("scheduled").description("Scheduled transaction operations");
1198
- cmd.command("list").description("List all scheduled transactions").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
1199
- const result = await client.getScheduledTransactions(options.budget, options.lastKnowledge);
1200
- outputJson(result?.scheduled_transactions);
1201
- }));
1202
- cmd.command("view").description("View scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1203
- const scheduledTransaction = await client.getScheduledTransaction(id, options.budget);
1204
- outputJson(scheduledTransaction);
1205
- }));
1206
- cmd.command("delete").description("Delete scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(withErrorHandling(async (id, options) => {
1207
- if (!await confirmDelete("scheduled transaction", options.yes)) {
1208
- return;
1209
- }
1210
- const scheduledTransaction = await client.deleteScheduledTransaction(id, options.budget);
1211
- outputJson({
1212
- message: "Scheduled transaction deleted",
1213
- scheduled_transaction: scheduledTransaction
1214
- });
1215
- }));
1202
+ cmd.command("list").description("List all scheduled transactions").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
1203
+ withErrorHandling(
1204
+ async (options) => {
1205
+ const result = await client.getScheduledTransactions(
1206
+ options.budget,
1207
+ options.lastKnowledge
1208
+ );
1209
+ outputJson(result?.scheduled_transactions);
1210
+ }
1211
+ )
1212
+ );
1213
+ cmd.command("view").description("View scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").action(
1214
+ withErrorHandling(async (id, options) => {
1215
+ const scheduledTransaction = await client.getScheduledTransaction(id, options.budget);
1216
+ outputJson(scheduledTransaction);
1217
+ })
1218
+ );
1219
+ cmd.command("delete").description("Delete scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(
1220
+ withErrorHandling(
1221
+ async (id, options) => {
1222
+ if (!await confirmDelete("scheduled transaction", options.yes)) {
1223
+ return;
1224
+ }
1225
+ const scheduledTransaction = await client.deleteScheduledTransaction(id, options.budget);
1226
+ outputJson({
1227
+ message: "Scheduled transaction deleted",
1228
+ scheduled_transaction: scheduledTransaction
1229
+ });
1230
+ }
1231
+ )
1232
+ );
1216
1233
  return cmd;
1217
1234
  }
1218
1235
 
@@ -1220,38 +1237,54 @@ function createScheduledCommand() {
1220
1237
  import { Command as Command8 } from "commander";
1221
1238
  function createPayeesCommand() {
1222
1239
  const cmd = new Command8("payees").description("Payee operations");
1223
- cmd.command("list").description("List all payees").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
1224
- const result = await client.getPayees(options.budget, options.lastKnowledge);
1225
- outputJson(result?.payees);
1226
- }));
1227
- cmd.command("view").description("View payee details").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1228
- const payee = await client.getPayee(id, options.budget);
1229
- outputJson(payee);
1230
- }));
1231
- cmd.command("update").description("Rename payee").argument("<id>", "Payee ID").requiredOption("--name <name>", "New payee name").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1232
- if (!options.name?.trim()) {
1233
- throw new YnabCliError("Name cannot be empty", 400);
1234
- }
1235
- const payee = await client.updatePayee(
1236
- id,
1237
- { payee: { name: options.name } },
1238
- options.budget
1239
- );
1240
- outputJson(payee);
1241
- }));
1242
- cmd.command("locations").description("List locations for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1243
- const locations = await client.getPayeeLocationsByPayee(id, options.budget);
1244
- outputJson(locations);
1245
- }));
1246
- cmd.command("transactions").description("List transactions for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (id, options) => {
1247
- const result = await client.getTransactionsByPayee(id, {
1248
- budgetId: options.budget,
1249
- sinceDate: options.since,
1250
- type: options.type,
1251
- lastKnowledgeOfServer: options.lastKnowledge
1252
- });
1253
- outputJson(result?.transactions);
1254
- }));
1240
+ cmd.command("list").description("List all payees").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
1241
+ withErrorHandling(
1242
+ async (options) => {
1243
+ const result = await client.getPayees(options.budget, options.lastKnowledge);
1244
+ outputJson(result?.payees);
1245
+ }
1246
+ )
1247
+ );
1248
+ cmd.command("view").description("View payee details").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(
1249
+ withErrorHandling(async (id, options) => {
1250
+ const payee = await client.getPayee(id, options.budget);
1251
+ outputJson(payee);
1252
+ })
1253
+ );
1254
+ cmd.command("update").description("Rename payee").argument("<id>", "Payee ID").requiredOption("--name <name>", "New payee name").option("-b, --budget <id>", "Budget ID").action(
1255
+ withErrorHandling(
1256
+ async (id, options) => {
1257
+ if (!options.name?.trim()) {
1258
+ throw new YnabCliError("Name cannot be empty", 400);
1259
+ }
1260
+ const payee = await client.updatePayee(
1261
+ id,
1262
+ { payee: { name: options.name } },
1263
+ options.budget
1264
+ );
1265
+ outputJson(payee);
1266
+ }
1267
+ )
1268
+ );
1269
+ cmd.command("locations").description("List locations for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(
1270
+ withErrorHandling(async (id, options) => {
1271
+ const locations = await client.getPayeeLocationsByPayee(id, options.budget);
1272
+ outputJson(locations);
1273
+ })
1274
+ );
1275
+ cmd.command("transactions").description("List transactions for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
1276
+ withErrorHandling(
1277
+ async (id, options) => {
1278
+ const result = await client.getTransactionsByPayee(id, {
1279
+ budgetId: options.budget,
1280
+ sinceDate: options.since,
1281
+ type: options.type,
1282
+ lastKnowledgeOfServer: options.lastKnowledge
1283
+ });
1284
+ outputJson(result?.transactions);
1285
+ }
1286
+ )
1287
+ );
1255
1288
  return cmd;
1256
1289
  }
1257
1290
 
@@ -1259,14 +1292,20 @@ function createPayeesCommand() {
1259
1292
  import { Command as Command9 } from "commander";
1260
1293
  function createMonthsCommand() {
1261
1294
  const cmd = new Command9("months").description("Monthly budget operations");
1262
- cmd.command("list").description("List all budget months").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
1263
- const result = await client.getBudgetMonths(options.budget, options.lastKnowledge);
1264
- outputJson(result?.months);
1265
- }));
1266
- cmd.command("view").description("View specific month details").argument("<month>", "Month in YYYY-MM-DD format (e.g., 2025-07-01)").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (month, options) => {
1267
- const monthData = await client.getBudgetMonth(month, options.budget);
1268
- outputJson(monthData);
1269
- }));
1295
+ cmd.command("list").description("List all budget months").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
1296
+ withErrorHandling(
1297
+ async (options) => {
1298
+ const result = await client.getBudgetMonths(options.budget, options.lastKnowledge);
1299
+ outputJson(result?.months);
1300
+ }
1301
+ )
1302
+ );
1303
+ cmd.command("view").description("View specific month details").argument("<month>", "Month in YYYY-MM-DD format (e.g., 2025-07-01)").option("-b, --budget <id>", "Budget ID").action(
1304
+ withErrorHandling(async (month, options) => {
1305
+ const monthData = await client.getBudgetMonth(month, options.budget);
1306
+ outputJson(monthData);
1307
+ })
1308
+ );
1270
1309
  return cmd;
1271
1310
  }
1272
1311
 
@@ -1275,32 +1314,36 @@ import { Command as Command10 } from "commander";
1275
1314
  var VALID_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
1276
1315
  function createApiCommand() {
1277
1316
  const cmd = new Command10("api").description("Raw API access");
1278
- cmd.argument("<method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)").argument("<path>", "API path (e.g., /budgets or /budgets/{budget_id}/transactions)").option("-b, --budget <id>", "Budget ID (used to replace {budget_id} in path)").option("--data <json>", "JSON data for POST/PUT/PATCH requests").description("Make raw API calls to YNAB").action(withErrorHandling(async (method, path, options) => {
1279
- const upperMethod = method.toUpperCase();
1280
- if (!VALID_HTTP_METHODS.includes(upperMethod)) {
1281
- throw new YnabCliError(
1282
- `Invalid HTTP method: ${method}. Must be one of: ${VALID_HTTP_METHODS.join(", ")}`,
1283
- 400
1284
- );
1285
- }
1286
- let data;
1287
- if (options.data) {
1288
- try {
1289
- const parsedData = JSON.parse(options.data);
1290
- data = validateJson(parsedData, ApiDataSchema, "API data");
1291
- } catch (error) {
1292
- throw new YnabCliError("Invalid JSON in --data parameter", 400);
1317
+ cmd.argument("<method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)").argument("<path>", "API path (e.g., /budgets or /budgets/{budget_id}/transactions)").option("-b, --budget <id>", "Budget ID (used to replace {budget_id} in path)").option("--data <json>", "JSON data for POST/PUT/PATCH requests").description("Make raw API calls to YNAB").action(
1318
+ withErrorHandling(
1319
+ async (method, path, options) => {
1320
+ const upperMethod = method.toUpperCase();
1321
+ if (!VALID_HTTP_METHODS.includes(upperMethod)) {
1322
+ throw new YnabCliError(
1323
+ `Invalid HTTP method: ${method}. Must be one of: ${VALID_HTTP_METHODS.join(", ")}`,
1324
+ 400
1325
+ );
1326
+ }
1327
+ let data;
1328
+ if (options.data) {
1329
+ try {
1330
+ const parsedData = JSON.parse(options.data);
1331
+ data = validateJson(parsedData, ApiDataSchema, "API data");
1332
+ } catch {
1333
+ throw new YnabCliError("Invalid JSON in --data parameter", 400);
1334
+ }
1335
+ }
1336
+ const result = await client.rawApiCall(upperMethod, path, data, options.budget);
1337
+ outputJson(result);
1293
1338
  }
1294
- }
1295
- const result = await client.rawApiCall(upperMethod, path, data, options.budget);
1296
- outputJson(result);
1297
- }));
1339
+ )
1340
+ );
1298
1341
  return cmd;
1299
1342
  }
1300
1343
 
1301
1344
  // src/cli.ts
1302
1345
  var program = new Command11();
1303
- program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("1.2.5").option("-c, --compact", "Minified JSON output (single line)").hook("preAction", (thisCommand) => {
1346
+ program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("1.2.6").option("-c, --compact", "Minified JSON output (single line)").hook("preAction", (thisCommand) => {
1304
1347
  const options = thisCommand.opts();
1305
1348
  setOutputOptions({
1306
1349
  compact: options.compact