earnrate 0.2.15 → 0.2.17
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/README.md +14 -8
- package/data/CHANGELOG.md +14 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-business-platinum.yaml +15 -1
- package/data/cards/barclays-jetblue-plus.yaml +19 -1
- package/data/cards/chase-freedom-flex.yaml +7 -7
- package/data/cards/chase-ihg-one-rewards-business.yaml +19 -3
- package/data/cards/chase-ihg-one-rewards-premier-select.yaml +5 -1
- package/data/cards/chase-ihg-one-rewards-premier.yaml +5 -1
- package/data/cards/chase-ihg-one-rewards.yaml +13 -2
- package/data/cards/chase-ink-business-cash.yaml +5 -1
- package/data/cards/chase-ink-business-unlimited.yaml +5 -1
- package/data/cards/chase-prime-visa.yaml +3 -3
- package/data/cards/chase-sapphire-reserve.yaml +12 -3
- package/data/cards/chase-united-business.yaml +5 -1
- package/data/cards/chase-united-club-business.yaml +5 -1
- package/data/cards/chase-united-club.yaml +5 -1
- package/data/cards/chase-united-explorer.yaml +5 -1
- package/data/cards/chase-united-quest.yaml +5 -1
- package/data/cards/citi-strata-premier.yaml +14 -1
- package/data/cards/wells-fargo-autograph-journey.yaml +16 -1
- package/data/merchants/overrides.yaml +3 -3
- package/dist/cli/args.js +36 -1
- package/dist/cli/run.js +14 -1
- package/dist/cli/wallet.js +18 -8
- package/dist/data/integrity.js +13 -0
- package/dist/data/wallet-edit.js +5 -0
- package/dist/engine/cheat-sheet.js +1 -1
- package/dist/engine/checklist.js +26 -6
- package/dist/engine/credits.js +56 -7
- package/dist/engine/evaluate.js +21 -1
- package/dist/engine/explore.js +32 -10
- package/dist/engine/memberships.js +22 -10
- package/dist/engine/milestones.js +11 -3
- package/dist/engine/offers.js +9 -4
- package/dist/engine/recommend.js +21 -2
- package/dist/engine/rule-text.js +20 -7
- package/dist/engine/setup.js +7 -6
- package/dist/engine/simulate.js +43 -10
- package/dist/engine/text.js +46 -5
- package/dist/engine/wallet-change.js +18 -7
- package/dist/engine/wallet-summary.js +45 -24
- package/dist/format/card-text.js +6 -12
- package/dist/format/explore.js +12 -8
- package/dist/format/offers.js +1 -1
- package/dist/format/wallet.js +2 -0
- package/dist/mcp/inputs.js +17 -1
- package/dist/mcp/schemas.js +24 -0
- package/dist/mcp/tools/account.js +50 -42
- package/dist/mcp/tools/wallet.js +207 -150
- package/dist/mcp/wallet-api.js +78 -0
- package/dist/schema/card.js +11 -0
- package/dist/schema/wallet.js +6 -2
- package/package.json +1 -1
package/dist/format/explore.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { membershipName, membershipNote, } from "../engine/memberships.js";
|
|
2
|
-
import { capitalize, choiceName, CREDIT_GROUP,
|
|
2
|
+
import { capitalize, choiceName, CREDIT_GROUP, CREDITS_APART, ENROLL_FIRST, HELD_CREDITS_COUNTED, hostname, feeText, dollarsAndCents, pct, wholeDollars, words, listed, } from "../engine/text.js";
|
|
3
3
|
const membershipFees = (memberships) => memberships
|
|
4
4
|
.map((m) => ` + ${wholeDollars(m.annualFee)} for ${m.name}`)
|
|
5
5
|
.join('');
|
|
6
6
|
const TOP_BEST = 10;
|
|
7
|
+
// " · $X if you'd use all its credits ($424 a year)"
|
|
8
|
+
const ifCredits = (credits, net, sign = '', its = 'its') => credits > 0
|
|
9
|
+
? ` · ${sign}${wholeDollars(net)} if you'd use all ${its} credits (${wholeDollars(credits)} a year)`
|
|
10
|
+
: '';
|
|
7
11
|
export function formatExploration(r, catalog, all) {
|
|
8
12
|
const m = r.merchant;
|
|
9
13
|
const title = m.source === 'category'
|
|
@@ -83,15 +87,15 @@ export function formatCatalogSimulation(r, catalog, all) {
|
|
|
83
87
|
const card = s.cards[0];
|
|
84
88
|
if (!card)
|
|
85
89
|
return;
|
|
86
|
-
lines.push(`${String(i + 1).padStart(2)}. ${name(card.cardId)} ${wholeDollars(s.net)} (${wholeDollars(s.rewards)} rewards, ${wholeDollars(s.annualFees)} fee${membershipFees(s.memberships)})${picksText(card.picks)}`);
|
|
90
|
+
lines.push(`${String(i + 1).padStart(2)}. ${name(card.cardId)} ${wholeDollars(s.net)} (${wholeDollars(s.rewards)} rewards, ${wholeDollars(s.annualFees)} fee${membershipFees(s.memberships)})${ifCredits(s.otherCredits, s.netIfCreditsUsed)}${picksText(card.picks)}`);
|
|
87
91
|
});
|
|
88
92
|
const pairs = r.pairs.slice(0, top);
|
|
89
93
|
if (pairs.length > 0) {
|
|
90
94
|
lines.push('', 'Best two-card pairs:');
|
|
91
95
|
pairs.forEach((s, i) => {
|
|
92
|
-
lines.push(`${String(i + 1).padStart(2)}. ${s.cards.map((c) => `${name(c.cardId)}${picksText(c.picks)}`).join(' + ')} ${wholeDollars(s.net)}`);
|
|
96
|
+
lines.push(`${String(i + 1).padStart(2)}. ${s.cards.map((c) => `${name(c.cardId)}${picksText(c.picks)}`).join(' + ')} ${wholeDollars(s.net)}${ifCredits(s.otherCredits, s.netIfCreditsUsed, '', 'their')}`);
|
|
93
97
|
if (s.alternatives.length > 0)
|
|
94
|
-
lines.push(` Same result with ${s.alternatives.map(name)
|
|
98
|
+
lines.push(` Same result with ${listed(s.alternatives.map(name), 'or')} as the second card.`);
|
|
95
99
|
for (const split of splitText(s, name))
|
|
96
100
|
lines.push(` ${split}`);
|
|
97
101
|
});
|
|
@@ -105,13 +109,13 @@ export function formatCatalogSimulation(r, catalog, all) {
|
|
|
105
109
|
]);
|
|
106
110
|
if (notes.size > 0)
|
|
107
111
|
lines.push('', ...notes);
|
|
108
|
-
lines.push('', `Assumes each card's best picks, activations done each quarter, and no welcome bonuses. ${
|
|
112
|
+
lines.push('', `Assumes each card's best picks, activations done each quarter, and no welcome bonuses. ${CREDITS_APART}`);
|
|
109
113
|
return `${lines.join('\n')}\n`;
|
|
110
114
|
}
|
|
111
115
|
export function formatAdditions(r, catalog, all) {
|
|
112
116
|
const name = cardNamer(catalog);
|
|
113
117
|
const lines = [
|
|
114
|
-
`Your wallet earns about ${wholeDollars(r.current.rewards)} a year on this spending${r.current.annualFees > 0 ? `, with ${wholeDollars(r.current.annualFees)} in annual fees: ${wholeDollars(r.current.net)} after fees` : ''}.`,
|
|
118
|
+
`Your wallet earns about ${wholeDollars(r.current.rewards)} a year on this spending${r.current.annualFees > 0 ? `, with ${wholeDollars(r.current.annualFees)} in annual fees${r.current.heldCredits > 0 ? ` and ${wholeDollars(r.current.heldCredits)} of credits you use` : ''}: ${wholeDollars(r.current.net)} after fees` : ''}.`,
|
|
115
119
|
];
|
|
116
120
|
for (const split of splitText(r.current, name))
|
|
117
121
|
lines.push(` ${split}`);
|
|
@@ -122,7 +126,7 @@ export function formatAdditions(r, catalog, all) {
|
|
|
122
126
|
else {
|
|
123
127
|
lines.push('', 'Cards that would add the most (after their annual fee and any membership they need):');
|
|
124
128
|
gains.slice(0, all ? 20 : TOP_SIMULATED).forEach((a, i) => {
|
|
125
|
-
lines.push(`${String(i + 1).padStart(2)}. ${name(a.cardId)} +${wholeDollars(a.gain)} a year (+${wholeDollars(a.extraRewards)} rewards, ${wholeDollars(a.annualFee)} fee${membershipFees(a.simulation.memberships.filter((m) => m.cardIds.includes(a.cardId)))})${picksText(a.picks)}`);
|
|
129
|
+
lines.push(`${String(i + 1).padStart(2)}. ${name(a.cardId)} +${wholeDollars(a.gain)} a year (+${wholeDollars(a.extraRewards)} rewards, ${wholeDollars(a.annualFee)} fee${membershipFees(a.simulation.memberships.filter((m) => m.cardIds.includes(a.cardId)))})${ifCredits(a.credits, a.gainIfCreditsUsed, '+')}${picksText(a.picks)}`);
|
|
126
130
|
});
|
|
127
131
|
}
|
|
128
132
|
const notes = new Set([
|
|
@@ -133,6 +137,6 @@ export function formatAdditions(r, catalog, all) {
|
|
|
133
137
|
]);
|
|
134
138
|
if (notes.size > 0)
|
|
135
139
|
lines.push('', ...notes);
|
|
136
|
-
lines.push('', `Assumes your picks carry over, rotating categories get activated, and caps start fresh.
|
|
140
|
+
lines.push('', `Assumes your picks carry over, rotating categories get activated, and caps start fresh. ${r.current.heldCredits > 0 ? `${HELD_CREDITS_COUNTED} ` : ''}${CREDITS_APART} Perks like lounge access aren't counted. Each kind of spending lists the card that earns the most on it over the whole year, so a one-quarter bonus may not show.`);
|
|
137
141
|
return `${lines.join('\n')}\n`;
|
|
138
142
|
}
|
package/dist/format/offers.js
CHANGED
|
@@ -11,7 +11,7 @@ export function formatOffers(c, currencyName, all) {
|
|
|
11
11
|
shown.forEach((o, i) => {
|
|
12
12
|
const weeks = (o.months * 52) / 12;
|
|
13
13
|
const pace = o.spend > 0 ? ` (about ${wholeDollars(o.spend / weeks)} a week)` : '';
|
|
14
|
-
lines.push(`${String(i + 1).padStart(2)}. ${o.cardName.padEnd(width)} about ${wholeDollars(o.firstYearValue)} the first year`, ` ${offerTerms(o, currencyName(o.currency))}${pace}. ${capitalize(feeText(o))}.${o.membershipFee > 0 ? ` Needs a ${money(o.membershipFee)} membership, counted in the first year.` : ''}${o.held ? ' In your wallet.' : ''}`);
|
|
14
|
+
lines.push(`${String(i + 1).padStart(2)}. ${o.cardName.padEnd(width)} about ${wholeDollars(o.firstYearValue)} the first year${o.credits > 0 ? `, ${wholeDollars(o.firstYearValueIfCreditsUsed)} if you'd use all its credits (${wholeDollars(o.credits)} a year)` : ''}`, ` ${offerTerms(o, currencyName(o.currency))}${pace}. ${capitalize(feeText(o))}.${o.membershipFee > 0 ? ` Needs a ${money(o.membershipFee)} membership, counted in the first year.` : ''}${o.held ? ' In your wallet.' : ''}`);
|
|
15
15
|
lines.push(` ${o.validTo ? `Offer ends ${shortDate(o.validTo)}. ` : ''}Public offer as of ${shortDate(o.lastVerified)}; yours may differ.`);
|
|
16
16
|
if (o.ineligible)
|
|
17
17
|
lines.push(` ${o.ineligible}`);
|
package/dist/format/wallet.js
CHANGED
|
@@ -41,6 +41,8 @@ export function formatWalletSummary(summary) {
|
|
|
41
41
|
lines.push(` Spend bonus (${m.id}): ${m.text}`);
|
|
42
42
|
for (const credit of c.credits)
|
|
43
43
|
lines.push(` Credit (${credit.id}): ${credit.text}`);
|
|
44
|
+
if (c.usedCreditsText !== null)
|
|
45
|
+
lines.push(` ${c.usedCreditsText}`);
|
|
44
46
|
if (c.anniversary !== null)
|
|
45
47
|
lines.push(` Account anniversary: ${c.anniversary}.`);
|
|
46
48
|
if (c.intro?.state === 'active' && c.intro.until !== null)
|
package/dist/mcp/inputs.js
CHANGED
|
@@ -49,6 +49,18 @@ const NamedCard = z.object({
|
|
|
49
49
|
.array(z.string())
|
|
50
50
|
.optional()
|
|
51
51
|
.describe('Bonuses the user has activated with the issuer, as rule ids from search_cards.'),
|
|
52
|
+
enrolled: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Statement credits the user has enrolled in with the issuer, as credit ids from search_cards. A credit that needs enrolling counts only when it's here. Ask; never assume."),
|
|
56
|
+
skippedCredits: z
|
|
57
|
+
.array(z.string())
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Statement credit ids the user says they don't use. They don't count toward a pick or against the card's annual fee."),
|
|
60
|
+
opened: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('The day the account opened (YYYY-MM-DD), for a card with a first-year rate. Optional; without it the card is ranked at its standard rates.'),
|
|
52
64
|
});
|
|
53
65
|
const cardFields = {
|
|
54
66
|
checkoutLabel: z
|
|
@@ -84,11 +96,15 @@ const cardFields = {
|
|
|
84
96
|
enrolled: z
|
|
85
97
|
.array(z.string())
|
|
86
98
|
.optional()
|
|
87
|
-
.describe(
|
|
99
|
+
.describe("Statement credit ids the user has enrolled in with the issuer, or, for a credit that needs no enrolling, says they use (search_cards and list_wallet list each card's credits, e.g. \"lululemon\" on Amex Platinum). A credit that needs enrolling counts in answers only once it's here, and any credit counts against the annual fee only once it's here (or has been used). Replaces the current list. Ask; never assume."),
|
|
88
100
|
creditUsed: z
|
|
89
101
|
.record(z.string(), z.number().nonnegative())
|
|
90
102
|
.optional()
|
|
91
103
|
.describe('Dollars of a statement credit already received this period: credit id → dollars, e.g. {"lululemon": 40}. Ask; recorded purchases add to it from then on.'),
|
|
104
|
+
skippedCredits: z
|
|
105
|
+
.array(z.string())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Statement credit ids the user doesn't use, e.g. [\"dunkin\"] on Amex Gold. They're left out of picks, the to-do list and the annual-fee math; every other credit on the card counts against its fee once it's in enrolled (or, for one that needs no enrolling, has been used). Replaces the current list; [] counts them all again."),
|
|
92
108
|
bonus: z
|
|
93
109
|
.object({
|
|
94
110
|
opened: IsoDate.describe('The day the account opened, YYYY-MM-DD.'),
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -179,6 +179,8 @@ export const WalletSummarySchema = z.object({
|
|
|
179
179
|
period: CreditPeriod,
|
|
180
180
|
enroll: z.boolean(),
|
|
181
181
|
enrolled: z.boolean(),
|
|
182
|
+
skipped: z.boolean(), // the user doesn't use it: not counted anywhere
|
|
183
|
+
confirmed: z.boolean(), // said in use: counts against the annual fee
|
|
182
184
|
currentPeriod: z.string().nullable(),
|
|
183
185
|
until: z.string().nullable(),
|
|
184
186
|
used: z.number(),
|
|
@@ -186,6 +188,8 @@ export const WalletSummarySchema = z.object({
|
|
|
186
188
|
tracked: z.boolean(),
|
|
187
189
|
text: z.string(),
|
|
188
190
|
})),
|
|
191
|
+
usedCredits: z.number(), // a year, counted against the annual fee
|
|
192
|
+
usedCreditsText: z.string().nullable(),
|
|
189
193
|
chosen: z
|
|
190
194
|
.object({
|
|
191
195
|
period: z.string(),
|
|
@@ -282,6 +286,10 @@ export const OfferComparisonSchema = z.object({
|
|
|
282
286
|
ineligible: z.string().nullable(),
|
|
283
287
|
bonusValue: z.number(),
|
|
284
288
|
firstYearValue: z.number(),
|
|
289
|
+
// Statement credits, a year at face value, and the first-year value
|
|
290
|
+
// if all were used. Not ranked on.
|
|
291
|
+
credits: z.number(),
|
|
292
|
+
firstYearValueIfCreditsUsed: z.number(),
|
|
285
293
|
held: z.boolean(),
|
|
286
294
|
lastVerified: z.string(),
|
|
287
295
|
verifiedBy: z.string(),
|
|
@@ -381,6 +389,7 @@ export const ExplorationSchema = z.object({
|
|
|
381
389
|
enroll: z.boolean(),
|
|
382
390
|
note: z.string().nullable(),
|
|
383
391
|
back: z.string(),
|
|
392
|
+
terms: z.string(),
|
|
384
393
|
earn: z.string(),
|
|
385
394
|
text: z.string(),
|
|
386
395
|
evidence: z.object({ quote: z.string(), url: z.string() }).nullable(),
|
|
@@ -413,6 +422,7 @@ export const ExplorationSchema = z.object({
|
|
|
413
422
|
conditions: z.array(z.string()),
|
|
414
423
|
evidence: z.object({ quote: z.string(), url: z.string() }).nullable(),
|
|
415
424
|
withMembership: z.array(z.object({ membership: z.string(), effectiveReturnPct: z.number() })),
|
|
425
|
+
needsMembership: z.string().nullable(),
|
|
416
426
|
})),
|
|
417
427
|
excluded: z.array(ExcludedCardSchema),
|
|
418
428
|
assumptions: z.array(z.string()),
|
|
@@ -430,7 +440,19 @@ const SimulationSchema = z.object({
|
|
|
430
440
|
cardIds: z.array(z.string()),
|
|
431
441
|
})),
|
|
432
442
|
membershipFees: z.number(),
|
|
443
|
+
// Credits the user uses on cards they hold, a year: counted in net.
|
|
444
|
+
heldCredits: z.number(),
|
|
433
445
|
net: z.number(),
|
|
446
|
+
// Credits on cards not held, a year at face value: not in net.
|
|
447
|
+
otherCredits: z.number(),
|
|
448
|
+
netIfCreditsUsed: z.number(),
|
|
449
|
+
credits: z.array(z.object({
|
|
450
|
+
cardId: z.string(),
|
|
451
|
+
yearly: z.number(),
|
|
452
|
+
creditIds: z.array(z.string()),
|
|
453
|
+
names: z.array(z.string()),
|
|
454
|
+
held: z.boolean(),
|
|
455
|
+
})),
|
|
434
456
|
lines: z.array(z.object({
|
|
435
457
|
label: z.string(),
|
|
436
458
|
spend: z.number(),
|
|
@@ -458,6 +480,8 @@ export const SpendingSimulationSchema = z.object({
|
|
|
458
480
|
annualFee: z.number(),
|
|
459
481
|
membershipFee: z.number(),
|
|
460
482
|
gain: z.number(),
|
|
483
|
+
credits: z.number(), // its statement credits, a year: not in gain
|
|
484
|
+
gainIfCreditsUsed: z.number(),
|
|
461
485
|
})),
|
|
462
486
|
});
|
|
463
487
|
export const CheatSheetSchema = z.object({
|
|
@@ -5,22 +5,59 @@ import { listed } from "../../engine/text.js";
|
|
|
5
5
|
import { savedSettingsText } from "../../engine/wallet-summary.js";
|
|
6
6
|
import { fail } from "../context.js";
|
|
7
7
|
import { WalletChangeSchema } from "../schemas.js";
|
|
8
|
+
const cardsText = (n) => `${String(n)} ${n === 1 ? 'card' : 'cards'}`;
|
|
9
|
+
// Everything a saved wallet holds, in words; empty when there's nothing.
|
|
10
|
+
const contentsOf = (catalog, w) => [
|
|
11
|
+
...(w.cards.length > 0
|
|
12
|
+
? [`${cardsText(w.cards.length)} (${w.cards.map((c) => c.id).join(', ')})`]
|
|
13
|
+
: []),
|
|
14
|
+
...savedSettingsText(catalog, w),
|
|
15
|
+
];
|
|
16
|
+
const ImportInput = z.object({
|
|
17
|
+
yaml: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(100_000)
|
|
21
|
+
.describe('The wallet.yaml text, as the user gave it.'),
|
|
22
|
+
replace: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('true to replace what the wallet already holds, after the user confirms.'),
|
|
26
|
+
});
|
|
27
|
+
export function importHandler(catalog, store, ctx) {
|
|
28
|
+
const contents = (w) => contentsOf(catalog, w);
|
|
29
|
+
return {
|
|
30
|
+
input: ImportInput,
|
|
31
|
+
run: async ({ yaml, replace }) => {
|
|
32
|
+
const parsed = parseWalletYaml(yaml, catalog);
|
|
33
|
+
if (typeof parsed === 'string')
|
|
34
|
+
return fail(parsed);
|
|
35
|
+
let current;
|
|
36
|
+
try {
|
|
37
|
+
current = await store.load();
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (replace !== true)
|
|
41
|
+
return fail(`Couldn't read the saved wallet to check what it holds (${e instanceof Error ? e.message : String(e)}). Ask the user, then call again with replace: true to overwrite it.`);
|
|
42
|
+
current = null;
|
|
43
|
+
}
|
|
44
|
+
const before = current === null ? [] : contents(current);
|
|
45
|
+
if (before.length > 0 && replace !== true)
|
|
46
|
+
return fail(`This would replace what's saved: ${listed(before)}. Ask the user, then call again with replace: true.`);
|
|
47
|
+
return ctx.changed(await store.replace(parsed), before.length > 0
|
|
48
|
+
? `Replaced what was saved (${listed(before)}) with the imported wallet: ${listed(contents(parsed))}.`
|
|
49
|
+
: `Imported a wallet with ${listed(contents(parsed)) || 'nothing in it'}.`, { notes: false });
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
8
53
|
// A saved wallet on the hosted server has no file to copy or delete, so
|
|
9
54
|
// these do it.
|
|
10
55
|
export function registerAccountTools(server, catalog, store, ctx) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const contents = (w) => [
|
|
14
|
-
...(w.cards.length > 0
|
|
15
|
-
? [
|
|
16
|
-
`${cardsText(w.cards.length)} (${w.cards.map((c) => c.id).join(', ')})`,
|
|
17
|
-
]
|
|
18
|
-
: []),
|
|
19
|
-
...savedSettingsText(catalog, w),
|
|
20
|
-
];
|
|
56
|
+
const contents = (w) => contentsOf(catalog, w);
|
|
57
|
+
const importWallet = importHandler(catalog, store, ctx);
|
|
21
58
|
server.registerTool('export_wallet', {
|
|
22
59
|
title: 'Export the wallet',
|
|
23
|
-
description: 'The saved wallet as a wallet.yaml file: card ids, checkout labels, picks, activations, opening dates and anniversaries, memberships, point values, cap usage, welcome-bonus progress, yearly spending toward spend bonuses, and statement credits enrolled in and used. earnrate.dev imports it, and the earnrate command line reads it saved as ~/.earnrate/wallet.yaml. Never card numbers.',
|
|
60
|
+
description: 'The saved wallet as a wallet.yaml file: card ids, checkout labels, picks, activations, opening dates and anniversaries, memberships, point values, cap usage, welcome-bonus progress, yearly spending toward spend bonuses, and statement credits enrolled in, used and not used. earnrate.dev imports it, and the earnrate command line reads it saved as ~/.earnrate/wallet.yaml. Never card numbers.',
|
|
24
61
|
inputSchema: z.object({}),
|
|
25
62
|
outputSchema: z.object({ yaml: z.string() }),
|
|
26
63
|
annotations: {
|
|
@@ -42,17 +79,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
42
79
|
server.registerTool('import_wallet', {
|
|
43
80
|
title: 'Import a wallet',
|
|
44
81
|
description: 'Replaces the saved wallet with a wallet.yaml from the earnrate command line or earnrate.dev, when the user pastes or attaches one. Wallet files only: never statements or card numbers. If anything is already saved (cards, memberships or point values), it refuses unless replace is true: confirm with the user first.',
|
|
45
|
-
inputSchema:
|
|
46
|
-
yaml: z
|
|
47
|
-
.string()
|
|
48
|
-
.min(1)
|
|
49
|
-
.max(100_000)
|
|
50
|
-
.describe('The wallet.yaml text, as the user gave it.'),
|
|
51
|
-
replace: z
|
|
52
|
-
.boolean()
|
|
53
|
-
.optional()
|
|
54
|
-
.describe('true to replace what the wallet already holds, after the user confirms.'),
|
|
55
|
-
}),
|
|
82
|
+
inputSchema: importWallet.input,
|
|
56
83
|
outputSchema: WalletChangeSchema,
|
|
57
84
|
annotations: {
|
|
58
85
|
title: 'Import a wallet',
|
|
@@ -61,26 +88,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
61
88
|
idempotentHint: true,
|
|
62
89
|
openWorldHint: false,
|
|
63
90
|
},
|
|
64
|
-
},
|
|
65
|
-
const parsed = parseWalletYaml(yaml, catalog);
|
|
66
|
-
if (typeof parsed === 'string')
|
|
67
|
-
return fail(parsed);
|
|
68
|
-
let current;
|
|
69
|
-
try {
|
|
70
|
-
current = await store.load();
|
|
71
|
-
}
|
|
72
|
-
catch (e) {
|
|
73
|
-
if (replace !== true)
|
|
74
|
-
return fail(`Couldn't read the saved wallet to check what it holds (${e instanceof Error ? e.message : String(e)}). Ask the user, then call again with replace: true to overwrite it.`);
|
|
75
|
-
current = null;
|
|
76
|
-
}
|
|
77
|
-
const before = current === null ? [] : contents(current);
|
|
78
|
-
if (before.length > 0 && replace !== true)
|
|
79
|
-
return fail(`This would replace what's saved: ${listed(before)}. Ask the user, then call again with replace: true.`);
|
|
80
|
-
return ctx.changed(await store.replace(parsed), before.length > 0
|
|
81
|
-
? `Replaced what was saved (${listed(before)}) with the imported wallet: ${listed(contents(parsed))}.`
|
|
82
|
-
: `Imported a wallet with ${listed(contents(parsed)) || 'nothing in it'}.`, { notes: false });
|
|
83
|
-
});
|
|
91
|
+
}, (args) => importWallet.run(args));
|
|
84
92
|
server.registerTool('delete_wallet', {
|
|
85
93
|
title: 'Delete the wallet',
|
|
86
94
|
description: "Deletes the saved wallet and its recovery code now and for good, and signs out every app connected to it, this one included. earnrate.dev and the earnrate command line keep their own wallets and aren't affected. Only when the user asks to delete it: show it with list_wallet, offer export_wallet, and pass confirm, cards and saved only after the user confirms. It refuses if the wallet no longer holds what list_wallet showed, so a retried call can't delete a different wallet.",
|