@profullstack/coinpay 0.15.0 → 0.16.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.16.0] - 2026-10-12
9
+
10
+ ### Added
11
+ - `coinpay finances due [--days 7]`: what is due, overdue or not posted yet
12
+ across every linked account, projected from each recurring charge's own
13
+ cadence plus statement due days set on cards and loans.
14
+ - `coinpay finances recurring`: every recurring charge with cadence, last and
15
+ next date and monthly cost.
16
+ - `coinpay finances due-day <account-id> <1-31|clear> [--amount N|balance]`:
17
+ a card or loan's statement due day, which no bank feed carries.
18
+ - `coinpay finances daily [show|on|off|send-now]`: the daily summary email
19
+ (due soon, new activity since the last one, balance sheet).
20
+ - MCP: `finance_due_soon`, `finance_recurring`, `finance_summary`,
21
+ `finance_transactions`, `finance_set_due_day`, `finance_daily_summary`.
22
+ - SDK: `getDueSoon`, `getDailySummary`, `setDailySummary`,
23
+ `deleteDailySummary`, `sendDailySummaryNow`, `setAccountDueDay`.
24
+
25
+ ### Fixed
26
+ - `coinpay mcp` `tools/list` now carries `resultType`, `ttlMs` and
27
+ `cacheScope`; without them Claude Code rejected the whole list
28
+ ("connected · tools fetch failed").
29
+
8
30
  ## [0.15.0] - 2026-10-07
9
31
 
10
32
  ### Added
package/bin/coinpay.js CHANGED
@@ -529,6 +529,10 @@ ${colors.cyan}Commands:${colors.reset}
529
529
  --with-documents attaches the period's tax documents within
530
530
  8 MiB; only to your own address)
531
531
  digest [show|set|off|send-now] Weekly digest email (--days mon,fri --hour 8 --to …)
532
+ due [--days 7] What is due and overdue across every account (--days up to 60)
533
+ recurring Every recurring charge detected, with next date and monthly cost
534
+ due-day <id> <1-31|clear> A card/loan's statement due day (--amount 810.46, or balance)
535
+ daily [show|on|off|send-now] Daily summary email: due soon, new activity, position (--hour 7 --to …)
532
536
 
533
537
  ${colors.bright}escrow${colors.reset}
534
538
  create Create a new escrow
