@profullstack/coinpay 0.27.1 → 0.28.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,18 @@ 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.28.0] - 2026-10-29
9
+
10
+ ### Added
11
+ - `coinpay finances goals edit <id>` changes any field of a goal (`--name`, `--kind`, `--target`,
12
+ `--deadline`, `--start`, `--start-amount n|none`, `--notes "..."|none`) and which accounts or wallets
13
+ count (`--account <id>,<id>` replaces the list, `--account none` unlinks all).
14
+ - `coinpay finances goals unarchive <id>`. Archived goals are hidden from `goals list` (unless
15
+ `--archived`) and left out of the report and the goals email.
16
+ - `goals show` prints what the progress is made of: every linked account with its amount, flagged when
17
+ its balance is over a week old. `FinanceGoal.sources` / `GoalSource` in the types.
18
+ - MCP: `finance_goal_update` (edit, relink, archive/unarchive) and `finance_goal_delete`.
19
+
8
20
  ## [0.27.1] - 2026-10-28
9
21
 
10
22
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "description": "CoinPay SDK & CLI — 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",
@@ -968,7 +968,7 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
968
968
  case 'goals':
969
969
  case 'goal': {
970
970
  const action = args[0] || 'list';
971
- const usage = 'Usage: coinpay finances goals [list|show <id>|create --name "Down payment" --kind house|car|emergency_fund|debt_payoff|custom --target 60000 --deadline 2031-06-30 [--start-amount 5000] [--account <id>,<id>] [--wallet <id>]|contribute <id> <amount> [--date 2026-10-01] [--note "..."]|report [--period daily|weekly|monthly|quarterly]|digest [show|on --cadence monthly [--hour 8]|off|send-now]|sources|archive <id>|delete <id> --yes]';
971
+ const usage = 'Usage: coinpay finances goals [list|show <id>|create --name "Down payment" --kind house|car|emergency_fund|debt_payoff|custom --target 60000 --deadline 2031-06-30 [--start-amount 5000] [--account <id>,<id>] [--wallet <id>]|contribute <id> <amount> [--date 2026-10-01] [--note "..."]|report [--period daily|weekly|monthly|quarterly]|digest [show|on --cadence monthly [--hour 8]|off|send-now]|edit <id> [--name ..] [--target ..] [--deadline ..] [--account <id>,<id>|none] ...|sources|archive <id>|unarchive <id>|delete <id> --yes]';
972
972
  const money = (cur, n) => new Intl.NumberFormat('en-US', { style: 'currency', currency: cur || 'USD', maximumFractionDigits: 2 }).format(n);
973
973
  const STATUS = { achieved: 'achieved', on_track: 'on track', behind: 'BEHIND', overdue: 'PAST DEADLINE' };
974
974
  const line = (g) => {
@@ -1000,6 +1000,10 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
1000
1000
  ` To finish on time: ${money(g.currency, s.required.perDay)}/day · ${money(g.currency, s.required.perWeek)}/week · ${money(g.currency, s.required.perMonth)}/month · ${money(g.currency, s.required.perQuarter)}/quarter`,
1001
1001
  s.recentPerMonth !== null ? ` Last 90 days: ${money(g.currency, s.recentPerMonth)}/month${s.projectedDate ? `, lands ${s.projectedDate}` : ''}` : null,
1002
1002
  data.contributions.length ? ` Contributions: ${data.contributions.slice(0, 10).map((c) => `${c.occurredOn} ${money(g.currency, c.amount)}`).join(', ')}` : null,
1003
+ g.archivedAt ? ` Archived ${g.archivedAt.slice(0, 10)} (unarchive: coinpay finances goals unarchive ${g.id.slice(0, 8)})` : null,
1004
+ ...(g.sources?.length
1005
+ ? [' Made of:', ...g.sources.map((src) => ` ${(src.counted ? money(g.currency, src.amount) : 'not counted').padStart(12)} ${src.institution && src.type === 'account' ? `${src.institution} · ` : ''}${src.name}${src.id ? ` [${src.id.slice(0, 8)}]` : ''}${src.stale && src.asOf ? ` (balance from ${src.asOf.slice(0, 10)})` : ''}`)]
1006
+ : []),
1003
1007
  ].filter(Boolean).join('\n');
1004
1008
  emit(data, text);
1005
1009
  return EXIT.OK;
@@ -1040,7 +1044,7 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
1040
1044
  const data = await api.getGoalSources(client);
1041
1045
  emit(data, [
1042
1046
  'Accounts:',
1043
- ...data.accounts.map((a) => ` ${a.id} ${(a.institution ? `${a.institution} · ` : '') + a.name}${a.liability ? ' (debt)' : ''}${a.balance !== null ? ` ${money(a.currency, a.balance)}` : ''}`),
1047
+ ...data.accounts.map((a) => ` ${a.id} ${(a.institution ? `${a.institution} · ` : '') + a.name}${a.liability ? ' (debt)' : ''}${a.hidden ? ' (hidden)' : ''}${a.balance !== null ? ` ${money(a.currency, a.balance)}` : ''}`),
1044
1048
  'Wallets:',
1045
1049
  ...(data.wallets.length ? data.wallets.map((w) => ` ${w.walletId} ${w.label ?? ''}`) : [' none']),
1046
1050
  ].join('\n'));
@@ -1070,9 +1074,31 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
1070
1074
  }
1071
1075
  throw new CliExit(EXIT.INVALID, usage);
1072
1076
  }
