@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 +22 -0
- package/bin/coinpay.js +11 -2
- package/package.json +1 -1
- package/src/finances-commands.js +75 -1
- package/src/finances-reports.d.ts +41 -0
- package/src/finances-reports.js +39 -0
- package/src/mcp.js +87 -3
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.
|
|
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",
|
package/src/finances-commands.js
CHANGED
|
@@ -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>>;
|
package/src/finances-reports.js
CHANGED
|
@@ -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
|
-
|
|
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}`);
|