@@ -4355,7 +4359,12 @@ async function handleFinances(subcommand, args, flags) {
4355
4359
  case 'statements':
4356
4360
  case 'books':
4357
4361
  case 'payloads':
4358
- case 'digest': {
4362
+ case 'digest':
4363
+ case 'due':
4364
+ case 'due-soon':
4365
+ case 'recurring':
4366
+ case 'due-day':
4367
+ case 'daily': {
4359
4368
  const { client } = financesClient();
4360
4369
  const { runFinancesCommand } = await import('../src/finances-commands.js');
4361
4370
  const code = await runFinancesCommand(subcommand, args, flags, {
@@ -4369,7 +4378,7 @@ async function handleFinances(subcommand, args, flags) {
4369
4378
 
4370
4379
  default:
4371
4380
  print.error(`Unknown finances command: ${subcommand}`);
4372
- console.log('Usage: coinpay finances [tui|summary|accounts|ledger|connections|sync|connect|disconnect|backfill|jobs|coverage|report|reports|statements|books|payloads|digest]');
4381
+ console.log('Usage: coinpay finances [tui|summary|accounts|ledger|connections|sync|connect|disconnect|backfill|jobs|coverage|report|reports|statements|books|payloads|digest|due|recurring|due-day|daily]');
4373
4382
  process.exit(1);
4374
4383
  }
4375
4384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -846,6 +846,80 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
846
846
  throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances digest [show|set --days mon,fri --hour 8 --to a@x,b@y [--scope business]|off|delete|send-now]');
847
847
  }
848
848
 
849
+ case 'due':
850
+ case 'due-soon':
851
+ case 'recurring': {
852
+ const days = flags.days !== undefined ? Number(flags.days) : undefined;
853
+ if (days !== undefined && (!Number.isInteger(days) || days < 0 || days > 60)) throw new CliExit(EXIT.INVALID, '--days must be 0 to 60');
854
+ const d = await api.getDueSoon(client, { days, timezone: typeof flags.timezone === 'string' ? flags.timezone : undefined });
855
+ const cur = d.currency || 'USD';
856
+ const money = (n) => new Intl.NumberFormat('en-US', { style: 'currency', currency: cur }).format(n);
857
+ if (subcommand === 'recurring') {
858
+ if (json) { out(JSON.stringify(d.recurring, null, 2)); return EXIT.OK; }
859
+ const rows = [...d.recurring.charges].sort((a, b) => a.nextExpected.localeCompare(b.nextExpected));
860
+ out(`${rows.length} recurring charges · ${money(d.recurring.monthlyTotal)}/mo${d.recurring.monthlyDebtService ? ` (${money(d.recurring.monthlyDebtService)} debt service)` : ''}`);
861
+ for (const c of rows) {
862
+ out(` next ${c.nextExpected.slice(0, 10)} ${c.cadence.padEnd(9)} ${money(c.amount).padStart(11)} ${c.payee} · ${c.accountName}${c.isDebtService ? ' · debt' : ''}`);
863
+ }
864
+ return EXIT.OK;
865
+ }
866
+ if (json) { out(JSON.stringify(d, null, 2)); return EXIT.OK; }
867
+ const when = (i) => i.status === 'paid' ? 'paid' : i.status === 'awaiting' ? 'not yet?' : i.daysUntil === 0 ? 'today' : i.daysUntil === 1 ? 'tomorrow' : i.daysUntil < 0 ? `${-i.daysUntil}d LATE` : `in ${i.daysUntil}d`;
868
+ const amt = (i) => i.amount !== null ? money(i.amount) : i.owed !== null ? `bal ${money(i.owed)}` : 'statement';
869
+ out(`${d.today} (${d.timezone}) · ${money(d.totals.due)} due in ${d.horizonDays} days${d.totals.overdue ? ` · ${money(d.totals.overdue)} OVERDUE` : ''}${d.totals.unpriced ? ` · +${d.totals.unpriced} statement balance(s)` : ''}`);
870
+ if (d.items.length === 0 && d.paid.length === 0) out(' Nothing due.');
871
+ for (const i of [...d.items, ...d.paid]) {
872
+ out(` ${i.dueOn} ${when(i).padEnd(9)} ${amt(i).padStart(14)} ${i.payee} · ${i.accountName}${i.source === 'scheduled' ? ' · statement due' : ''}`);
873
+ }
874
+ if (d.missed.length) out(` Not seen lately: ${d.missed.slice(0, 8).map((m) => `${m.payee} (expected ${m.expectedOn})`).join(', ')}`);
875
+ return EXIT.OK;
876
+ }
877
+
878
+ case 'due-day': {
879
+ const id = args[0];
880
+ const value = args[1];
881
+ if (!id || value === undefined) throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances due-day <account-id> <1-31|clear> [--amount 810.46|--amount balance] (ids: coinpay finances accounts)');
882
+ const day = value === 'clear' || value === 'off' ? null : Number(value);
883
+ if (day !== null && (!Number.isInteger(day) || day < 1 || day > 31)) throw new CliExit(EXIT.INVALID, 'The due day must be 1 to 31, or clear');
884
+ const amount = flags.amount === undefined ? undefined : flags.amount === 'balance' || flags.amount === true ? null : String(flags.amount);
885
+ const account = await api.setAccountDueDay(client, id, { day, amount });
886
+ emit({ account }, day === null ? `Cleared the due day on ${account.name}.` : `${account.name}: due on day ${day}${account.payment_due_amount != null ? `, ${account.payment_due_amount}` : ', statement balance'}.`);
887
+ return EXIT.OK;
888
+ }
889
+
890
+ case 'daily': {
891
+ const action = flags.off ? 'off' : args[0] || 'show';
892
+ const describe = (s) => s
893
+ ? `Daily summary ${s.active ? 'on' : 'off'} · ${String(s.hour).padStart(2, '0')}:00 ${s.timezone} · to ${s.recipients.join(', ')}${s.nextRunAt ? ` · next ${s.nextRunAt}` : ''}${s.lastSentAt ? ` · last ${s.lastSentAt}` : ''}`
894
+ : 'No daily summary configured. Turn it on with: coinpay finances daily on --hour 7 --to you@example.com';
895
+ if (action === 'show') {
896
+ const s = await api.getDailySummary(client);
897
+ emit({ schedule: s }, describe(s));
898
+ return EXIT.OK;
899
+ }
900
+ if (action === 'on' || action === 'set') {
901
+ const s = await api.setDailySummary(client, {
902
+ hour: flags.hour !== undefined ? Number(flags.hour) : undefined,
903
+ timezone: typeof flags.timezone === 'string' ? flags.timezone : undefined,
904
+ recipients: flags.to ? listFlag(flags.to) : undefined,
905
+ active: true,
906
+ });
907
+ emit({ schedule: s }, describe(s));
908
+ return EXIT.OK;
909
+ }
910
+ if (action === 'off' || action === 'delete') {
911
+ const data = await api.deleteDailySummary(client);
912
+ emit(data, 'Daily summary turned off.');
913
+ return EXIT.OK;
914
+ }
915
+ if (action === 'send-now') {
916
+ const data = await api.sendDailySummaryNow(client);
917
+ emit(data, `Sent to ${data.sent.join(', ') || 'nobody'} (${data.due} due, ${data.overdue} overdue)${data.failed.length ? `; failed: ${data.failed.map((f) => `${f.to} (${f.error})`).join(', ')}` : ''}`);
918
+ return data.sent.length > 0 ? EXIT.OK : EXIT.FAILURE;
919
+ }
920
+ throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances daily [show|on --hour 7 --to a@x|off|send-now]');
921
+ }
922
+
849
923
  case 'payloads': {
850
924
  const action = args[0] || 'list';
851
925
  if (action === 'list') {
@@ -881,4 +955,4 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
881
955
  }
882
956
  }
883
957
 
884
- export const EXTENDED_SUBCOMMANDS = ['connect', 'disconnect', 'consent', 'backfill', 'jobs', 'coverage', 'report', 'reports', 'statements', 'books', 'payloads', 'digest'];
958
+ export const EXTENDED_SUBCOMMANDS = ['connect', 'disconnect', 'consent', 'backfill', 'jobs', 'coverage', 'report', 'reports', 'statements', 'books', 'payloads', 'digest', 'due', 'due-soon', 'recurring', 'due-day', 'daily'];
@@ -395,3 +395,44 @@ export function setWeeklyDigest(
395
395
  ): Promise<WeeklyDigest>;
396
396
  export function deleteWeeklyDigest(client: CoinPayClient): Promise<{ success: boolean }>;
397
397
  export function sendWeeklyDigestNow(client: CoinPayClient): Promise<SendOutcome>;
398
+
399
+ export interface DueItem {
400
+ source: 'projected' | 'scheduled';
401
+ payee: string;
402
+ amount: number | null;
403
+ owed: number | null;
404
+ cadence: string;
405
+ dueOn: string;
406
+ daysUntil: number;
407
+ status: 'overdue' | 'awaiting' | 'today' | 'soon' | 'paid';
408
+ accountId: string;
409
+ accountName: string;
410
+ scope: 'business' | 'personal';
411
+ isDebtService: boolean;
412
+ }
413
+
414
+ export interface DueSoon {
415
+ timezone: string;
416
+ today: string;
417
+ horizonDays: number;
418
+ items: DueItem[];
419
+ paid: DueItem[];
420
+ missed: Array<{ payee: string; amount: number; expectedOn: string; lastSeen: string; accountName: string }>;
421
+ totals: { due: number; overdue: number; unpriced: number };
422
+ recurring: {
423
+ charges: Array<{ payee: string; amount: number; cadence: string; occurrences: number; lastSeen: string; nextExpected: string; monthlyEquivalent: number; scope: 'business' | 'personal'; isDebtService: boolean; accountId: string; accountName: string }>;
424
+ monthlyTotal: number;
425
+ monthlyDebtService: number;
426
+ };
427
+ currency: string | null;
428
+ }
429
+
430
+ export function getDueSoon(client: CoinPayClient, opts?: { days?: number; timezone?: string }): Promise<DueSoon>;
431
+ export function getDailySummary(client: CoinPayClient): Promise<WeeklyDigest | null>;
432
+ export function setDailySummary(
433
+ client: CoinPayClient,
434
+ opts?: { weekdays?: number[]; hour?: number; timezone?: string; recipients?: string[]; active?: boolean },
435
+ ): Promise<WeeklyDigest>;
436
+ export function deleteDailySummary(client: CoinPayClient): Promise<{ success: boolean }>;
437
+ export function sendDailySummaryNow(client: CoinPayClient): Promise<{ sent: string[]; failed: Array<{ to: string; error: string }>; due: number; overdue: number }>;
438
+ export function setAccountDueDay(client: CoinPayClient, accountId: string, opts: { day: number | null; amount?: number | string | null }): Promise<Record<string, unknown>>;
@@ -445,3 +445,42 @@ export async function deleteWeeklyDigest(client) {
445
445
  export async function sendWeeklyDigestNow(client) {
446
446
  return call(client, '/finances/email-schedules/send-now', { method: 'POST', body: '{}' });
447
447
  }
448
+
449
+ /** Charges due across every account: projected recurring charges plus statement due days. */
450
+ export async function getDueSoon(client, { days, timezone } = {}) {
451
+ const params = new URLSearchParams();
452
+ if (days !== undefined) params.set('days', String(days));
453
+ if (timezone) params.set('timezone', timezone);
454
+ const qs = params.toString();
455
+ const data = await call(client, `/finances/due-soon${qs ? `?${qs}` : ''}`);
456
+ return data.dueSoon;
457
+ }
458
+
459
+ export async function getDailySummary(client) {
460
+ const data = await call(client, '/finances/email-schedules?kind=daily_summary');
461
+ return data.schedule;
462
+ }
463
+
464
+ export async function setDailySummary(client, { weekdays, hour, timezone, recipients, active } = {}) {
465
+ const data = await call(client, '/finances/email-schedules', {
466
+ method: 'POST',
467
+ body: JSON.stringify({ kind: 'daily_summary', weekdays, hour, timezone, recipients, active }),
468
+ });
469
+ return data.schedule;
470
+ }
471
+
472
+ export async function deleteDailySummary(client) {
473
+ return call(client, '/finances/email-schedules?kind=daily_summary', { method: 'DELETE' });
474
+ }
475
+
476
+ export async function sendDailySummaryNow(client) {
477
+ return call(client, '/finances/email-schedules/send-now?kind=daily_summary', { method: 'POST', body: '{}' });
478
+ }
479
+
480
+ /** Set (or clear with day null) a card or loan's statement due day and fixed amount. */
481
+ export async function setAccountDueDay(client, accountId, { day, amount } = {}) {
482
+ const body = { payment_due_day: day ?? null };
483
+ if (amount !== undefined) body.payment_due_amount = amount;
484
+ const data = await call(client, `/finances/accounts/${encodeURIComponent(accountId)}`, { method: 'PATCH', body: JSON.stringify(body) });
485
+ return data.account;
486
+ }
package/src/mcp.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { createInterface } from 'node:readline';
14
14
 
15
15
  import * as reports from './finances-reports.js';
16
- import { listFinanceAccounts } from './finances.js';
16
+ import { getFinanceSummary, listFinanceAccounts, listFinanceTransactions } from './finances.js';
17
17
  import * as sf from './statements-fetch.js';
18
18
 
19
19
  export const PROTOCOL_VERSION = '2025-06-18';
@@ -28,6 +28,88 @@ export const TOOLS = [
28
28
  inputSchema: { type: 'object', properties: { includeHidden: { type: 'boolean' } }, additionalProperties: false },
29
29
  handler: async (args, { client }) => text({ accounts: await listFinanceAccounts(client, { includeHidden: args.includeHidden === true }) }),
30
30
  },
31
+ {
32
+ name: 'finance_due_soon',
33
+ description:
34
+ 'What is due across every linked account: overdue, today and the next N days. Projected from each recurring charge\'s own cadence, plus statement due days set on cards and loans. Also lists charges that stopped appearing ("missed") and paid-this-cycle statements.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ days: { type: 'integer', minimum: 0, maximum: 60, description: 'Horizon in days (default 7)' },
39
+ timezone: { type: 'string', description: 'IANA zone for "today"; defaults to the saved finance timezone' },
40
+ },
41
+ additionalProperties: false,
42
+ },
43
+ handler: async (args, { client }) => {
44
+ const { recurring, ...due } = await reports.getDueSoon(client, { days: args.days, timezone: args.timezone });
45
+ return text({ ...due, recurringMonthlyTotal: recurring.monthlyTotal });
46
+ },
47
+ },
48
+ {
49
+ name: 'finance_recurring',
50
+ description: 'Every recurring charge detected across all accounts (subscriptions, bills, loan and card payments): cadence, typical amount, last and next date, monthly equivalent.',
51
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
52
+ handler: async (_args, { client }) => text((await reports.getDueSoon(client, { days: 0 })).recurring),
53
+ },
54
+ {
55
+ name: 'finance_summary',
56
+ description: 'Balance sheet (assets, debt, net by currency), cashflow for the window, top spending categories, and the debt-and-income position.',
57
+ inputSchema: { type: 'object', properties: { days: { type: 'integer', minimum: 1, maximum: 3650, description: 'Cashflow window (default 30)' } }, additionalProperties: false },
58
+ handler: async (args, { client }) => text(await getFinanceSummary(client, { days: args.days || 30 })),
59
+ },
60
+ {
61
+ name: 'finance_transactions',
62
+ description: 'The bank and card ledger, newest first. Filter by account id, text search, category or date range (YYYY-MM-DD).',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ accountId: { type: 'string' },
67
+ search: { type: 'string' },
68
+ category: { type: 'string' },
69
+ startDate: { type: 'string' },
70
+ endDate: { type: 'string' },
71
+ limit: { type: 'integer', minimum: 1, maximum: 500 },
72
+ offset: { type: 'integer', minimum: 0 },
73
+ },
74
+ additionalProperties: false,
75
+ },
76
+ handler: async (args, { client }) => text(await listFinanceTransactions(client, { ...args, limit: args.limit || 50 })),
77
+ },
78
+ {
79
+ name: 'finance_set_due_day',
80
+ description: 'Set or clear a card or loan\'s statement due day (1-31, null clears) and optionally a fixed monthly amount (null = statement balance). Puts the account on the due-soon list.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ accountId: { type: 'string' },
85
+ day: { type: ['integer', 'null'], minimum: 1, maximum: 31 },
86
+ amount: { type: ['number', 'string', 'null'] },
87
+ },
88
+ required: ['accountId', 'day'],
89
+ additionalProperties: false,
90
+ },
91
+ handler: async (args, { client }) => text({ account: await reports.setAccountDueDay(client, args.accountId, { day: args.day, amount: args.amount }) }),
92
+ },
93
+ {
94
+ name: 'finance_daily_summary',
95
+ description: 'The daily summary email (due soon, new activity, position): show it, turn it on (optionally hour 0-23 and recipients), turn it off, or send one now.',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ action: { type: 'string', enum: ['show', 'on', 'off', 'send_now'] },
100
+ hour: { type: 'integer', minimum: 0, maximum: 23 },
101
+ to: { type: 'array', items: { type: 'string' }, maxItems: 5 },
102
+ },
103
+ required: ['action'],
104
+ additionalProperties: false,
105
+ },
106
+ handler: async (args, { client }) => {
107
+ if (args.action === 'on') return text({ schedule: await reports.setDailySummary(client, { hour: args.hour, recipients: args.to, active: true }) });
108
+ if (args.action === 'off') return text(await reports.deleteDailySummary(client));
109
+ if (args.action === 'send_now') return text(await reports.sendDailySummaryNow(client));
110
+ return text({ schedule: await reports.getDailySummary(client) });
111
+ },
112
+ },
31
113
  {
32
114
  name: 'statements_list',
33
115
  description: 'List PDF statements in the CoinPay statement library, optionally for one account or period (2026-08, 2026-Q3).',
@@ -151,7 +233,7 @@ export async function handleMessage(message, ctx) {
151
233
  capabilities: { tools: {} },
152
234
  serverInfo: { name: 'coinpay', version: ctx.version || '0.0.0' },
153
235
  instructions:
154
- 'CoinPay finances. Statement PDFs come from the banks themselves (SimpleFIN has none): statements_coverage shows what is missing, statements_fetch downloads what is new on this machine. A bank marked login_needed needs a person to run `coinpay finances statements login <bank>`. Tax notices, letters and transcripts (California FTB, IRS) are in tax_documents_list; tax data stays on CoinPay surfaces and is never sent elsewhere.',
236
+ 'CoinPay finances. finance_due_soon answers "what is due" across every account; finance_recurring lists subscriptions and bills; finance_summary is the balance sheet. Statement PDFs come from the banks themselves (SimpleFIN has none): statements_coverage shows what is missing, statements_fetch downloads what is new on this machine. A bank marked login_needed needs a person to run `coinpay finances statements login <bank>`. Tax notices, letters and transcripts (California FTB, IRS) are in tax_documents_list; tax data stays on CoinPay surfaces and is never sent elsewhere.',
155
237
  });
156
238
  case 'notifications/initialized':
157
239
  case 'notifications/cancelled':
@@ -159,7 +241,9 @@ export async function handleMessage(message, ctx) {
159
241
  case 'ping':
160
242
  return reply({});
161
243
  case 'tools/list':
162
- return reply({ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
244
+ // Claude Code's client (revision 2026-07-28) rejects a whole list that
245
+ // lacks these three cache fields, showing "tools fetch failed".
246
+ return reply({ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })), resultType: 'complete', ttlMs: 0, cacheScope: 'private' });
163
247
  case 'tools/call': {
164
248
  const tool = TOOLS.find((t) => t.name === params.name);
165
249
  if (!tool) return fail(-32602, `Unknown tool: ${params.name}`);