earnrate 0.1.4 → 0.2.1
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 +52 -17
- package/data/CHANGELOG.md +8 -0
- package/data/VERSION +1 -1
- package/data/categories.yaml +22 -7
- package/dist/brand.js +2 -3
- package/dist/cli/args.js +67 -45
- package/dist/cli/cheatsheet.js +36 -35
- package/dist/cli/explore.js +47 -163
- package/dist/cli/import.js +3 -8
- package/dist/cli/offers.js +14 -56
- package/dist/cli/run.js +90 -20
- package/dist/cli/wallet.js +47 -6
- package/dist/data/file-store.js +95 -0
- package/dist/data/load.js +10 -2
- package/dist/data/store.js +92 -0
- package/dist/data/wallet-edit.js +93 -4
- package/dist/engine/bonus.js +15 -12
- package/dist/engine/cheat-sheet.js +59 -4
- package/dist/engine/evaluate.js +34 -10
- package/dist/engine/explore.js +16 -28
- package/dist/engine/offers.js +8 -21
- package/dist/engine/periods.js +9 -0
- package/dist/engine/recommend.js +17 -53
- package/dist/engine/record.js +34 -4
- package/dist/engine/simulate.js +33 -19
- package/dist/engine/text.js +53 -0
- package/dist/engine/transient.js +26 -0
- package/dist/engine/valuation.js +1 -1
- package/dist/engine/wallet-change.js +36 -15
- package/dist/engine/wallet-summary.js +4 -8
- package/dist/format/card-text.js +79 -0
- package/dist/format/cheat-sheet.js +19 -0
- package/dist/format/explore.js +114 -0
- package/dist/format/offers.js +33 -0
- package/dist/{cli/format.js → format/recommendation.js} +9 -11
- package/dist/format/wallet-yaml.js +45 -0
- package/dist/format/wallet.js +38 -0
- package/dist/import/analyze.js +17 -19
- package/dist/mcp/context.js +92 -0
- package/dist/mcp/inputs.js +148 -0
- package/dist/mcp/schemas.js +8 -2
- package/dist/mcp/serve.js +3 -1
- package/dist/mcp/server.js +43 -660
- package/dist/mcp/tools/account.js +71 -0
- package/dist/mcp/tools/answers.js +329 -0
- package/dist/mcp/tools/wallet.js +128 -0
- package/dist/merchants/from-env.js +1 -1
- package/dist/merchants/resolve.js +3 -3
- package/dist/schema/catalog.js +1 -0
- package/package.json +8 -3
- package/dist/data/wallet-setup.js +0 -60
- package/dist/data/wallet-write.js +0 -140
- /package/dist/{cli → format}/cards.js +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ALL_PERSONAL_CARDS } from "../engine/explore.js";
|
|
3
|
+
import { VALUATION_PROFILES } from "../engine/profiles.js";
|
|
4
|
+
import { knownMemberships } from "../engine/setup.js";
|
|
5
|
+
import { IsoDate } from "../schema/common.js";
|
|
6
|
+
export const Merchant = z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.describe('Merchant name or web address as the user would say it or see it, e.g. "Whole Foods" or "delta.com".');
|
|
10
|
+
export const ChannelInput = z
|
|
11
|
+
.enum(['online', 'in_store'])
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('How the purchase is made. Omit if unknown; the answer says what it assumed.');
|
|
14
|
+
export const DateInput = IsoDate.optional().describe('Purchase date, YYYY-MM-DD. Defaults to today.');
|
|
15
|
+
export const CardId = z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.describe('The card id from search_cards, e.g. chase-sapphire-preferred.');
|
|
19
|
+
// The filters every catalog-wide answer takes.
|
|
20
|
+
const OfferFilters = {
|
|
21
|
+
kind: z
|
|
22
|
+
.enum(['any', 'cash', 'points'])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('cash: cash back cards only. points: points and miles cards only.'),
|
|
25
|
+
noAnnualFee: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Only cards with no annual fee.'),
|
|
29
|
+
business: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Business cards instead of personal ones.'),
|
|
33
|
+
};
|
|
34
|
+
const NamedCard = z.object({
|
|
35
|
+
cardId: z.string().min(1).describe('A card id from search_cards.'),
|
|
36
|
+
picks: z
|
|
37
|
+
.record(z.string(), z.array(z.string()).min(1))
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Bonus categories the user picked, for cards that have them: choice id → option ids, from search_cards. Ask; don't guess."),
|
|
40
|
+
activated: z
|
|
41
|
+
.array(z.string())
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Bonuses the user has activated with the issuer, as rule ids from search_cards.'),
|
|
44
|
+
});
|
|
45
|
+
const cardFields = {
|
|
46
|
+
checkoutLabel: z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('How the card appears at checkout or in a browser wallet, e.g. "Amex Gold". Optional.'),
|
|
51
|
+
picks: z
|
|
52
|
+
.record(z.string(), z.array(z.string()).min(1))
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('For cards where the cardholder picks bonus categories: choice id → option ids, from search_cards, e.g. {"five-percent": ["tv-internet-streaming", "fast-food"]}. Saved for the current period. Ask the user; don\'t guess.'),
|
|
55
|
+
anniversary: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(/^\d{2}-\d{2}$/)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("The account's anniversary as MM-DD, for cards whose cap resets on it."),
|
|
60
|
+
activated: z
|
|
61
|
+
.array(z.string())
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Rule ids the user has activated with the issuer, such as rotating categories. Replaces the current list.'),
|
|
64
|
+
bonus: z
|
|
65
|
+
.object({
|
|
66
|
+
opened: IsoDate.describe('The day the account opened, YYYY-MM-DD.'),
|
|
67
|
+
spent: z
|
|
68
|
+
.number()
|
|
69
|
+
.nonnegative()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('Dollars already spent toward the bonus. Default 0.'),
|
|
72
|
+
spend: z
|
|
73
|
+
.number()
|
|
74
|
+
.positive()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Dollars of spending the bonus needs, only if the user's offer differs from the published one."),
|
|
77
|
+
by: IsoDate.optional().describe("The deadline, YYYY-MM-DD, only if the user's offer differs from the published one."),
|
|
78
|
+
})
|
|
79
|
+
.nullable()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("A welcome bonus the user is working toward, so purchases recorded on this card count toward it and answers can favor it. Ask when they opened the account; spending needed and the deadline default to the card's published offer. null stops tracking it."),
|
|
82
|
+
};
|
|
83
|
+
export const CardChange = z.object({ cardId: CardId, ...cardFields });
|
|
84
|
+
// Drops keys whose value is undefined, which optional zod fields produce and
|
|
85
|
+
// exactOptionalPropertyTypes won't accept.
|
|
86
|
+
export function defined(value) {
|
|
87
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
88
|
+
}
|
|
89
|
+
export function cardInput({ cardId, bonus, ...rest }) {
|
|
90
|
+
return {
|
|
91
|
+
id: cardId,
|
|
92
|
+
...defined(rest),
|
|
93
|
+
...(bonus === undefined ? {} : { bonus: bonus && defined(bonus) }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Inputs that list the catalog's categories, issuers and memberships.
|
|
97
|
+
export function makeInputs(catalog) {
|
|
98
|
+
const categoryIds = catalog.categories.map((c) => c.id);
|
|
99
|
+
const categoryList = catalog.categories
|
|
100
|
+
.map((c) => `${c.id} (${c.name})`)
|
|
101
|
+
.join(', ');
|
|
102
|
+
const membershipIds = knownMemberships(catalog);
|
|
103
|
+
const issuers = [...new Set(catalog.cards.map((c) => c.issuer))].sort();
|
|
104
|
+
const CategoryInput = z
|
|
105
|
+
.enum(categoryIds)
|
|
106
|
+
.optional()
|
|
107
|
+
.describe(`Only for a merchant earnrate may not know, and only if you're confident what kind of business it is: its spending category, e.g. grocery.supermarket for a supermarket, dining for a restaurant, gas for a gas station. Ignored when earnrate knows the merchant. Leave it out if unsure. Categories: ${categoryList}.`);
|
|
108
|
+
const CatalogCategory = z
|
|
109
|
+
.enum(categoryIds)
|
|
110
|
+
.describe(`A spending category: ${categoryList}.`);
|
|
111
|
+
const Memberships = z.array(z.enum(membershipIds));
|
|
112
|
+
const NamedCards = z
|
|
113
|
+
.array(NamedCard)
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("Cards the user named in this conversation, to answer for just those cards without saving anything. Leave out to use the user's saved wallet if they have one; with neither, the answer covers every card earnrate knows.");
|
|
116
|
+
const NamedMemberships = Memberships.optional().describe("Memberships the user has, such as amazon-prime, when they're named in the conversation rather than saved.");
|
|
117
|
+
const CatalogFilters = {
|
|
118
|
+
...OfferFilters,
|
|
119
|
+
issuer: z.enum(issuers).optional().describe("Only this issuer's cards."),
|
|
120
|
+
memberships: Memberships.optional().describe("Memberships the user has, such as amazon-prime. Rates that need one they don't have are listed separately."),
|
|
121
|
+
valuation: z
|
|
122
|
+
.enum(['flat', 'points-heavy'])
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('flat (default): points at their cash-out value, about 1¢. points-heavy: Amex MR and Chase UR at 1.7¢, for people who transfer points to airlines and hotels.'),
|
|
125
|
+
};
|
|
126
|
+
const AllMemberships = Memberships.describe(`Every membership the user has. Known: ${membershipIds.join(', ')}.`);
|
|
127
|
+
return {
|
|
128
|
+
CategoryInput,
|
|
129
|
+
CatalogCategory,
|
|
130
|
+
NamedCards,
|
|
131
|
+
NamedMemberships,
|
|
132
|
+
OfferFilters,
|
|
133
|
+
CatalogFilters,
|
|
134
|
+
AllMemberships,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// The catalog filters as exploreCards and simulate take them.
|
|
138
|
+
export const settings = (args) => ({
|
|
139
|
+
filters: {
|
|
140
|
+
...ALL_PERSONAL_CARDS,
|
|
141
|
+
kind: args.kind ?? 'any',
|
|
142
|
+
noAnnualFee: args.noAnnualFee ?? false,
|
|
143
|
+
business: args.business ?? false,
|
|
144
|
+
issuer: args.issuer ?? null,
|
|
145
|
+
},
|
|
146
|
+
memberships: args.memberships ?? [],
|
|
147
|
+
valuations: args.valuation ? VALUATION_PROFILES[args.valuation] : {},
|
|
148
|
+
});
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -17,7 +17,7 @@ export const MerchantResolutionSchema = z.object({
|
|
|
17
17
|
.nullable(),
|
|
18
18
|
channel: Channel.nullable(),
|
|
19
19
|
confidence: Confidence,
|
|
20
|
-
source: z.enum(['override', 'seed', 'llm', 'caller', 'none']),
|
|
20
|
+
source: z.enum(['override', 'seed', 'llm', 'caller', 'category', 'none']),
|
|
21
21
|
bookedVia: z.array(z.string()),
|
|
22
22
|
inference: z
|
|
23
23
|
.object({
|
|
@@ -42,7 +42,7 @@ const RankedCardSchema = z.object({
|
|
|
42
42
|
reasoning: z.array(z.string()),
|
|
43
43
|
warnings: z.array(z.string()),
|
|
44
44
|
});
|
|
45
|
-
|
|
45
|
+
const RecommendationSchema = z.object({
|
|
46
46
|
decision: z
|
|
47
47
|
.object({
|
|
48
48
|
cardId: z.string(),
|
|
@@ -277,6 +277,7 @@ const SimulationSchema = z.object({
|
|
|
277
277
|
notes: z.array(z.string()),
|
|
278
278
|
});
|
|
279
279
|
export const SpendingSimulationSchema = z.object({
|
|
280
|
+
notes: z.array(z.string()),
|
|
280
281
|
singles: z.array(SimulationSchema),
|
|
281
282
|
pairs: z.array(SimulationSchema.extend({ alternatives: z.array(z.string()) })),
|
|
282
283
|
current: SimulationSchema.nullable(),
|
|
@@ -289,6 +290,7 @@ export const SpendingSimulationSchema = z.object({
|
|
|
289
290
|
})),
|
|
290
291
|
});
|
|
291
292
|
export const CheatSheetSchema = z.object({
|
|
293
|
+
scope: z.enum(['wallet', 'all cards']),
|
|
292
294
|
quarter: z.string(),
|
|
293
295
|
date: z.string(),
|
|
294
296
|
rows: z.array(z.object({
|
|
@@ -301,3 +303,7 @@ export const CheatSheetSchema = z.object({
|
|
|
301
303
|
})),
|
|
302
304
|
notes: z.array(z.string()),
|
|
303
305
|
});
|
|
306
|
+
export const PurchaseAnswerSchema = RecommendationSchema.extend({
|
|
307
|
+
// Every card earnrate knows, ranked: set when no cards were given.
|
|
308
|
+
allCards: ExplorationSchema.nullable(),
|
|
309
|
+
});
|
package/dist/mcp/serve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
2
|
+
import { FileWalletStore } from "../data/file-store.js";
|
|
2
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
3
4
|
import { localToday as today } from "../engine/periods.js";
|
|
4
5
|
import { llmEnabled, openLlmSession } from "../merchants/from-env.js";
|
|
@@ -21,7 +22,8 @@ export async function serveMcp(env) {
|
|
|
21
22
|
}
|
|
22
23
|
serveStdio(() => createEarnrateServer({
|
|
23
24
|
catalog,
|
|
24
|
-
|
|
25
|
+
store: new FileWalletStore(catalog, walletPath(env)),
|
|
26
|
+
savedWallets: 'file',
|
|
25
27
|
today,
|
|
26
28
|
llm,
|
|
27
29
|
version,
|