earnrate 0.2.11 → 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.
Files changed (126) hide show
  1. package/README.md +21 -9
  2. package/data/CHANGELOG.md +110 -0
  3. package/data/VERSION +1 -1
  4. package/data/cards/amex-blue-business-plus.yaml +1 -0
  5. package/data/cards/amex-blue-cash-everyday.yaml +1 -0
  6. package/data/cards/amex-blue-cash-preferred.yaml +7 -0
  7. package/data/cards/amex-business-gold.yaml +134 -0
  8. package/data/cards/amex-business-platinum.yaml +115 -0
  9. package/data/cards/amex-delta-skymiles-blue.yaml +91 -0
  10. package/data/cards/amex-delta-skymiles-gold.yaml +2 -0
  11. package/data/cards/amex-delta-skymiles-platinum.yaml +2 -0
  12. package/data/cards/amex-delta-skymiles-reserve.yaml +2 -0
  13. package/data/cards/amex-gold.yaml +1 -0
  14. package/data/cards/amex-hilton-honors-aspire.yaml +2 -0
  15. package/data/cards/amex-hilton-honors-business.yaml +93 -0
  16. package/data/cards/amex-hilton-honors-surpass.yaml +2 -0
  17. package/data/cards/amex-hilton-honors.yaml +2 -0
  18. package/data/cards/amex-marriott-bonvoy-bevy.yaml +2 -0
  19. package/data/cards/amex-marriott-bonvoy-brilliant.yaml +2 -0
  20. package/data/cards/amex-marriott-bonvoy-business.yaml +116 -0
  21. package/data/cards/amex-platinum.yaml +1 -0
  22. package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +1 -0
  23. package/data/cards/bank-of-america-atmos-rewards-business.yaml +2 -0
  24. package/data/cards/bank-of-america-atmos-rewards-summit.yaml +1 -0
  25. package/data/cards/bank-of-america-customized-cash-rewards.yaml +34 -1
  26. package/data/cards/bank-of-america-premium-rewards.yaml +134 -0
  27. package/data/cards/bank-of-america-travel-rewards.yaml +123 -0
  28. package/data/cards/bank-of-america-unlimited-cash-rewards.yaml +83 -0
  29. package/data/cards/barclays-jetblue-plus.yaml +86 -0
  30. package/data/cards/bilt-blue.yaml +60 -0
  31. package/data/cards/bilt-obsidian.yaml +1 -0
  32. package/data/cards/bilt-palladium.yaml +76 -0
  33. package/data/cards/capital-one-kohls-card.yaml +1 -0
  34. package/data/cards/capital-one-kohls-rewards-visa.yaml +1 -0
  35. package/data/cards/capital-one-quicksilver.yaml +2 -0
  36. package/data/cards/capital-one-savor.yaml +2 -0
  37. package/data/cards/capital-one-spark-cash-plus.yaml +80 -0
  38. package/data/cards/capital-one-venture-x-business.yaml +97 -0
  39. package/data/cards/capital-one-venture-x.yaml +2 -0
  40. package/data/cards/capital-one-venture.yaml +2 -0
  41. package/data/cards/chase-aeroplan.yaml +2 -0
  42. package/data/cards/chase-freedom-flex.yaml +1 -0
  43. package/data/cards/chase-freedom-rise.yaml +29 -0
  44. package/data/cards/chase-freedom-unlimited.yaml +1 -0
  45. package/data/cards/chase-ihg-one-rewards-business.yaml +1 -0
  46. package/data/cards/chase-ihg-one-rewards-premier-select.yaml +2 -0
  47. package/data/cards/chase-ihg-one-rewards-premier.yaml +1 -0
  48. package/data/cards/chase-ihg-one-rewards.yaml +1 -0
  49. package/data/cards/chase-ink-business-cash.yaml +1 -0
  50. package/data/cards/chase-ink-business-preferred.yaml +1 -0
  51. package/data/cards/chase-ink-business-unlimited.yaml +69 -0
  52. package/data/cards/chase-marriott-bonvoy-bold.yaml +1 -0
  53. package/data/cards/chase-marriott-bonvoy-boundless.yaml +2 -0
  54. package/data/cards/chase-prime-visa.yaml +1 -0
  55. package/data/cards/chase-sapphire-preferred.yaml +1 -0
  56. package/data/cards/chase-sapphire-reserve.yaml +1 -0
  57. package/data/cards/chase-southwest-performance-business.yaml +2 -0
  58. package/data/cards/chase-southwest-plus.yaml +2 -0
  59. package/data/cards/chase-southwest-premier-business.yaml +2 -0
  60. package/data/cards/chase-southwest-premier.yaml +2 -0
  61. package/data/cards/chase-southwest-priority.yaml +2 -0
  62. package/data/cards/chase-united-business.yaml +1 -0
  63. package/data/cards/chase-united-club-business.yaml +1 -0
  64. package/data/cards/chase-united-club.yaml +1 -0
  65. package/data/cards/chase-united-explorer.yaml +1 -0
  66. package/data/cards/chase-united-gateway.yaml +1 -0
  67. package/data/cards/chase-united-quest.yaml +1 -0
  68. package/data/cards/citi-aadvantage-business.yaml +2 -0
  69. package/data/cards/citi-aadvantage-executive.yaml +2 -0
  70. package/data/cards/citi-aadvantage-globe.yaml +2 -0
  71. package/data/cards/citi-aadvantage-mileup.yaml +2 -0
  72. package/data/cards/citi-aadvantage-platinum-select.yaml +2 -0
  73. package/data/cards/citi-costco-anywhere-visa-business.yaml +2 -0
  74. package/data/cards/citi-costco-anywhere-visa.yaml +2 -0
  75. package/data/cards/citi-custom-cash.yaml +1 -0
  76. package/data/cards/citi-double-cash.yaml +1 -0
  77. package/data/cards/citi-my-best-buy-visa.yaml +1 -0
  78. package/data/cards/citi-strata-elite.yaml +1 -0
  79. package/data/cards/citi-strata-premier.yaml +1 -0
  80. package/data/cards/citi-strata.yaml +164 -0
  81. package/data/cards/discover-it-cash-back.yaml +22 -3
  82. package/data/cards/paypal-cashback-mastercard.yaml +41 -0
  83. package/data/cards/sams-club-mastercard.yaml +68 -0
  84. package/data/cards/sofi-unlimited-2.yaml +1 -0
  85. package/data/cards/td-bank-nordstrom-credit-card.yaml +1 -0
  86. package/data/cards/td-bank-nordstrom-visa.yaml +1 -0
  87. package/data/cards/td-bank-target-circle-card.yaml +1 -0
  88. package/data/cards/usbank-altitude-connect.yaml +150 -0
  89. package/data/cards/usbank-altitude-go.yaml +118 -0
  90. package/data/cards/usbank-cash-plus.yaml +1 -0
  91. package/data/cards/wells-fargo-active-cash.yaml +1 -0
  92. package/data/cards/wells-fargo-autograph-journey.yaml +73 -0
  93. package/data/cards/wells-fargo-autograph.yaml +1 -0
  94. package/data/categories.yaml +3 -1
  95. package/data/currencies.yaml +34 -0
  96. package/data/issuers.yaml +22 -1
  97. package/data/memberships.yaml +10 -0
  98. package/data/merchants/overrides.yaml +499 -1
  99. package/data/merchants/seed.yaml +843 -6
  100. package/dist/cli/import.js +2 -2
  101. package/dist/cli/wallet.js +8 -1
  102. package/dist/data/integrity.js +37 -0
  103. package/dist/data/wallet-edit.js +3 -1
  104. package/dist/engine/checklist.js +14 -3
  105. package/dist/engine/evaluate.js +19 -7
  106. package/dist/engine/intro.js +36 -0
  107. package/dist/engine/recommend.js +31 -2
  108. package/dist/engine/record.js +2 -0
  109. package/dist/engine/rule-text.js +15 -1
  110. package/dist/engine/setup.js +6 -0
  111. package/dist/engine/text.js +23 -0
  112. package/dist/engine/wallet-change.js +20 -3
  113. package/dist/engine/wallet-summary.js +24 -4
  114. package/dist/format/card-art.js +21 -0
  115. package/dist/format/cards.js +1 -0
  116. package/dist/format/wallet-yaml.js +1 -0
  117. package/dist/format/wallet.js +10 -4
  118. package/dist/import/analyze.js +1 -0
  119. package/dist/mcp/inputs.js +3 -0
  120. package/dist/mcp/schemas.js +17 -0
  121. package/dist/mcp/tools/account.js +3 -1
  122. package/dist/mcp/tools/wallet.js +6 -2
  123. package/dist/schema/card.js +23 -0
  124. package/dist/schema/issuer.js +4 -0
  125. package/dist/schema/wallet.js +3 -0
  126. package/package.json +12 -3
