earnrate 0.2.10 → 0.2.12
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 +31 -11
- package/data/CHANGELOG.md +154 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-blue-business-plus.yaml +1 -0
- package/data/cards/amex-blue-cash-everyday.yaml +1 -0
- package/data/cards/amex-blue-cash-preferred.yaml +7 -0
- package/data/cards/amex-business-gold.yaml +134 -0
- package/data/cards/amex-business-platinum.yaml +115 -0
- package/data/cards/amex-delta-skymiles-blue.yaml +91 -0
- package/data/cards/amex-delta-skymiles-gold.yaml +122 -0
- package/data/cards/amex-delta-skymiles-platinum.yaml +120 -0
- package/data/cards/amex-delta-skymiles-reserve.yaml +82 -0
- package/data/cards/amex-gold.yaml +1 -0
- package/data/cards/amex-hilton-honors-aspire.yaml +142 -0
- package/data/cards/amex-hilton-honors-business.yaml +93 -0
- package/data/cards/amex-hilton-honors-surpass.yaml +153 -0
- package/data/cards/amex-hilton-honors.yaml +119 -0
- package/data/cards/amex-marriott-bonvoy-bevy.yaml +118 -0
- package/data/cards/amex-marriott-bonvoy-brilliant.yaml +93 -0
- package/data/cards/amex-marriott-bonvoy-business.yaml +116 -0
- package/data/cards/amex-platinum.yaml +15 -0
- package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +14 -0
- package/data/cards/bank-of-america-atmos-rewards-business.yaml +15 -0
- package/data/cards/bank-of-america-atmos-rewards-summit.yaml +17 -0
- package/data/cards/bank-of-america-customized-cash-rewards.yaml +34 -1
- package/data/cards/bank-of-america-premium-rewards.yaml +134 -0
- package/data/cards/bank-of-america-travel-rewards.yaml +123 -0
- package/data/cards/bank-of-america-unlimited-cash-rewards.yaml +83 -0
- package/data/cards/barclays-jetblue-plus.yaml +86 -0
- package/data/cards/bilt-blue.yaml +60 -0
- package/data/cards/bilt-obsidian.yaml +1 -0
- package/data/cards/bilt-palladium.yaml +76 -0
- package/data/cards/capital-one-kohls-card.yaml +1 -0
- package/data/cards/capital-one-kohls-rewards-visa.yaml +1 -0
- package/data/cards/capital-one-quicksilver.yaml +82 -0
- package/data/cards/capital-one-savor.yaml +2 -0
- package/data/cards/capital-one-spark-cash-plus.yaml +80 -0
- package/data/cards/capital-one-venture-x-business.yaml +97 -0
- package/data/cards/capital-one-venture-x.yaml +2 -0
- package/data/cards/capital-one-venture.yaml +2 -0
- package/data/cards/chase-aeroplan.yaml +30 -0
- package/data/cards/chase-freedom-flex.yaml +1 -0
- package/data/cards/chase-freedom-rise.yaml +29 -0
- package/data/cards/chase-freedom-unlimited.yaml +1 -0
- package/data/cards/chase-ihg-one-rewards-business.yaml +1 -0
- package/data/cards/chase-ihg-one-rewards-premier-select.yaml +2 -0
- package/data/cards/chase-ihg-one-rewards-premier.yaml +1 -0
- package/data/cards/chase-ihg-one-rewards.yaml +1 -0
- package/data/cards/chase-ink-business-cash.yaml +1 -0
- package/data/cards/chase-ink-business-preferred.yaml +1 -0
- package/data/cards/chase-ink-business-unlimited.yaml +69 -0
- package/data/cards/chase-marriott-bonvoy-bold.yaml +124 -0
- package/data/cards/chase-marriott-bonvoy-boundless.yaml +78 -0
- package/data/cards/chase-prime-visa.yaml +1 -0
- package/data/cards/chase-sapphire-preferred.yaml +1 -0
- package/data/cards/chase-sapphire-reserve.yaml +30 -0
- package/data/cards/chase-southwest-performance-business.yaml +2 -0
- package/data/cards/chase-southwest-plus.yaml +2 -0
- package/data/cards/chase-southwest-premier-business.yaml +2 -0
- package/data/cards/chase-southwest-premier.yaml +2 -0
- package/data/cards/chase-southwest-priority.yaml +2 -0
- package/data/cards/chase-united-business.yaml +11 -0
- package/data/cards/chase-united-club-business.yaml +20 -0
- package/data/cards/chase-united-club.yaml +32 -0
- package/data/cards/chase-united-explorer.yaml +20 -0
- package/data/cards/chase-united-gateway.yaml +19 -0
- package/data/cards/chase-united-quest.yaml +20 -0
- package/data/cards/citi-aadvantage-business.yaml +14 -1
- package/data/cards/citi-aadvantage-executive.yaml +2 -0
- package/data/cards/citi-aadvantage-globe.yaml +2 -0
- package/data/cards/citi-aadvantage-mileup.yaml +2 -0
- package/data/cards/citi-aadvantage-platinum-select.yaml +14 -1
- package/data/cards/citi-costco-anywhere-visa-business.yaml +2 -0
- package/data/cards/citi-costco-anywhere-visa.yaml +2 -0
- package/data/cards/citi-custom-cash.yaml +1 -0
- package/data/cards/citi-double-cash.yaml +1 -0
- package/data/cards/citi-my-best-buy-visa.yaml +1 -0
- package/data/cards/citi-strata-elite.yaml +1 -0
- package/data/cards/citi-strata-premier.yaml +1 -0
- package/data/cards/citi-strata.yaml +164 -0
- package/data/cards/discover-it-cash-back.yaml +188 -0
- package/data/cards/paypal-cashback-mastercard.yaml +41 -0
- package/data/cards/sams-club-mastercard.yaml +68 -0
- package/data/cards/sofi-unlimited-2.yaml +1 -0
- package/data/cards/td-bank-nordstrom-credit-card.yaml +1 -0
- package/data/cards/td-bank-nordstrom-visa.yaml +1 -0
- package/data/cards/td-bank-target-circle-card.yaml +1 -0
- package/data/cards/usbank-altitude-connect.yaml +150 -0
- package/data/cards/usbank-altitude-go.yaml +118 -0
- package/data/cards/usbank-cash-plus.yaml +1 -0
- package/data/cards/wells-fargo-active-cash.yaml +1 -0
- package/data/cards/wells-fargo-autograph-journey.yaml +73 -0
- package/data/cards/wells-fargo-autograph.yaml +1 -0
- package/data/categories.yaml +3 -1
- package/data/currencies.yaml +60 -0
- package/data/issuers.yaml +25 -1
- package/data/memberships.yaml +10 -0
- package/data/merchants/overrides.yaml +515 -1
- package/data/merchants/seed.yaml +883 -6
- package/dist/cli/import.js +2 -2
- package/dist/cli/wallet.js +8 -1
- package/dist/data/integrity.js +37 -0
- package/dist/data/wallet-edit.js +3 -1
- package/dist/engine/cheat-sheet.js +3 -0
- package/dist/engine/checklist.js +14 -3
- package/dist/engine/evaluate.js +19 -7
- package/dist/engine/intro.js +36 -0
- package/dist/engine/recommend.js +41 -10
- package/dist/engine/record.js +2 -0
- package/dist/engine/rule-text.js +15 -1
- package/dist/engine/setup.js +6 -0
- package/dist/engine/text.js +23 -0
- package/dist/engine/wallet-change.js +20 -3
- package/dist/engine/wallet-summary.js +24 -4
- package/dist/format/card-art.js +21 -0
- package/dist/format/cards.js +1 -0
- package/dist/format/wallet-yaml.js +1 -0
- package/dist/format/wallet.js +10 -4
- package/dist/import/analyze.js +1 -0
- package/dist/mcp/inputs.js +3 -0
- package/dist/mcp/prompts.js +90 -0
- package/dist/mcp/schemas.js +17 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools/account.js +8 -5
- package/dist/mcp/tools/answers.js +7 -0
- package/dist/mcp/tools/wallet.js +7 -2
- package/dist/schema/card.js +23 -0
- package/dist/schema/issuer.js +4 -0
- package/dist/schema/wallet.js +3 -0
- package/package.json +12 -3
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { bonusStatus } from "./bonus.js";
|
|
2
2
|
import { activationGroup, capUsed, isActivated, pickedCategories, } from "./evaluate.js";
|
|
3
|
+
import { introUntil, introWindows, openedOn } from "./intro.js";
|
|
3
4
|
import { membershipName } from "./memberships.js";
|
|
4
5
|
import { milestoneSummaries } from "./milestones.js";
|
|
5
|
-
import { optionName, ruleParts
|
|
6
|
+
import { optionName, ruleParts } from "./rule-text.js";
|
|
6
7
|
import { choicePeriod, daysBetween, inEffect, periodEnd } from "./periods.js";
|
|
7
8
|
import { STALE_AFTER_DAYS } from "./recommend.js";
|
|
8
9
|
import { rotationGap } from "./rotation.js";
|
|
9
10
|
import { activationRules } from "./setup.js";
|
|
10
|
-
import { CAP_PERIODS, choiceName, currencyLabel, earnText, listed, shortDate, wholeDollars, words, } from "./text.js";
|
|
11
|
+
import { CAP_PERIODS, categoryReadsOn, choiceName, currencyLabel, earnText, listed, shortDate, wholeDollars, words, } from "./text.js";
|
|
11
12
|
import { cardValuation, currencyValuation } from "./valuation.js";
|
|
12
13
|
const matchKeys = (rule) => rule.match.choice !== undefined
|
|
13
14
|
? []
|
|
@@ -98,8 +99,13 @@ export function activationsDue(catalog, card, held, today) {
|
|
|
98
99
|
seen.add(g);
|
|
99
100
|
const rules = card.rules.filter((r) => group.includes(r.id));
|
|
100
101
|
const first = rules[0];
|
|
102
|
+
const categories = rules.flatMap((r) => r.match.categories ?? []);
|
|
103
|
+
const servicesOnly = categories.length > 0 &&
|
|
104
|
+
rules.every((r) => (r.match.merchants ?? []).length === 0) &&
|
|
105
|
+
categories.every((c) => categoryReadsOn(catalog, c));
|
|
101
106
|
out.push({
|
|
102
|
-
what: rules.
|
|
107
|
+
what: listed([...new Set(rules.flatMap((r) => ruleParts(catalog, r)))]),
|
|
108
|
+
preposition: servicesOnly ? 'on' : 'at',
|
|
103
109
|
from: first?.valid_from ?? null,
|
|
104
110
|
to: first?.valid_to ?? null,
|
|
105
111
|
activateBy: first?.activate_by ?? null,
|
|
@@ -144,7 +150,7 @@ function capSummaries(catalog, card, held, today) {
|
|
|
144
150
|
const shared = card.caps.find((c) => c.id === key);
|
|
145
151
|
const label = shared?.rotating
|
|
146
152
|
? 'the rotating bonus categories'
|
|
147
|
-
: [...new Set(covered.
|
|
153
|
+
: listed([...new Set(covered.flatMap((r) => ruleParts(catalog, r)))]) ||
|
|
148
154
|
words(key);
|
|
149
155
|
return {
|
|
150
156
|
key,
|
|
@@ -211,6 +217,8 @@ export function walletSummary(catalog, wallet, today) {
|
|
|
211
217
|
activated: held.activated,
|
|
212
218
|
toActivate: toActivate(catalog, card, held, today),
|
|
213
219
|
anniversary: held.anniversary ?? null,
|
|
220
|
+
opened: openedOn(held),
|
|
221
|
+
intro: introSummary(card, held, today),
|
|
214
222
|
});
|
|
215
223
|
}
|
|
216
224
|
return {
|
|
@@ -221,6 +229,18 @@ export function walletSummary(catalog, wallet, today) {
|
|
|
221
229
|
cards,
|
|
222
230
|
};
|
|
223
231
|
}
|
|
232
|
+
function introSummary(card, held, today) {
|
|
233
|
+
const [first] = card.intro_rates;
|
|
234
|
+
if (!first)
|
|
235
|
+
return null;
|
|
236
|
+
const opened = openedOn(held);
|
|
237
|
+
if (opened === null)
|
|
238
|
+
return { state: 'no-date', until: null, days: first.days };
|
|
239
|
+
const [active] = introWindows(card, held, today);
|
|
240
|
+
if (active)
|
|
241
|
+
return { state: 'active', until: active.until, days: active.intro.days };
|
|
242
|
+
return { state: 'ended', until: introUntil(first, opened), days: first.days };
|
|
243
|
+
}
|
|
224
244
|
function pointValues(catalog, wallet) {
|
|
225
245
|
return Object.keys(wallet.valuations)
|
|
226
246
|
.filter((id) => catalog.currencies.some((c) => c.id === id))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const FALLBACK = '#5c5650';
|
|
2
|
+
export function cardColor(catalog, cardId) {
|
|
3
|
+
const card = catalog.cards.find((c) => c.id === cardId);
|
|
4
|
+
if (card?.color)
|
|
5
|
+
return card.color;
|
|
6
|
+
return catalog.issuers.find((i) => i.id === card?.issuer)?.color ?? FALLBACK;
|
|
7
|
+
}
|
|
8
|
+
// Relative luminance, to keep the chip and edge visible on light cards.
|
|
9
|
+
function light(hex) {
|
|
10
|
+
const [r, g, b] = [1, 3, 5].map((i) => {
|
|
11
|
+
const c = parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
12
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
13
|
+
});
|
|
14
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.35;
|
|
15
|
+
}
|
|
16
|
+
// 86 x 54 is a card's 85.6 x 54 mm; `width` scales it.
|
|
17
|
+
export function cardArtSvg(color, width = 48) {
|
|
18
|
+
const height = Math.round((width * 54) / 86);
|
|
19
|
+
const edge = light(color) ? 'rgba(0,0,0,0.18)' : 'rgba(255,255,255,0.14)';
|
|
20
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 54" width="${String(width)}" height="${String(height)}" aria-hidden="true" focusable="false"><rect x="0.5" y="0.5" width="85" height="53" rx="5" fill="${color}" stroke="${edge}"/><path d="M0 40 L56 0 H80 Q85.5 0 85.5 5 V8 L20 54 H5 Q0 54 0 49 Z" fill="#fff" opacity="0.07"/><rect x="10" y="17" width="14" height="11" rx="2" fill="#d9c08a" stroke="rgba(0,0,0,0.25)" stroke-width="0.6"/><path d="M10 22.5 H24 M17 17 V28" stroke="rgba(0,0,0,0.25)" stroke-width="0.6"/><rect x="10" y="40" width="30" height="3" rx="1.5" fill="${light(color) ? '#000' : '#fff'}" opacity="0.28"/></svg>`;
|
|
21
|
+
}
|
package/dist/format/cards.js
CHANGED
|
@@ -20,6 +20,7 @@ export function formatCardInfo(card) {
|
|
|
20
20
|
: []),
|
|
21
21
|
...(card.needsAnniversary ? ['cap resets on the card anniversary'] : []),
|
|
22
22
|
...card.milestones.map((m) => `spend bonus: ${m.text} (${m.id})`),
|
|
23
|
+
...card.introRates.map((i) => `new accounts: ${i.text}; needs the opening date`),
|
|
23
24
|
...(card.onlyAt ? [`works only at ${card.onlyAt.join(', ')}`] : []),
|
|
24
25
|
foreignFeeText(card.foreignTransactionFee).toLowerCase(),
|
|
25
26
|
];
|
|
@@ -19,6 +19,7 @@ function compact(wallet) {
|
|
|
19
19
|
? {}
|
|
20
20
|
: { checkout_label: c.checkout_label }),
|
|
21
21
|
...(c.anniversary === undefined ? {} : { anniversary: c.anniversary }),
|
|
22
|
+
...(c.opened === undefined ? {} : { opened: c.opened }),
|
|
22
23
|
...(Object.keys(c.cap_usage).length > 0
|
|
23
24
|
? { cap_usage: c.cap_usage }
|
|
24
25
|
: {}),
|
package/dist/format/wallet.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CAP_PERIODS, money, NETWORK_NAMES, shortDate } from "../engine/text.js";
|
|
1
|
+
import { CAP_PERIODS, introAdjective, introSince, money, NETWORK_NAMES, periodPhrase, shortDate, } from "../engine/text.js";
|
|
2
2
|
// "To do now", above the card details; nothing when all's done.
|
|
3
3
|
export function formatChecklist(items) {
|
|
4
4
|
return items.length === 0
|
|
@@ -36,11 +36,17 @@ export function formatWalletSummary(summary) {
|
|
|
36
36
|
for (const cap of c.caps)
|
|
37
37
|
lines.push(cap.remaining === null
|
|
38
38
|
? ` Cap on ${cap.label}: ${money(cap.amount)} ${CAP_PERIODS[cap.period]}; give the card's anniversary to track it (cap key ${cap.key}).`
|
|
39
|
-
: ` Cap on ${cap.label}: ${money(cap.spent)} of ${money(cap.amount)} used ${cap.period
|
|
39
|
+
: ` Cap on ${cap.label}: ${money(cap.spent)} of ${money(cap.amount)} used ${periodPhrase(cap.period, cap.currentPeriod ?? '')}${cap.until === null ? '' : `, through ${shortDate(cap.until)}`} (cap key ${cap.key}).`);
|
|
40
40
|
for (const m of c.milestones)
|
|
41
41
|
lines.push(` Spend bonus (${m.id}): ${m.text}`);
|
|
42
42
|
if (c.anniversary !== null)
|
|
43
43
|
lines.push(` Account anniversary: ${c.anniversary}.`);
|
|
44
|
+
if (c.intro?.state === 'active' && c.intro.until !== null)
|
|
45
|
+
lines.push(` Opened ${c.opened ?? ''}: ${introAdjective(c.intro.days)} rates through ${shortDate(c.intro.until)}.`);
|
|
46
|
+
else if (c.intro?.state === 'no-date')
|
|
47
|
+
lines.push(` Has ${introAdjective(c.intro.days)} rates for new accounts; give its opening date if it opened ${introSince(c.intro.days)}.`);
|
|
48
|
+
else if (c.opened !== null)
|
|
49
|
+
lines.push(` Opened ${c.opened}.`);
|
|
44
50
|
const b = c.bonus;
|
|
45
51
|
if (b?.state === 'open' && b.spend !== null)
|
|
46
52
|
lines.push(` Welcome bonus: ${money(b.spent)} of ${money(b.spend)} spent, due ${b.deadline ?? ''}.`);
|
|
@@ -60,7 +66,7 @@ export function formatBooking(amount, cardId, cap, bonus, milestones = []) {
|
|
|
60
66
|
const messages = [];
|
|
61
67
|
if (cap) {
|
|
62
68
|
const left = Math.max(0, cap.capAmount - cap.after);
|
|
63
|
-
messages.push(`Recorded ${money(amount)} on ${cardId} against "${cap.capKey}": ${money(cap.before)} → ${money(cap.after)} of ${money(cap.capAmount)}
|
|
69
|
+
messages.push(`Recorded ${money(amount)} on ${cardId} against "${cap.capKey}": ${money(cap.before)} → ${money(cap.after)} of ${money(cap.capAmount)} ${periodPhrase(cap.periodKind, cap.period)}, ${money(left)} left.`);
|
|
64
70
|
}
|
|
65
71
|
if (bonus) {
|
|
66
72
|
const left = Math.max(0, bonus.spend - bonus.after);
|
|
@@ -74,7 +80,7 @@ export function formatBooking(amount, cardId, cap, bonus, milestones = []) {
|
|
|
74
80
|
const reached = milestones.filter((m) => m.before < m.spend && m.after >= m.spend);
|
|
75
81
|
const next = milestones.find((m) => m.after < m.spend);
|
|
76
82
|
messages.push([
|
|
77
|
-
`Spend on ${cardId} this year: ${money(first.after)}.`,
|
|
83
|
+
`Spend on ${cardId} this ${first.periodKind === 'cardmember_year' ? 'cardmember year' : 'year'}: ${money(first.after)}.`,
|
|
78
84
|
...reached.map((m) => `That reaches its ${money(m.spend)} spend bonus.`),
|
|
79
85
|
...(next
|
|
80
86
|
? [
|
package/dist/import/analyze.js
CHANGED
package/dist/mcp/inputs.js
CHANGED
|
@@ -64,6 +64,9 @@ const cardFields = {
|
|
|
64
64
|
.regex(/^\d{2}-\d{2}$/)
|
|
65
65
|
.optional()
|
|
66
66
|
.describe("The account's anniversary as MM-DD, for cards whose cap resets on it."),
|
|
67
|
+
opened: IsoDate.nullable()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Optional: the day the account opened, YYYY-MM-DD. Only for cards with a first-year rate (search_cards lists it), which applies only while a purchase falls in the window; without a date the card is ranked at its standard rates. Ask the user; never guess or assume a card is new. null removes it.'),
|
|
67
70
|
activated: z
|
|
68
71
|
.array(z.string())
|
|
69
72
|
.optional()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Ready-made questions a client can offer: the user's question, then what
|
|
3
|
+
// to call and how to answer. Rates and card facts come only from the tools.
|
|
4
|
+
export const PROMPTS = ['which-card', 'cheat-sheet', 'this-quarter', 'set-up'];
|
|
5
|
+
const FROM_SAVED_OR_ALL = "the user's saved cards if there are any, otherwise every card earnrate knows";
|
|
6
|
+
const ask = (question, guidance) => ({
|
|
7
|
+
messages: [
|
|
8
|
+
{
|
|
9
|
+
role: 'user',
|
|
10
|
+
content: {
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: `${question}\n\n${guidance.join(' ')}`,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
const dollars = (amount) => (/^\d/.test(amount) ? `$${amount}` : amount);
|
|
18
|
+
export function registerPrompts(server, deps, ctx) {
|
|
19
|
+
server.registerPrompt('which-card', {
|
|
20
|
+
title: 'Which credit card should I use?',
|
|
21
|
+
description: 'The card that earns the most at a store or for a kind of purchase, with the reason. From your saved cards, or every card if you have none saved.',
|
|
22
|
+
argsSchema: z.object({
|
|
23
|
+
store: z
|
|
24
|
+
.string()
|
|
25
|
+
.trim()
|
|
26
|
+
.min(1)
|
|
27
|
+
.describe('The store, or the kind of purchase: Costco, groceries'),
|
|
28
|
+
amount: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('How much you are spending, in US dollars'),
|
|
32
|
+
country: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Where you are paying, if not the U.S.: FR, Japan'),
|
|
36
|
+
}),
|
|
37
|
+
}, (args) => {
|
|
38
|
+
const amount = args.amount?.trim() || null;
|
|
39
|
+
const country = args.country?.trim() || null;
|
|
40
|
+
const call = [
|
|
41
|
+
`merchant ${JSON.stringify(args.store)}`,
|
|
42
|
+
...(amount === null
|
|
43
|
+
? []
|
|
44
|
+
: [`amount ${JSON.stringify(amount)} as a number of US dollars`]),
|
|
45
|
+
...(country === null
|
|
46
|
+
? []
|
|
47
|
+
: [`country as the two-letter code for ${JSON.stringify(country)}`]),
|
|
48
|
+
];
|
|
49
|
+
return ask(`Which credit card should I use at ${args.store}${amount === null ? '' : ` for ${dollars(amount)}`}${country === null ? '' : ` in ${country}`}?`, [
|
|
50
|
+
`Call earnrate's best_card_for_purchase with ${call.join(', ')}.`,
|
|
51
|
+
`It answers from ${FROM_SAVED_OR_ALL}, so don't ask for their cards first.`,
|
|
52
|
+
'Say which card to use and why, with the assumptions and warnings it gives.',
|
|
53
|
+
...(country === null
|
|
54
|
+
? [
|
|
55
|
+
'If the user mentions paying abroad or in another currency, ask where, or pass `country` as its two-letter code.',
|
|
56
|
+
]
|
|
57
|
+
: []),
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
server.registerPrompt('cheat-sheet', {
|
|
61
|
+
title: 'Which card for everyday spending?',
|
|
62
|
+
description: 'The card to use for each everyday kind of purchase right now, from your saved cards or every card.',
|
|
63
|
+
}, () => ask('Which card should I use for everyday spending?', [
|
|
64
|
+
"Call earnrate's cheat_sheet and show the card for each kind of purchase, with its notes.",
|
|
65
|
+
`It uses ${FROM_SAVED_OR_ALL}.`,
|
|
66
|
+
]));
|
|
67
|
+
server.registerPrompt('this-quarter', {
|
|
68
|
+
title: 'What should I do with my cards this quarter?',
|
|
69
|
+
description: 'Bonuses to activate, categories to pick, caps running low and welcome bonuses to finish, from your saved cards.',
|
|
70
|
+
}, () => ask('What should I do with my cards this quarter?', [
|
|
71
|
+
"Call earnrate's list_wallet: it starts with a to-do list. Walk through it, soonest first.",
|
|
72
|
+
ctx.walletUse,
|
|
73
|
+
'If no cards are saved, say in a sentence or two what saving them adds (reminders to activate bonuses, pick categories, and watch caps and welcome bonuses), without pressing the user to do it, and offer the cheat sheet from every card instead.',
|
|
74
|
+
]));
|
|
75
|
+
server.registerPrompt('set-up', {
|
|
76
|
+
title: 'Set up my cards',
|
|
77
|
+
description: 'Save the cards you hold, by name, with their category picks, memberships and activations.',
|
|
78
|
+
}, () => ask('Help me set up my cards in earnrate.', [
|
|
79
|
+
'Ask which cards the user has; their names are enough. Never ask for card numbers, expiry dates or security codes.',
|
|
80
|
+
...(deps.savedWallets === 'account'
|
|
81
|
+
? [
|
|
82
|
+
"Never ask for the recovery code either; it's typed only on earnrate's page.",
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
'Find each card with search_cards and check the match with the user, then save it with add_card.',
|
|
86
|
+
ctx.walletUse,
|
|
87
|
+
'When a card needs category picks, a membership like Amazon Prime, activations or its anniversary, ask rather than guessing, and save the answers with update_card or set_memberships.',
|
|
88
|
+
'When done, call list_wallet and show its to-do list.',
|
|
89
|
+
]));
|
|
90
|
+
}
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -100,6 +100,7 @@ export const WalletSummarySchema = z.object({
|
|
|
100
100
|
'bonus',
|
|
101
101
|
'milestone',
|
|
102
102
|
'rotation',
|
|
103
|
+
'intro',
|
|
103
104
|
]),
|
|
104
105
|
cardId: z.string(),
|
|
105
106
|
text: z.string(),
|
|
@@ -169,6 +170,14 @@ export const WalletSummarySchema = z.object({
|
|
|
169
170
|
activated: z.array(z.string()),
|
|
170
171
|
toActivate: z.array(z.string()),
|
|
171
172
|
anniversary: z.string().nullable(),
|
|
173
|
+
opened: z.string().nullable(),
|
|
174
|
+
intro: z
|
|
175
|
+
.object({
|
|
176
|
+
state: z.enum(['active', 'ended', 'no-date']),
|
|
177
|
+
until: z.string().nullable(),
|
|
178
|
+
days: z.number(),
|
|
179
|
+
})
|
|
180
|
+
.nullable(),
|
|
172
181
|
})),
|
|
173
182
|
});
|
|
174
183
|
export const RecordResultSchema = z.object({
|
|
@@ -179,6 +188,12 @@ export const RecordResultSchema = z.object({
|
|
|
179
188
|
capKey: z.string(),
|
|
180
189
|
ruleId: z.string(),
|
|
181
190
|
period: z.string(),
|
|
191
|
+
periodKind: z.enum([
|
|
192
|
+
'month',
|
|
193
|
+
'quarter',
|
|
194
|
+
'calendar_year',
|
|
195
|
+
'cardmember_year',
|
|
196
|
+
]),
|
|
182
197
|
capAmount: z.number(),
|
|
183
198
|
before: z.number(),
|
|
184
199
|
after: z.number(),
|
|
@@ -197,6 +212,7 @@ export const RecordResultSchema = z.object({
|
|
|
197
212
|
cardId: z.string(),
|
|
198
213
|
milestoneId: z.string(),
|
|
199
214
|
period: z.string(),
|
|
215
|
+
periodKind: z.enum(['calendar_year', 'cardmember_year']),
|
|
200
216
|
spend: z.number(),
|
|
201
217
|
before: z.number(),
|
|
202
218
|
after: z.number(),
|
|
@@ -270,6 +286,7 @@ export const CardSearchSchema = z.object({
|
|
|
270
286
|
rewards: z.array(z.string()),
|
|
271
287
|
text: z.string(),
|
|
272
288
|
})),
|
|
289
|
+
introRates: z.array(z.object({ id: z.string(), days: z.number(), text: z.string() })),
|
|
273
290
|
})),
|
|
274
291
|
});
|
|
275
292
|
export const WalletChangeSchema = z.object({
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/server';
|
|
|
2
2
|
import { MCP_SERVER_NAME } from "../brand.js";
|
|
3
3
|
import { makeContext } from "./context.js";
|
|
4
4
|
import { makeInputs } from "./inputs.js";
|
|
5
|
+
import { registerPrompts } from "./prompts.js";
|
|
5
6
|
import { registerAccountTools } from "./tools/account.js";
|
|
6
7
|
import { registerAnswerTools } from "./tools/answers.js";
|
|
7
8
|
import { registerWalletTools } from "./tools/wallet.js";
|
|
@@ -66,6 +67,7 @@ export function createEarnrateServer(deps) {
|
|
|
66
67
|
const inputs = makeInputs(deps.catalog);
|
|
67
68
|
registerAnswerTools(server, deps, ctx, inputs);
|
|
68
69
|
registerWalletTools(server, deps, ctx, inputs);
|
|
70
|
+
registerPrompts(server, deps, ctx);
|
|
69
71
|
if (deps.savedWallets === 'account')
|
|
70
72
|
registerAccountTools(server, deps.catalog, deps.store, ctx);
|
|
71
73
|
return server;
|
|
@@ -26,6 +26,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
26
26
|
annotations: {
|
|
27
27
|
title: 'Export the wallet',
|
|
28
28
|
readOnlyHint: true,
|
|
29
|
+
destructiveHint: false,
|
|
29
30
|
openWorldHint: false,
|
|
30
31
|
},
|
|
31
32
|
}, async () => {
|
|
@@ -82,7 +83,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
82
83
|
});
|
|
83
84
|
server.registerTool('delete_wallet', {
|
|
84
85
|
title: 'Delete the wallet',
|
|
85
|
-
description: "Deletes the saved wallet and its recovery code now and for good, and signs out every
|
|
86
|
+
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.",
|
|
86
87
|
inputSchema: z.object({
|
|
87
88
|
confirm: z
|
|
88
89
|
.literal(true)
|
|
@@ -121,7 +122,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
121
122
|
content: [
|
|
122
123
|
{
|
|
123
124
|
type: 'text',
|
|
124
|
-
text: "Deleted the saved wallet and its recovery code, and signed out every
|
|
125
|
+
text: "Deleted the saved wallet and its recovery code, and signed out every app connected to it, this one included. To keep using earnrate here, the user connects again on earnrate's page and starts a new wallet.",
|
|
125
126
|
},
|
|
126
127
|
],
|
|
127
128
|
structuredContent: { deleted: true },
|
|
@@ -129,13 +130,15 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
129
130
|
});
|
|
130
131
|
server.registerTool('switch_wallet', {
|
|
131
132
|
title: 'Switch wallets',
|
|
132
|
-
description: "Disconnects this app from its saved wallet so the user can connect a different one with its recovery code, or start a new one, on earnrate's page. Nothing is deleted. It also signs out anyone else connected to this wallet through the same app (another
|
|
133
|
+
description: "Disconnects this app from its saved wallet so the user can connect a different one with its recovery code, or start a new one, on earnrate's page. Nothing is deleted. It also signs out anyone else connected to this wallet through the same app (another account the recovery code was shared with): say so before calling it. Use it when the user wants another wallet. Never ask for the recovery code in the conversation.",
|
|
133
134
|
inputSchema: z.object({}),
|
|
134
135
|
outputSchema: z.object({ disconnected: z.boolean() }),
|
|
135
136
|
annotations: {
|
|
136
137
|
title: 'Switch wallets',
|
|
137
138
|
readOnlyHint: false,
|
|
138
|
-
|
|
139
|
+
// It signs out this app's other connections to the wallet, which only
|
|
140
|
+
// the recovery code undoes.
|
|
141
|
+
destructiveHint: true,
|
|
139
142
|
idempotentHint: true,
|
|
140
143
|
openWorldHint: false,
|
|
141
144
|
},
|
|
@@ -145,7 +148,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
145
148
|
content: [
|
|
146
149
|
{
|
|
147
150
|
type: 'text',
|
|
148
|
-
text: "Disconnected from this saved wallet; nothing was deleted. earnrate's sign-in page opens on the next earnrate request (or reconnect earnrate in the app's connector settings). There the user chooses \"Already have a wallet?\" and types the other wallet's recovery code, or starts a new wallet. Other
|
|
151
|
+
text: "Disconnected from this saved wallet; nothing was deleted. earnrate's sign-in page opens on the next earnrate request (or reconnect earnrate in the app's connector settings). There the user chooses \"Already have a wallet?\" and types the other wallet's recovery code, or starts a new wallet. Other accounts connected to this wallet through the same app are signed out too.",
|
|
149
152
|
},
|
|
150
153
|
],
|
|
151
154
|
structuredContent: { disconnected: true },
|
|
@@ -54,6 +54,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
54
54
|
annotations: {
|
|
55
55
|
title: 'Best card for a purchase',
|
|
56
56
|
readOnlyHint: true,
|
|
57
|
+
destructiveHint: false,
|
|
57
58
|
openWorldHint: deps.llm !== null,
|
|
58
59
|
},
|
|
59
60
|
}, async ({ merchant, amount, channel, date, country, category, cards, memberships, valuation, }) => {
|
|
@@ -127,6 +128,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
127
128
|
annotations: {
|
|
128
129
|
title: 'Resolve a merchant',
|
|
129
130
|
readOnlyHint: true,
|
|
131
|
+
destructiveHint: false,
|
|
130
132
|
openWorldHint: deps.llm !== null,
|
|
131
133
|
},
|
|
132
134
|
}, async ({ merchant, channel, category }) => {
|
|
@@ -166,6 +168,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
166
168
|
annotations: {
|
|
167
169
|
title: 'Compare welcome offers',
|
|
168
170
|
readOnlyHint: true,
|
|
171
|
+
destructiveHint: false,
|
|
169
172
|
openWorldHint: false,
|
|
170
173
|
},
|
|
171
174
|
}, async (args) => {
|
|
@@ -219,6 +222,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
219
222
|
annotations: {
|
|
220
223
|
title: 'Best cards for a purchase',
|
|
221
224
|
readOnlyHint: true,
|
|
225
|
+
destructiveHint: false,
|
|
222
226
|
openWorldHint: false,
|
|
223
227
|
},
|
|
224
228
|
}, async (args) => {
|
|
@@ -266,6 +270,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
266
270
|
annotations: {
|
|
267
271
|
title: 'Simulate a year of spending',
|
|
268
272
|
readOnlyHint: true,
|
|
273
|
+
destructiveHint: false,
|
|
269
274
|
openWorldHint: false,
|
|
270
275
|
},
|
|
271
276
|
}, async (args) => {
|
|
@@ -349,6 +354,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
349
354
|
annotations: {
|
|
350
355
|
title: 'Cheat sheet',
|
|
351
356
|
readOnlyHint: true,
|
|
357
|
+
destructiveHint: false,
|
|
352
358
|
openWorldHint: false,
|
|
353
359
|
},
|
|
354
360
|
}, async (args) => {
|
|
@@ -391,6 +397,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
391
397
|
annotations: {
|
|
392
398
|
title: 'Search cards',
|
|
393
399
|
readOnlyHint: true,
|
|
400
|
+
destructiveHint: false,
|
|
394
401
|
openWorldHint: false,
|
|
395
402
|
},
|
|
396
403
|
}, async ({ query }) => {
|
package/dist/mcp/tools/wallet.js
CHANGED
|
@@ -39,6 +39,10 @@ function changes(catalog, args) {
|
|
|
39
39
|
out.push(`checkout name "${args.checkoutLabel}"`);
|
|
40
40
|
if (args.anniversary !== undefined)
|
|
41
41
|
out.push(`anniversary ${args.anniversary}`);
|
|
42
|
+
if (args.opened === null)
|
|
43
|
+
out.push('opening date cleared');
|
|
44
|
+
else if (args.opened !== undefined)
|
|
45
|
+
out.push(`opened ${args.opened}`);
|
|
42
46
|
if (args.activated !== undefined)
|
|
43
47
|
out.push(args.activated.length === 0
|
|
44
48
|
? 'nothing activated'
|
|
@@ -46,7 +50,7 @@ function changes(catalog, args) {
|
|
|
46
50
|
for (const [key, spent] of Object.entries(args.capSpent ?? {}))
|
|
47
51
|
out.push(`${money(spent)} already spent on ${key} this period`);
|
|
48
52
|
for (const [id, spent] of Object.entries(args.milestoneSpent ?? {}))
|
|
49
|
-
out.push(`${money(spent)} spent toward ${id} this year`);
|
|
53
|
+
out.push(`${money(spent)} spent toward ${id} this ${card?.milestones.find((m) => m.id === id)?.period === 'cardmember_year' ? 'cardmember year' : 'year'}`);
|
|
50
54
|
if (args.bonus === null)
|
|
51
55
|
out.push('welcome bonus no longer tracked');
|
|
52
56
|
else if (args.bonus !== undefined)
|
|
@@ -63,6 +67,7 @@ export function registerWalletTools(server, deps, ctx, inputs) {
|
|
|
63
67
|
annotations: {
|
|
64
68
|
title: 'List wallet',
|
|
65
69
|
readOnlyHint: true,
|
|
70
|
+
destructiveHint: false,
|
|
66
71
|
openWorldHint: false,
|
|
67
72
|
},
|
|
68
73
|
}, async () => {
|
|
@@ -178,7 +183,7 @@ export function registerWalletTools(server, deps, ctx, inputs) {
|
|
|
178
183
|
});
|
|
179
184
|
server.registerTool('update_card', {
|
|
180
185
|
title: 'Update a card',
|
|
181
|
-
description: `Changes settings on a card already in the wallet: its category picks for this period, checkout label, anniversary, activated categories, cap spending so far (capSpent), spending this year toward its spend milestones (milestoneSpent) or a welcome bonus to track. Picks replace the saved ones in each choice group you set (other groups are kept) for the current period. Use it when the user picks new categories, such as each quarter on U.S. Bank Cash+. ${ctx.walletUse}`,
|
|
186
|
+
description: `Changes settings on a card already in the wallet: its category picks for this period, checkout label, anniversary, opening date (optional; for first-year rates), activated categories, cap spending so far (capSpent), spending this year toward its spend milestones (milestoneSpent) or a welcome bonus to track. Picks replace the saved ones in each choice group you set (other groups are kept) for the current period. Use it when the user picks new categories, such as each quarter on U.S. Bank Cash+. ${ctx.walletUse}`,
|
|
182
187
|
inputSchema: CardChange,
|
|
183
188
|
outputSchema: WalletChangeSchema,
|
|
184
189
|
annotations: {
|
package/dist/schema/card.js
CHANGED
|
@@ -145,10 +145,32 @@ export const ChoiceSchema = z.strictObject({
|
|
|
145
145
|
})),
|
|
146
146
|
])),
|
|
147
147
|
});
|
|
148
|
+
// A rate for a stretch after the account opens, counted from the opening
|
|
149
|
+
// day: Discover's Cashback Match doubles every rate for the first 365 days,
|
|
150
|
+
// Customized Cash adds 3% to its choice category. With `rules`, it changes
|
|
151
|
+
// only those rules' rates, inside their caps; past a cap they earn what they
|
|
152
|
+
// always do. Without, it changes every rate on the card, past caps included.
|
|
153
|
+
export const IntroRateSchema = z
|
|
154
|
+
.strictObject({
|
|
155
|
+
id: KebabId,
|
|
156
|
+
days: z.number().int().positive(),
|
|
157
|
+
rules: z.array(KebabId).min(1).optional(),
|
|
158
|
+
multiply: z.number().positive().optional(),
|
|
159
|
+
add: z.number().positive().optional(), // currency units per dollar
|
|
160
|
+
note: z.string().optional(),
|
|
161
|
+
})
|
|
162
|
+
.refine((i) => (i.multiply === undefined) !== (i.add === undefined), 'an intro rate needs multiply or add, not both');
|
|
163
|
+
const HexColor = z.string().regex(/^#[0-9a-f]{6}$/);
|
|
148
164
|
export const CardSchema = z
|
|
149
165
|
.strictObject({
|
|
150
166
|
id: KebabId,
|
|
151
167
|
name: z.string().min(1),
|
|
168
|
+
// A name that fits one line of the Wallet pass's strip (24 characters),
|
|
169
|
+
// for cards whose name is longer: "AAdvantage Platinum".
|
|
170
|
+
short_name: z.string().min(1).max(24).optional(),
|
|
171
|
+
// The card face's main color, roughly, for earnrate's drawing of the
|
|
172
|
+
// card. Never the issuer's card art.
|
|
173
|
+
color: HexColor.optional(),
|
|
152
174
|
issuer: z.string().min(1),
|
|
153
175
|
network: CardNetwork,
|
|
154
176
|
// Merchant ids a store card works at. Required for, and only for, `store`.
|
|
@@ -183,6 +205,7 @@ export const CardSchema = z
|
|
|
183
205
|
choices: z.array(ChoiceSchema).default([]),
|
|
184
206
|
rules: z.array(RuleSchema).default([]),
|
|
185
207
|
milestones: z.array(MilestoneSchema).default([]),
|
|
208
|
+
intro_rates: z.array(IntroRateSchema).default([]),
|
|
186
209
|
// A short quote from an issuer page for each rule (or `base_rate`). The
|
|
187
210
|
// weekly source check fails a rule whose quote disappears from its page.
|
|
188
211
|
evidence: z
|
package/dist/schema/issuer.js
CHANGED
|
@@ -4,5 +4,9 @@ export const IssuerSchema = z.strictObject({
|
|
|
4
4
|
id: KebabId,
|
|
5
5
|
name: z.string().min(1), // as shown: "American Express"
|
|
6
6
|
search: z.array(z.string().min(1)).default([]), // other words people use
|
|
7
|
+
color: z
|
|
8
|
+
.string()
|
|
9
|
+
.regex(/^#[0-9a-f]{6}$/)
|
|
10
|
+
.optional(), // for a card without its own
|
|
7
11
|
});
|
|
8
12
|
export const IssuersFileSchema = z.array(IssuerSchema).min(1);
|
package/dist/schema/wallet.js
CHANGED
|
@@ -28,6 +28,9 @@ export const WalletCardSchema = z.strictObject({
|
|
|
28
28
|
.string()
|
|
29
29
|
.regex(/^\d{2}-\d{2}$/)
|
|
30
30
|
.optional(),
|
|
31
|
+
// The day the account opened, for rates that apply only for a stretch
|
|
32
|
+
// after it (intro_rates). bonus.opened stands in when this isn't set.
|
|
33
|
+
opened: IsoDate.optional(),
|
|
31
34
|
// Keyed by shared cap id, or by rule id for an inline cap.
|
|
32
35
|
cap_usage: z.record(KebabId, CapUsage).default({}),
|
|
33
36
|
chosen: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "earnrate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Which credit card earns the most at a merchant, from your cards or every card, and why",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"credit cards",
|
|
@@ -42,19 +42,23 @@
|
|
|
42
42
|
"prepack": "yarn build",
|
|
43
43
|
"smoke:pack": "node scripts/smoke-pack.ts",
|
|
44
44
|
"mcpb:pack": "node scripts/build-mcpb.ts",
|
|
45
|
+
"chatgpt:pack": "node scripts/pack-chatgpt.ts",
|
|
45
46
|
"smoke:mcpb": "node scripts/smoke-mcpb.ts",
|
|
46
47
|
"remote:build": "node scripts/build-remote.ts",
|
|
47
48
|
"remote:check": "yarn remote:build && WRANGLER_SEND_METRICS=false wrangler deploy --config remote/wrangler.jsonc --dry-run --outdir dist",
|
|
48
49
|
"remote:deploy": "yarn remote:build && WRANGLER_SEND_METRICS=false wrangler deploy --config remote/wrangler.jsonc",
|
|
50
|
+
"pass:build": "node scripts/build-pass.ts",
|
|
51
|
+
"pass:check": "yarn pass:build && WRANGLER_SEND_METRICS=false wrangler deploy --config pass/wrangler.jsonc --dry-run --outdir dist",
|
|
52
|
+
"pass:deploy": "yarn pass:build && WRANGLER_SEND_METRICS=false wrangler deploy --config pass/wrangler.jsonc",
|
|
49
53
|
"monitor:check": "WRANGLER_SEND_METRICS=false wrangler deploy --config monitor/wrangler.jsonc --dry-run --outdir dist",
|
|
50
54
|
"monitor:deploy": "WRANGLER_SEND_METRICS=false wrangler deploy --config monitor/wrangler.jsonc",
|
|
51
55
|
"site:build": "vite build site && node scripts/build-pages.ts",
|
|
52
56
|
"site:dev": "vite site",
|
|
53
|
-
"site:deploy": "yarn site:build && WRANGLER_SEND_METRICS=false wrangler deploy",
|
|
57
|
+
"site:deploy": "yarn site:build && WRANGLER_SEND_METRICS=false wrangler deploy && node scripts/indexnow.ts",
|
|
54
58
|
"test": "vitest run",
|
|
55
59
|
"test:watch": "vitest",
|
|
56
60
|
"coverage": "vitest run --coverage --coverage.include=src/**",
|
|
57
|
-
"typecheck": "tsc && tsc -p site && tsc -p remote && tsc -p monitor",
|
|
61
|
+
"typecheck": "tsc && tsc -p site && tsc -p remote && tsc -p monitor && tsc -p pass",
|
|
58
62
|
"lint": "eslint",
|
|
59
63
|
"format": "prettier --write .",
|
|
60
64
|
"format:check": "prettier --check .",
|
|
@@ -71,11 +75,16 @@
|
|
|
71
75
|
"@cloudflare/workers-oauth-provider": "1.2.4",
|
|
72
76
|
"@cloudflare/workers-types": "5.20261031.1",
|
|
73
77
|
"@eslint/js": "^10.0.1",
|
|
78
|
+
"@expo-google-fonts/inter": "0.2.3",
|
|
74
79
|
"@modelcontextprotocol/client": "2.3.1",
|
|
80
|
+
"@resvg/resvg-wasm": "2.6.2",
|
|
75
81
|
"@types/node": "^24",
|
|
82
|
+
"@types/node-forge": "1.3.11",
|
|
76
83
|
"@vitest/coverage-v8": "5.0.5",
|
|
77
84
|
"eslint": "^10.13.0",
|
|
78
85
|
"eslint-config-prettier": "^10.1.8",
|
|
86
|
+
"fflate": "0.8.2",
|
|
87
|
+
"node-forge": "1.3.1",
|
|
79
88
|
"playwright": "1.64.1",
|
|
80
89
|
"preact": "^10.29.8",
|
|
81
90
|
"prettier": "^3.9.10",
|