@stephendolan/ynab-cli 2.7.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +461 -284
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -187,6 +187,92 @@ function applyTransactionFilters(transactions, filters) {
|
|
|
187
187
|
}
|
|
188
188
|
return filtered;
|
|
189
189
|
}
|
|
190
|
+
function summarizeTransactions(transactions, options) {
|
|
191
|
+
let totalAmount = 0;
|
|
192
|
+
const dates = [];
|
|
193
|
+
const byPayee = /* @__PURE__ */ new Map();
|
|
194
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
195
|
+
const byCleared = /* @__PURE__ */ new Map();
|
|
196
|
+
const byApproval = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const t of transactions) {
|
|
198
|
+
totalAmount += t.amount;
|
|
199
|
+
dates.push(t.date);
|
|
200
|
+
const payeeKey = t.payee_id || t.payee_name || "(none)";
|
|
201
|
+
const payeeEntry = byPayee.get(payeeKey) || {
|
|
202
|
+
payee_id: t.payee_id || null,
|
|
203
|
+
payee_name: t.payee_name || null,
|
|
204
|
+
count: 0,
|
|
205
|
+
total_amount: 0
|
|
206
|
+
};
|
|
207
|
+
payeeEntry.count++;
|
|
208
|
+
payeeEntry.total_amount += t.amount;
|
|
209
|
+
byPayee.set(payeeKey, payeeEntry);
|
|
210
|
+
const catKey = t.category_id || t.category_name || "(uncategorized)";
|
|
211
|
+
const catEntry = byCategory.get(catKey) || {
|
|
212
|
+
category_id: t.category_id || null,
|
|
213
|
+
category_name: t.category_name || null,
|
|
214
|
+
count: 0,
|
|
215
|
+
total_amount: 0
|
|
216
|
+
};
|
|
217
|
+
catEntry.count++;
|
|
218
|
+
catEntry.total_amount += t.amount;
|
|
219
|
+
byCategory.set(catKey, catEntry);
|
|
220
|
+
const clearedEntry = byCleared.get(t.cleared) || { count: 0, total_amount: 0 };
|
|
221
|
+
clearedEntry.count++;
|
|
222
|
+
clearedEntry.total_amount += t.amount;
|
|
223
|
+
byCleared.set(t.cleared, clearedEntry);
|
|
224
|
+
const approvalKey = String(t.approved);
|
|
225
|
+
const approvalEntry = byApproval.get(approvalKey) || { count: 0, total_amount: 0 };
|
|
226
|
+
approvalEntry.count++;
|
|
227
|
+
approvalEntry.total_amount += t.amount;
|
|
228
|
+
byApproval.set(approvalKey, approvalEntry);
|
|
229
|
+
}
|
|
230
|
+
const sortByAbsAmount = (entries) => entries.sort((a, b) => Math.abs(b.total_amount) - Math.abs(a.total_amount));
|
|
231
|
+
const truncate = (entries, rollupFactory) => {
|
|
232
|
+
const top = options?.top;
|
|
233
|
+
if (!top || top <= 0 || entries.length <= top) return entries;
|
|
234
|
+
const kept = entries.slice(0, top);
|
|
235
|
+
const rest = entries.slice(top);
|
|
236
|
+
const rollup = rollupFactory({
|
|
237
|
+
count: rest.reduce((sum, e) => sum + e.count, 0),
|
|
238
|
+
total_amount: rest.reduce((sum, e) => sum + e.total_amount, 0)
|
|
239
|
+
});
|
|
240
|
+
return [...kept, rollup];
|
|
241
|
+
};
|
|
242
|
+
const payeeBreakdown = sortByAbsAmount([...byPayee.values()]);
|
|
243
|
+
const categoryBreakdown = sortByAbsAmount([...byCategory.values()]);
|
|
244
|
+
return {
|
|
245
|
+
total_count: transactions.length,
|
|
246
|
+
total_amount: totalAmount,
|
|
247
|
+
date_range: dates.length > 0 ? {
|
|
248
|
+
from: dates.reduce((a, b) => a < b ? a : b),
|
|
249
|
+
to: dates.reduce((a, b) => a > b ? a : b)
|
|
250
|
+
} : null,
|
|
251
|
+
by_payee: truncate(payeeBreakdown, (e) => ({ payee_id: null, payee_name: "(other)", ...e })),
|
|
252
|
+
by_category: truncate(categoryBreakdown, (e) => ({ category_id: null, category_name: "(other)", ...e })),
|
|
253
|
+
by_cleared_status: [...byCleared.entries()].map(([status, entry]) => ({ status, ...entry })),
|
|
254
|
+
by_approval_status: [...byApproval.entries()].map(([approved, entry]) => ({ approved: approved === "true", ...entry }))
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function findTransferCandidates(source, allTransactions, options) {
|
|
258
|
+
const sourceAmount = Math.abs(source.amount);
|
|
259
|
+
const sourceDateMs = new Date(source.date).getTime();
|
|
260
|
+
const msPerDay = 864e5;
|
|
261
|
+
function daysBetween(t) {
|
|
262
|
+
return Math.abs(new Date(t.date).getTime() - sourceDateMs) / msPerDay;
|
|
263
|
+
}
|
|
264
|
+
return allTransactions.filter((t) => {
|
|
265
|
+
if (t.account_id === source.account_id) return false;
|
|
266
|
+
if (Math.abs(t.amount) !== sourceAmount) return false;
|
|
267
|
+
if (Math.sign(t.amount) === Math.sign(source.amount)) return false;
|
|
268
|
+
return daysBetween(t) <= options.maxDays;
|
|
269
|
+
}).map((t) => ({
|
|
270
|
+
transaction: t,
|
|
271
|
+
already_linked: !!t.transfer_transaction_id,
|
|
272
|
+
date_difference_days: Math.round(daysBetween(t)),
|
|
273
|
+
has_transfer_payee: !!t.payee_name?.startsWith("Transfer :")
|
|
274
|
+
})).sort((a, b) => a.date_difference_days - b.date_difference_days);
|
|
275
|
+
}
|
|
190
276
|
function applyFieldSelection(items, fields) {
|
|
191
277
|
if (!fields) return items;
|
|
192
278
|
const fieldList = fields.split(",").map((f) => f.trim());
|
|
@@ -352,338 +438,271 @@ var YnabClient = class {
|
|
|
352
438
|
}
|
|
353
439
|
return budgetId;
|
|
354
440
|
}
|
|
355
|
-
async withErrorHandling(fn) {
|
|
356
|
-
try {
|
|
357
|
-
return await fn();
|
|
358
|
-
} catch (error) {
|
|
359
|
-
handleYnabError(error);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
441
|
async getUser() {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return response.data.user;
|
|
367
|
-
});
|
|
442
|
+
const api = await this.getApi();
|
|
443
|
+
const response = await api.user.getUser();
|
|
444
|
+
return response.data.user;
|
|
368
445
|
}
|
|
369
446
|
async getBudgets(includeAccounts = false) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
};
|
|
377
|
-
});
|
|
447
|
+
const api = await this.getApi();
|
|
448
|
+
const response = await api.budgets.getBudgets(includeAccounts);
|
|
449
|
+
return {
|
|
450
|
+
budgets: response.data.budgets,
|
|
451
|
+
server_knowledge: 0
|
|
452
|
+
};
|
|
378
453
|
}
|
|
379
454
|
async getBudget(budgetId, lastKnowledgeOfServer) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
};
|
|
388
|
-
});
|
|
455
|
+
const api = await this.getApi();
|
|
456
|
+
const id = await this.getBudgetId(budgetId);
|
|
457
|
+
const response = await api.budgets.getBudgetById(id, lastKnowledgeOfServer);
|
|
458
|
+
return {
|
|
459
|
+
budget: response.data.budget,
|
|
460
|
+
server_knowledge: response.data.server_knowledge
|
|
461
|
+
};
|
|
389
462
|
}
|
|
390
463
|
async getBudgetSettings(budgetId) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return response.data.settings;
|
|
396
|
-
});
|
|
464
|
+
const api = await this.getApi();
|
|
465
|
+
const id = await this.getBudgetId(budgetId);
|
|
466
|
+
const response = await api.budgets.getBudgetSettingsById(id);
|
|
467
|
+
return response.data.settings;
|
|
397
468
|
}
|
|
398
469
|
async getAccounts(budgetId, lastKnowledgeOfServer) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
};
|
|
407
|
-
});
|
|
470
|
+
const api = await this.getApi();
|
|
471
|
+
const id = await this.getBudgetId(budgetId);
|
|
472
|
+
const response = await api.accounts.getAccounts(id, lastKnowledgeOfServer);
|
|
473
|
+
return {
|
|
474
|
+
accounts: response.data.accounts,
|
|
475
|
+
server_knowledge: response.data.server_knowledge
|
|
476
|
+
};
|
|
408
477
|
}
|
|
409
478
|
async getAccount(accountId, budgetId) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return response.data.account;
|
|
415
|
-
});
|
|
479
|
+
const api = await this.getApi();
|
|
480
|
+
const id = await this.getBudgetId(budgetId);
|
|
481
|
+
const response = await api.accounts.getAccountById(id, accountId);
|
|
482
|
+
return response.data.account;
|
|
416
483
|
}
|
|
417
484
|
async getCategories(budgetId, lastKnowledgeOfServer) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
};
|
|
426
|
-
});
|
|
485
|
+
const api = await this.getApi();
|
|
486
|
+
const id = await this.getBudgetId(budgetId);
|
|
487
|
+
const response = await api.categories.getCategories(id, lastKnowledgeOfServer);
|
|
488
|
+
return {
|
|
489
|
+
category_groups: response.data.category_groups,
|
|
490
|
+
server_knowledge: response.data.server_knowledge
|
|
491
|
+
};
|
|
427
492
|
}
|
|
428
493
|
async getCategory(categoryId, budgetId) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return response.data.category;
|
|
434
|
-
});
|
|
494
|
+
const api = await this.getApi();
|
|
495
|
+
const id = await this.getBudgetId(budgetId);
|
|
496
|
+
const response = await api.categories.getCategoryById(id, categoryId);
|
|
497
|
+
return response.data.category;
|
|
435
498
|
}
|
|
436
499
|
async updateMonthCategory(month, categoryId, data, budgetId) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return response.data.category;
|
|
442
|
-
});
|
|
500
|
+
const api = await this.getApi();
|
|
501
|
+
const id = await this.getBudgetId(budgetId);
|
|
502
|
+
const response = await api.categories.updateMonthCategory(id, month, categoryId, data);
|
|
503
|
+
return response.data.category;
|
|
443
504
|
}
|
|
444
505
|
async updateCategory(categoryId, data, budgetId) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return response.data.category;
|
|
450
|
-
});
|
|
506
|
+
const api = await this.getApi();
|
|
507
|
+
const id = await this.getBudgetId(budgetId);
|
|
508
|
+
const response = await api.categories.updateCategory(id, categoryId, data);
|
|
509
|
+
return response.data.category;
|
|
451
510
|
}
|
|
452
511
|
async getPayees(budgetId, lastKnowledgeOfServer) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
};
|
|
461
|
-
});
|
|
512
|
+
const api = await this.getApi();
|
|
513
|
+
const id = await this.getBudgetId(budgetId);
|
|
514
|
+
const response = await api.payees.getPayees(id, lastKnowledgeOfServer);
|
|
515
|
+
return {
|
|
516
|
+
payees: response.data.payees,
|
|
517
|
+
server_knowledge: response.data.server_knowledge
|
|
518
|
+
};
|
|
462
519
|
}
|
|
463
520
|
async getPayee(payeeId, budgetId) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return response.data.payee;
|
|
469
|
-
});
|
|
521
|
+
const api = await this.getApi();
|
|
522
|
+
const id = await this.getBudgetId(budgetId);
|
|
523
|
+
const response = await api.payees.getPayeeById(id, payeeId);
|
|
524
|
+
return response.data.payee;
|
|
470
525
|
}
|
|
471
526
|
async updatePayee(payeeId, data, budgetId) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return response.data.payee;
|
|
477
|
-
});
|
|
527
|
+
const api = await this.getApi();
|
|
528
|
+
const id = await this.getBudgetId(budgetId);
|
|
529
|
+
const response = await api.payees.updatePayee(id, payeeId, data);
|
|
530
|
+
return response.data.payee;
|
|
478
531
|
}
|
|
479
532
|
async getPayeeLocationsByPayee(payeeId, budgetId) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return response.data.payee_locations;
|
|
485
|
-
});
|
|
533
|
+
const api = await this.getApi();
|
|
534
|
+
const id = await this.getBudgetId(budgetId);
|
|
535
|
+
const response = await api.payeeLocations.getPayeeLocationsByPayee(id, payeeId);
|
|
536
|
+
return response.data.payee_locations;
|
|
486
537
|
}
|
|
487
538
|
async getBudgetMonths(budgetId, lastKnowledgeOfServer) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
};
|
|
496
|
-
});
|
|
539
|
+
const api = await this.getApi();
|
|
540
|
+
const id = await this.getBudgetId(budgetId);
|
|
541
|
+
const response = await api.months.getBudgetMonths(id, lastKnowledgeOfServer);
|
|
542
|
+
return {
|
|
543
|
+
months: response.data.months,
|
|
544
|
+
server_knowledge: response.data.server_knowledge
|
|
545
|
+
};
|
|
497
546
|
}
|
|
498
547
|
async getBudgetMonth(month, budgetId) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return response.data.month;
|
|
504
|
-
});
|
|
548
|
+
const api = await this.getApi();
|
|
549
|
+
const id = await this.getBudgetId(budgetId);
|
|
550
|
+
const response = await api.months.getBudgetMonth(id, month);
|
|
551
|
+
return response.data.month;
|
|
505
552
|
}
|
|
506
553
|
async getTransactions(params) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
};
|
|
520
|
-
});
|
|
554
|
+
const api = await this.getApi();
|
|
555
|
+
const id = await this.getBudgetId(params.budgetId);
|
|
556
|
+
const response = await api.transactions.getTransactions(
|
|
557
|
+
id,
|
|
558
|
+
params.sinceDate,
|
|
559
|
+
params.type,
|
|
560
|
+
params.lastKnowledgeOfServer
|
|
561
|
+
);
|
|
562
|
+
return {
|
|
563
|
+
transactions: response.data.transactions,
|
|
564
|
+
server_knowledge: response.data.server_knowledge
|
|
565
|
+
};
|
|
521
566
|
}
|
|
522
567
|
async getTransactionsByAccount(accountId, params) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
};
|
|
537
|
-
});
|
|
568
|
+
const api = await this.getApi();
|
|
569
|
+
const id = await this.getBudgetId(params.budgetId);
|
|
570
|
+
const response = await api.transactions.getTransactionsByAccount(
|
|
571
|
+
id,
|
|
572
|
+
accountId,
|
|
573
|
+
params.sinceDate,
|
|
574
|
+
params.type,
|
|
575
|
+
params.lastKnowledgeOfServer
|
|
576
|
+
);
|
|
577
|
+
return {
|
|
578
|
+
transactions: response.data.transactions,
|
|
579
|
+
server_knowledge: response.data.server_knowledge
|
|
580
|
+
};
|
|
538
581
|
}
|
|
539
582
|
async getTransactionsByCategory(categoryId, params) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
};
|
|
554
|
-
});
|
|
583
|
+
const api = await this.getApi();
|
|
584
|
+
const id = await this.getBudgetId(params.budgetId);
|
|
585
|
+
const response = await api.transactions.getTransactionsByCategory(
|
|
586
|
+
id,
|
|
587
|
+
categoryId,
|
|
588
|
+
params.sinceDate,
|
|
589
|
+
params.type,
|
|
590
|
+
params.lastKnowledgeOfServer
|
|
591
|
+
);
|
|
592
|
+
return {
|
|
593
|
+
transactions: response.data.transactions,
|
|
594
|
+
server_knowledge: response.data.server_knowledge
|
|
595
|
+
};
|
|
555
596
|
}
|
|
556
597
|
async getTransactionsByPayee(payeeId, params) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
};
|
|
571
|
-
});
|
|
598
|
+
const api = await this.getApi();
|
|
599
|
+
const id = await this.getBudgetId(params.budgetId);
|
|
600
|
+
const response = await api.transactions.getTransactionsByPayee(
|
|
601
|
+
id,
|
|
602
|
+
payeeId,
|
|
603
|
+
params.sinceDate,
|
|
604
|
+
params.type,
|
|
605
|
+
params.lastKnowledgeOfServer
|
|
606
|
+
);
|
|
607
|
+
return {
|
|
608
|
+
transactions: response.data.transactions,
|
|
609
|
+
server_knowledge: response.data.server_knowledge
|
|
610
|
+
};
|
|
572
611
|
}
|
|
573
612
|
async getTransaction(transactionId, budgetId) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return response.data.transaction;
|
|
579
|
-
});
|
|
613
|
+
const api = await this.getApi();
|
|
614
|
+
const id = await this.getBudgetId(budgetId);
|
|
615
|
+
const response = await api.transactions.getTransactionById(id, transactionId);
|
|
616
|
+
return response.data.transaction;
|
|
580
617
|
}
|
|
581
618
|
async createTransaction(transactionData, budgetId) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return response.data.transaction;
|
|
587
|
-
});
|
|
619
|
+
const api = await this.getApi();
|
|
620
|
+
const id = await this.getBudgetId(budgetId);
|
|
621
|
+
const response = await api.transactions.createTransaction(id, transactionData);
|
|
622
|
+
return response.data.transaction;
|
|
588
623
|
}
|
|
589
624
|
async updateTransaction(transactionId, transactionData, budgetId) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return response.data.transaction;
|
|
595
|
-
});
|
|
625
|
+
const api = await this.getApi();
|
|
626
|
+
const id = await this.getBudgetId(budgetId);
|
|
627
|
+
const response = await api.transactions.updateTransaction(id, transactionId, transactionData);
|
|
628
|
+
return response.data.transaction;
|
|
596
629
|
}
|
|
597
630
|
async updateTransactions(transactions, budgetId) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
};
|
|
607
|
-
});
|
|
631
|
+
const api = await this.getApi();
|
|
632
|
+
const id = await this.getBudgetId(budgetId);
|
|
633
|
+
const response = await api.transactions.updateTransactions(id, transactions);
|
|
634
|
+
return {
|
|
635
|
+
transactions: response.data.transactions,
|
|
636
|
+
transaction_ids: response.data.transaction_ids,
|
|
637
|
+
server_knowledge: response.data.server_knowledge
|
|
638
|
+
};
|
|
608
639
|
}
|
|
609
640
|
async deleteTransaction(transactionId, budgetId) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return response.data.transaction;
|
|
615
|
-
});
|
|
641
|
+
const api = await this.getApi();
|
|
642
|
+
const id = await this.getBudgetId(budgetId);
|
|
643
|
+
const response = await api.transactions.deleteTransaction(id, transactionId);
|
|
644
|
+
return response.data.transaction;
|
|
616
645
|
}
|
|
617
646
|
async importTransactions(budgetId) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return response.data.transaction_ids;
|
|
623
|
-
});
|
|
647
|
+
const api = await this.getApi();
|
|
648
|
+
const id = await this.getBudgetId(budgetId);
|
|
649
|
+
const response = await api.transactions.importTransactions(id);
|
|
650
|
+
return response.data.transaction_ids;
|
|
624
651
|
}
|
|
625
652
|
async getScheduledTransactions(budgetId, lastKnowledgeOfServer) {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
};
|
|
637
|
-
});
|
|
653
|
+
const api = await this.getApi();
|
|
654
|
+
const id = await this.getBudgetId(budgetId);
|
|
655
|
+
const response = await api.scheduledTransactions.getScheduledTransactions(
|
|
656
|
+
id,
|
|
657
|
+
lastKnowledgeOfServer
|
|
658
|
+
);
|
|
659
|
+
return {
|
|
660
|
+
scheduled_transactions: response.data.scheduled_transactions,
|
|
661
|
+
server_knowledge: response.data.server_knowledge
|
|
662
|
+
};
|
|
638
663
|
}
|
|
639
664
|
async getScheduledTransaction(scheduledTransactionId, budgetId) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return response.data.scheduled_transaction;
|
|
648
|
-
});
|
|
665
|
+
const api = await this.getApi();
|
|
666
|
+
const id = await this.getBudgetId(budgetId);
|
|
667
|
+
const response = await api.scheduledTransactions.getScheduledTransactionById(
|
|
668
|
+
id,
|
|
669
|
+
scheduledTransactionId
|
|
670
|
+
);
|
|
671
|
+
return response.data.scheduled_transaction;
|
|
649
672
|
}
|
|
650
673
|
async deleteScheduledTransaction(scheduledTransactionId, budgetId) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return response.data.scheduled_transaction;
|
|
659
|
-
});
|
|
674
|
+
const api = await this.getApi();
|
|
675
|
+
const id = await this.getBudgetId(budgetId);
|
|
676
|
+
const response = await api.scheduledTransactions.deleteScheduledTransaction(
|
|
677
|
+
id,
|
|
678
|
+
scheduledTransactionId
|
|
679
|
+
);
|
|
680
|
+
return response.data.scheduled_transaction;
|
|
660
681
|
}
|
|
661
682
|
async rawApiCall(method, path, data, budgetId) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
...hasBody && { body: JSON.stringify(data) }
|
|
680
|
-
});
|
|
681
|
-
if (!response.ok) {
|
|
682
|
-
const errorData = await response.json();
|
|
683
|
-
throw { error: sanitizeApiError(errorData.error || errorData) };
|
|
684
|
-
}
|
|
685
|
-
return await response.json();
|
|
683
|
+
await this.getApi();
|
|
684
|
+
const fullPath = path.includes("{budget_id}") ? path.replace("{budget_id}", await this.getBudgetId(budgetId)) : path;
|
|
685
|
+
const url = `https://api.ynab.com/v1${fullPath}`;
|
|
686
|
+
const accessToken = await auth.getAccessToken() || process.env.YNAB_API_KEY;
|
|
687
|
+
const headers = {
|
|
688
|
+
Authorization: `Bearer ${accessToken}`,
|
|
689
|
+
"Content-Type": "application/json"
|
|
690
|
+
};
|
|
691
|
+
const httpMethod = method.toUpperCase();
|
|
692
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(httpMethod);
|
|
693
|
+
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(httpMethod)) {
|
|
694
|
+
throw new YnabCliError(`Unsupported HTTP method: ${method}`, 400);
|
|
695
|
+
}
|
|
696
|
+
const response = await fetch(url, {
|
|
697
|
+
method: httpMethod,
|
|
698
|
+
headers,
|
|
699
|
+
...hasBody && { body: JSON.stringify(data) }
|
|
686
700
|
});
|
|
701
|
+
if (!response.ok) {
|
|
702
|
+
const errorData = await response.json();
|
|
703
|
+
throw { error: sanitizeApiError(errorData.error || errorData) };
|
|
704
|
+
}
|
|
705
|
+
return await response.json();
|
|
687
706
|
}
|
|
688
707
|
};
|
|
689
708
|
var client = new YnabClient();
|
|
@@ -986,6 +1005,7 @@ function createCategoriesCommand() {
|
|
|
986
1005
|
|
|
987
1006
|
// src/commands/transactions.ts
|
|
988
1007
|
import { Command as Command6 } from "commander";
|
|
1008
|
+
import dayjs2 from "dayjs";
|
|
989
1009
|
|
|
990
1010
|
// src/lib/schemas.ts
|
|
991
1011
|
function validateTransactionSplits(data) {
|
|
@@ -1054,6 +1074,18 @@ function validateApiData(data) {
|
|
|
1054
1074
|
}
|
|
1055
1075
|
|
|
1056
1076
|
// src/commands/transactions.ts
|
|
1077
|
+
async function fetchTransactions(options) {
|
|
1078
|
+
const params = {
|
|
1079
|
+
budgetId: options.budget,
|
|
1080
|
+
sinceDate: options.since ? parseDate(options.since) : void 0,
|
|
1081
|
+
type: options.type,
|
|
1082
|
+
lastKnowledgeOfServer: options.lastKnowledge
|
|
1083
|
+
};
|
|
1084
|
+
if (options.account) return client.getTransactionsByAccount(options.account, params);
|
|
1085
|
+
if (options.category) return client.getTransactionsByCategory(options.category, params);
|
|
1086
|
+
if (options.payee) return client.getTransactionsByPayee(options.payee, params);
|
|
1087
|
+
return client.getTransactions(params);
|
|
1088
|
+
}
|
|
1057
1089
|
function buildTransactionData(options) {
|
|
1058
1090
|
if (!options.account) {
|
|
1059
1091
|
throw new YnabCliError("--account is required in non-interactive mode", 400);
|
|
@@ -1081,25 +1113,27 @@ function createTransactionsCommand() {
|
|
|
1081
1113
|
).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(
|
|
1082
1114
|
"--fields <fields>",
|
|
1083
1115
|
"Comma-separated list of fields to include (e.g., id,date,amount,memo)"
|
|
1084
|
-
).action(
|
|
1116
|
+
).option("--last-knowledge <number>", "Last server knowledge for delta requests. When used, output includes server_knowledge.", parseInt).option("--limit <number>", "Maximum number of transactions to return", parseInt).action(
|
|
1085
1117
|
withErrorHandling(
|
|
1086
1118
|
async (options) => {
|
|
1087
|
-
const
|
|
1088
|
-
budgetId: options.budget,
|
|
1089
|
-
sinceDate: options.since ? parseDate(options.since) : void 0,
|
|
1090
|
-
type: options.type
|
|
1091
|
-
};
|
|
1092
|
-
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);
|
|
1119
|
+
const result = await fetchTransactions(options);
|
|
1093
1120
|
const transactions = result?.transactions || [];
|
|
1094
|
-
|
|
1121
|
+
let filtered = applyTransactionFilters(transactions, {
|
|
1095
1122
|
until: options.until ? parseDate(options.until) : void 0,
|
|
1096
1123
|
approved: options.approved,
|
|
1097
1124
|
status: options.status,
|
|
1098
1125
|
minAmount: options.minAmount,
|
|
1099
1126
|
maxAmount: options.maxAmount
|
|
1100
1127
|
});
|
|
1128
|
+
if (options.limit && options.limit > 0) {
|
|
1129
|
+
filtered = filtered.slice(0, options.limit);
|
|
1130
|
+
}
|
|
1101
1131
|
const selected = applyFieldSelection(filtered, options.fields);
|
|
1102
|
-
|
|
1132
|
+
if (options.lastKnowledge !== void 0) {
|
|
1133
|
+
outputJson({ transactions: selected, server_knowledge: result?.server_knowledge });
|
|
1134
|
+
} else {
|
|
1135
|
+
outputJson(selected);
|
|
1136
|
+
}
|
|
1103
1137
|
}
|
|
1104
1138
|
)
|
|
1105
1139
|
);
|
|
@@ -1294,6 +1328,49 @@ function createTransactionsCommand() {
|
|
|
1294
1328
|
}
|
|
1295
1329
|
)
|
|
1296
1330
|
);
|
|
1331
|
+
cmd.command("summary").description("Summarize transactions with aggregate counts by payee, category, and status").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").option("--until <date>", "Filter transactions until date").option("--type <type>", "Filter by transaction type").option("--approved <value>", "Filter by approval status: true or false").option(
|
|
1332
|
+
"--status <statuses>",
|
|
1333
|
+
"Filter by cleared status: cleared, uncleared, reconciled (comma-separated)"
|
|
1334
|
+
).option("--min-amount <amount>", "Minimum amount in currency units", parseFloat).option("--max-amount <amount>", "Maximum amount in currency units", parseFloat).option("--top <number>", "Limit payee/category breakdowns to top N entries", parseInt).action(
|
|
1335
|
+
withErrorHandling(
|
|
1336
|
+
async (options) => {
|
|
1337
|
+
const result = await fetchTransactions(options);
|
|
1338
|
+
const transactions = result?.transactions || [];
|
|
1339
|
+
const filtered = applyTransactionFilters(transactions, {
|
|
1340
|
+
until: options.until ? parseDate(options.until) : void 0,
|
|
1341
|
+
approved: options.approved,
|
|
1342
|
+
status: options.status,
|
|
1343
|
+
minAmount: options.minAmount,
|
|
1344
|
+
maxAmount: options.maxAmount
|
|
1345
|
+
});
|
|
1346
|
+
const summary = summarizeTransactions(
|
|
1347
|
+
filtered,
|
|
1348
|
+
options.top ? { top: options.top } : void 0
|
|
1349
|
+
);
|
|
1350
|
+
outputJson(summary);
|
|
1351
|
+
}
|
|
1352
|
+
)
|
|
1353
|
+
);
|
|
1354
|
+
cmd.command("find-transfers").description("Find candidate transfer matches for a transaction across accounts").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("--days <number>", "Maximum date difference in days (default: 3)", parseInt).option("--since <date>", "Search transactions since date (defaults to source date minus --days)").action(
|
|
1355
|
+
withErrorHandling(
|
|
1356
|
+
async (id, options) => {
|
|
1357
|
+
const maxDays = options.days ?? 3;
|
|
1358
|
+
const source = await client.getTransaction(id, options.budget);
|
|
1359
|
+
const sinceDate = options.since ? parseDate(options.since) : dayjs2(source.date).subtract(maxDays, "day").format("YYYY-MM-DD");
|
|
1360
|
+
const result = await client.getTransactions({
|
|
1361
|
+
budgetId: options.budget,
|
|
1362
|
+
sinceDate
|
|
1363
|
+
});
|
|
1364
|
+
const allTransactions = result?.transactions || [];
|
|
1365
|
+
const candidates = findTransferCandidates(
|
|
1366
|
+
source,
|
|
1367
|
+
allTransactions,
|
|
1368
|
+
{ maxDays }
|
|
1369
|
+
);
|
|
1370
|
+
outputJson({ source, candidates });
|
|
1371
|
+
}
|
|
1372
|
+
)
|
|
1373
|
+
);
|
|
1297
1374
|
return cmd;
|
|
1298
1375
|
}
|
|
1299
1376
|
|
|
@@ -1470,6 +1547,8 @@ var toolRegistry = [
|
|
|
1470
1547
|
{ name: "delete_transaction", description: "Delete a transaction" },
|
|
1471
1548
|
{ name: "import_transactions", description: "Trigger import of linked bank transactions" },
|
|
1472
1549
|
{ name: "batch_update_transactions", description: "Update multiple transactions in a single API call" },
|
|
1550
|
+
{ name: "summarize_transactions", description: "Get aggregate summary of transactions by payee, category, and status" },
|
|
1551
|
+
{ name: "find_transfer_candidates", description: "Find candidate transfer matches for a transaction across accounts" },
|
|
1473
1552
|
{ name: "list_transactions_by_account", description: "List transactions for a specific account" },
|
|
1474
1553
|
{ name: "list_transactions_by_category", description: "List transactions for a specific category" },
|
|
1475
1554
|
{ name: "list_transactions_by_payee", description: "List transactions for a specific payee" },
|
|
@@ -1489,6 +1568,37 @@ var server = new McpServer({
|
|
|
1489
1568
|
name: "ynab",
|
|
1490
1569
|
version: "1.0.0"
|
|
1491
1570
|
});
|
|
1571
|
+
function mcpErrorResult(error) {
|
|
1572
|
+
let errorBody;
|
|
1573
|
+
if (typeof error === "object" && error !== null && "error" in error) {
|
|
1574
|
+
const apiError = sanitizeApiError(error.error);
|
|
1575
|
+
errorBody = { name: apiError.name, detail: apiError.detail };
|
|
1576
|
+
} else if (error instanceof YnabCliError) {
|
|
1577
|
+
errorBody = { name: "cli_error", detail: sanitizeErrorMessage(error.message) };
|
|
1578
|
+
} else if (error instanceof Error) {
|
|
1579
|
+
errorBody = { name: error.name, detail: sanitizeErrorMessage(error.message) };
|
|
1580
|
+
} else {
|
|
1581
|
+
errorBody = { name: "unknown_error", detail: "An unexpected error occurred" };
|
|
1582
|
+
}
|
|
1583
|
+
return {
|
|
1584
|
+
content: [{ type: "text", text: JSON.stringify({ error: errorBody }) }],
|
|
1585
|
+
isError: true
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
var _serverTool = server.tool.bind(server);
|
|
1589
|
+
server.tool = (...args) => {
|
|
1590
|
+
const handler = args[args.length - 1];
|
|
1591
|
+
if (typeof handler === "function") {
|
|
1592
|
+
args[args.length - 1] = async (...handlerArgs) => {
|
|
1593
|
+
try {
|
|
1594
|
+
return await handler(...handlerArgs);
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
return mcpErrorResult(error);
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
return _serverTool.apply(null, args);
|
|
1601
|
+
};
|
|
1492
1602
|
function jsonResponse(data) {
|
|
1493
1603
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1494
1604
|
}
|
|
@@ -1582,9 +1692,18 @@ server.tool(
|
|
|
1582
1692
|
{
|
|
1583
1693
|
budgetId: z.string().optional().describe("Budget ID (uses default if not specified)"),
|
|
1584
1694
|
sinceDate: z.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD)"),
|
|
1585
|
-
type: z.enum(["uncategorized", "unapproved"]).optional().describe("Filter by transaction type")
|
|
1695
|
+
type: z.enum(["uncategorized", "unapproved"]).optional().describe("Filter by transaction type"),
|
|
1696
|
+
lastKnowledgeOfServer: z.number().optional().describe("Delta sync: only return changes since this server knowledge value. Response includes server_knowledge for next call."),
|
|
1697
|
+
limit: z.number().optional().describe("Maximum number of transactions to return"),
|
|
1698
|
+
fields: z.string().optional().describe("Comma-separated list of fields to include (e.g., id,date,amount,memo)")
|
|
1586
1699
|
},
|
|
1587
|
-
async ({ budgetId, sinceDate, type
|
|
1700
|
+
async ({ budgetId, sinceDate, type, lastKnowledgeOfServer, limit, fields }) => {
|
|
1701
|
+
const result = await client.getTransactions({ budgetId, sinceDate, type, lastKnowledgeOfServer });
|
|
1702
|
+
let transactions = result?.transactions || [];
|
|
1703
|
+
if (limit && limit > 0) transactions = transactions.slice(0, limit);
|
|
1704
|
+
const selected = fields ? applyFieldSelection(transactions, fields) : transactions;
|
|
1705
|
+
return currencyResponse({ transactions: selected, server_knowledge: result?.server_knowledge });
|
|
1706
|
+
}
|
|
1588
1707
|
);
|
|
1589
1708
|
server.tool(
|
|
1590
1709
|
"get_transaction",
|
|
@@ -1703,6 +1822,64 @@ server.tool(
|
|
|
1703
1822
|
);
|
|
1704
1823
|
}
|
|
1705
1824
|
);
|
|
1825
|
+
server.tool(
|
|
1826
|
+
"summarize_transactions",
|
|
1827
|
+
"Get aggregate summary of transactions by payee, category, and status",
|
|
1828
|
+
{
|
|
1829
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not specified)"),
|
|
1830
|
+
sinceDate: z.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD)"),
|
|
1831
|
+
untilDate: z.string().optional().describe("Only return transactions on or before this date (YYYY-MM-DD)"),
|
|
1832
|
+
type: z.enum(["uncategorized", "unapproved"]).optional().describe("Filter by transaction type"),
|
|
1833
|
+
approved: z.enum(["true", "false"]).optional().describe("Filter by approval status"),
|
|
1834
|
+
status: z.string().optional().describe("Filter by cleared status: cleared, uncleared, reconciled (comma-separated)"),
|
|
1835
|
+
minAmount: z.number().optional().describe("Minimum amount in dollars"),
|
|
1836
|
+
maxAmount: z.number().optional().describe("Maximum amount in dollars"),
|
|
1837
|
+
top: z.number().optional().describe("Limit payee/category breakdowns to top N entries")
|
|
1838
|
+
},
|
|
1839
|
+
async ({ budgetId, sinceDate, untilDate, type, approved, status, minAmount, maxAmount, top }) => {
|
|
1840
|
+
const result = await client.getTransactions({ budgetId, sinceDate, type });
|
|
1841
|
+
const transactions = result?.transactions || [];
|
|
1842
|
+
const filtered = applyTransactionFilters(transactions, {
|
|
1843
|
+
until: untilDate,
|
|
1844
|
+
approved,
|
|
1845
|
+
status,
|
|
1846
|
+
minAmount,
|
|
1847
|
+
maxAmount
|
|
1848
|
+
});
|
|
1849
|
+
const summary = summarizeTransactions(
|
|
1850
|
+
filtered,
|
|
1851
|
+
top ? { top } : void 0
|
|
1852
|
+
);
|
|
1853
|
+
return currencyResponse(summary);
|
|
1854
|
+
}
|
|
1855
|
+
);
|
|
1856
|
+
server.tool(
|
|
1857
|
+
"find_transfer_candidates",
|
|
1858
|
+
"Find candidate transfer matches for a transaction across accounts",
|
|
1859
|
+
{
|
|
1860
|
+
transactionId: z.string().describe("Transaction ID to find transfers for"),
|
|
1861
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not specified)"),
|
|
1862
|
+
maxDays: z.number().optional().describe("Maximum date difference in days (default: 3)"),
|
|
1863
|
+
sinceDate: z.string().optional().describe("Search transactions since date (defaults to source date minus maxDays)")
|
|
1864
|
+
},
|
|
1865
|
+
async ({ transactionId, budgetId, maxDays: maxDaysParam, sinceDate }) => {
|
|
1866
|
+
const days = maxDaysParam ?? 3;
|
|
1867
|
+
const source = await client.getTransaction(transactionId, budgetId);
|
|
1868
|
+
if (!sinceDate) {
|
|
1869
|
+
const d = new Date(source.date);
|
|
1870
|
+
d.setDate(d.getDate() - days);
|
|
1871
|
+
sinceDate = d.toISOString().split("T")[0];
|
|
1872
|
+
}
|
|
1873
|
+
const result = await client.getTransactions({ budgetId, sinceDate });
|
|
1874
|
+
const allTransactions = result?.transactions || [];
|
|
1875
|
+
const candidates = findTransferCandidates(
|
|
1876
|
+
source,
|
|
1877
|
+
allTransactions,
|
|
1878
|
+
{ maxDays: days }
|
|
1879
|
+
);
|
|
1880
|
+
return currencyResponse({ source, candidates });
|
|
1881
|
+
}
|
|
1882
|
+
);
|
|
1706
1883
|
server.tool(
|
|
1707
1884
|
"list_transactions_by_account",
|
|
1708
1885
|
"List transactions for a specific account",
|
|
@@ -1867,7 +2044,7 @@ function createMcpCommand() {
|
|
|
1867
2044
|
|
|
1868
2045
|
// src/cli.ts
|
|
1869
2046
|
var program = new Command12();
|
|
1870
|
-
program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("2.
|
|
2047
|
+
program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("2.8.1").option("-c, --compact", "Minified JSON output (single line)").hook("preAction", (thisCommand) => {
|
|
1871
2048
|
const options = thisCommand.opts();
|
|
1872
2049
|
setOutputOptions({
|
|
1873
2050
|
compact: options.compact
|