1073
- if (action === 'archive') {
1074
- const g = await api.updateGoal(client, await resolveId(args[1]), { archived: true });
1075
- emit({ goal: g }, `Archived ${g.name}.`);
1077
+ if (action === 'edit' || action === 'update') {
1078
+ const id = await resolveId(args[1]);
1079
+ const idsFlag = (v) => (v === 'none' ? [] : listFlag(v));
1080
+ const patch = {
1081
+ name: typeof flags.name === 'string' ? flags.name : undefined,
1082
+ kind: typeof flags.kind === 'string' ? flags.kind : undefined,
1083
+ targetAmount: flags.target !== undefined ? String(flags.target) : undefined,
1084
+ deadline: typeof flags.deadline === 'string' ? flags.deadline : undefined,
1085
+ startDate: typeof flags.start === 'string' ? flags.start : undefined,
1086
+ currency: typeof flags.currency === 'string' ? flags.currency : undefined,
1087
+ startingAmount: flags['start-amount'] === 'none' ? null : flags['start-amount'] !== undefined ? String(flags['start-amount']) : undefined,
1088
+ linkedAccountIds: flags.account ? idsFlag(flags.account) : undefined,
1089
+ linkedWalletIds: flags.wallet ? idsFlag(flags.wallet) : undefined,
1090
+ notes: flags.notes === 'none' ? null : typeof flags.notes === 'string' ? flags.notes : undefined,
1091
+ };
1092
+ for (const k of Object.keys(patch)) if (patch[k] === undefined) delete patch[k];
1093
+ if (Object.keys(patch).length === 0) throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances goals edit <id> [--name ..] [--kind ..] [--target ..] [--deadline ..] [--start YYYY-MM-DD] [--start-amount n|none] [--account <id>,<id>|none] [--wallet <id>|none] [--notes "..."|none]');
1094
+ const g = await api.updateGoal(client, id, patch);
1095
+ emit({ goal: g }, `Saved ${g.name}\n${g.snapshot ? line(g) : ''}`);
1096
+ return EXIT.OK;
1097
+ }
1098
+ if (action === 'archive' || action === 'unarchive') {
1099
+ const archived = action === 'archive';
1100
+ const g = await api.updateGoal(client, await resolveId(args[1]), { archived });
1101
+ emit({ goal: g }, archived ? `Archived ${g.name}. It is hidden from goals list and left out of reports (list --archived shows it).` : `${g.name} is active again.`);
1076
1102
  return EXIT.OK;
1077
1103
  }
1078
1104
  if (action === 'delete') {
@@ -494,8 +494,22 @@ export interface FinanceGoal {
494
494
  linkedAccountIds: string[];
495
495
  linkedWalletIds: string[];
496
496
  notes: string | null;
497
+ archivedAt: string | null;
497
498
  direction: 'save' | 'payoff';
498
499
  snapshot: GoalSnapshot;
500
+ /** What progress is made of; counted amounts sum to `snapshot.progress`. */
501
+ sources: GoalSource[];
502
+ }
503
+ export interface GoalSource {
504
+ type: 'account' | 'wallets' | 'contributions' | 'starting';
505
+ id: string | null;
506
+ name: string;
507
+ institution: string | null;
508
+ kind: string | null;
509
+ amount: number;
510
+ asOf: string | null;
511
+ stale: boolean;
512
+ counted: boolean;
499
513
  }
500
514
  export interface GoalInput {
501
515
  name: string;
package/src/mcp.js CHANGED
@@ -107,6 +107,36 @@ export const TOOLS = [
107
107
  },
108
108
  handler: async (args, { client }) => text({ goal: await reports.createGoal(client, args) }),
109
109
  },
110
+ {
111
+ name: 'finance_goal_update',
112
+ description: 'Edit a goal (any field), choose which linked accounts/wallets count (the full list replaces the old one; [] unlinks all), or archive/unarchive it. Archived goals are hidden from finance_goals unless archived=true and left out of reports and the goals email. Each goal\'s `sources` lists what its progress is made of.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ goalId: { type: 'string' },
117
+ name: { type: 'string' },
118
+ kind: { type: 'string', enum: ['house', 'car', 'emergency_fund', 'debt_payoff', 'custom'] },
119
+ targetAmount: { type: ['number', 'string'] },
120
+ deadline: { type: 'string', description: 'YYYY-MM-DD' },
121
+ startDate: { type: 'string', description: 'YYYY-MM-DD' },
122
+ currency: { type: 'string' },
123
+ startingAmount: { type: ['number', 'string', 'null'] },
124
+ linkedAccountIds: { type: 'array', items: { type: 'string' } },
125
+ linkedWalletIds: { type: 'array', items: { type: 'string' } },
126
+ notes: { type: ['string', 'null'] },
127
+ archived: { type: 'boolean' },
128
+ },
129
+ required: ['goalId'],
130
+ additionalProperties: false,
131
+ },
132
+ handler: async ({ goalId, ...patch }, { client }) => text({ goal: await reports.updateGoal(client, goalId, patch) }),
133
+ },
134
+ {
135
+ name: 'finance_goal_delete',
136
+ description: 'Delete a goal and its recorded contributions for good. Prefer finance_goal_update with archived=true to keep it.',
137
+ inputSchema: { type: 'object', properties: { goalId: { type: 'string' } }, required: ['goalId'], additionalProperties: false },
138
+ handler: async (args, { client }) => text(await reports.deleteGoal(client, args.goalId)),
139
+ },
110
140
  {
111
141
  name: 'finance_goal_contribute',
112
142
  description: 'Record an amount put toward a goal, or a payment on a payoff goal (negative = withdrawal). Records only; moves no money.',