@@ -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 === 'cardmember_year' ? 'this cardmember year' : `for ${cap.currentPeriod ?? ''}`}${cap.until === null ? '' : `, through ${shortDate(cap.until)}`} (cap key ${cap.key}).`);
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)} for ${cap.period}, ${money(left)} left.`);
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
  ? [
@@ -87,6 +87,7 @@ export function analyzeStatement(catalog, wallet, entries, today, alreadyRecorde
87
87
  cardId: held.id,
88
88
  milestoneId: p.milestone.id,
89
89
  period: p.period,
90
+ periodKind: p.milestone.period,
90
91
  spend: p.milestone.spend,
91
92
  before: p.spent,
92
93
  after: p.spent,
@@ -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()
@@ -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({
@@ -136,7 +136,9 @@ export function registerAccountTools(server, catalog, store, ctx) {
136
136
  annotations: {
137
137
  title: 'Switch wallets',
138
138
  readOnlyHint: false,
139
- destructiveHint: false,
139
+ // It signs out this app's other connections to the wallet, which only
140
+ // the recovery code undoes.
141
+ destructiveHint: true,
140
142
  idempotentHint: true,
141
143
  openWorldHint: false,
142
144
  },
@@ -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)
@@ -179,7 +183,7 @@ export function registerWalletTools(server, deps, ctx, inputs) {
179
183
  });
180
184
  server.registerTool('update_card', {
181
185
  title: 'Update a card',
182
- 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}`,
183
187
  inputSchema: CardChange,
184
188
  outputSchema: WalletChangeSchema,
185
189
  annotations: {
@@ -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
@@ -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);
@@ -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.11",
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",