earnrate 0.1.3 → 0.1.4
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 +6 -1
- package/dist/cli/cheatsheet.js +81 -0
- package/dist/cli/run.js +4 -0
- package/dist/engine/cheat-sheet.js +48 -0
- package/dist/mcp/schemas.js +13 -0
- package/dist/mcp/server.js +70 -18
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ Assumes Whole Foods Market codes as supermarkets (MCC 5411).
|
|
|
51
51
|
|
|
52
52
|
Options: `--online` or `--in-store`, `--valuation flat|points-heavy`, `--all`, `--json`, `--date`, `--country`. Run `earnrate --help` for details. To keep the wallet somewhere else, set `EARNRATE_WALLET` to its path.
|
|
53
53
|
|
|
54
|
+
`earnrate cheatsheet` prints the card to use for each everyday kind of purchase this quarter (groceries, dining, gas, Amazon, Costco, travel, streaming, drugstores, everything else), with your caps, picks and activations as they are now. It's also in the My wallet tab on [earnrate.dev](https://earnrate.dev).
|
|
55
|
+
|
|
54
56
|
### Your wallet
|
|
55
57
|
|
|
56
58
|
The wallet is a YAML file (`~/.earnrate/wallet.yaml`) listing your cards by ID, which is each card's file name in [`data/cards/`](data/cards). `earnrate add`, `update`, `remove` and `memberships` write it for you and keep your comments, or you can edit it by hand. Only the cards are required:
|
|
@@ -152,12 +154,13 @@ Spotted a wrong rate, or want a card added? [Open an issue](https://github.com/c
|
|
|
152
154
|
|
|
153
155
|
## Use it from an AI agent (MCP)
|
|
154
156
|
|
|
155
|
-
`earnrate mcp` runs earnrate as a local MCP server over stdio, with
|
|
157
|
+
`earnrate mcp` runs earnrate as a local MCP server over stdio, with thirteen tools:
|
|
156
158
|
|
|
157
159
|
- `best_card_for_purchase`: which card to use, with reasons
|
|
158
160
|
- `best_cards_for` and `simulate_spending`: compare every card earnrate knows, for one purchase or a year of spending, with no wallet needed
|
|
159
161
|
- `resolve_merchant`: how a merchant was identified
|
|
160
162
|
- `list_wallet`: your cards, caps left this period, chosen categories and welcome-bonus progress
|
|
163
|
+
- `cheat_sheet`: the card to use for each everyday kind of purchase this quarter
|
|
161
164
|
- `compare_welcome_offers`: the best public sign-up offers on cards you don't have
|
|
162
165
|
- `record_purchase`: books a purchase against its cap and any open welcome bonus in your wallet file, keeping your comments
|
|
163
166
|
- `search_cards`, `add_card`, `update_card`, `remove_card` and `set_memberships`: set up the wallet in conversation
|
|
@@ -166,6 +169,8 @@ There's no wallet to write first: tell the assistant which cards you have, and i
|
|
|
166
169
|
|
|
167
170
|
For a merchant earnrate doesn't know, the calling model can pass a `category` (e.g. `grocery.supermarket`). It's used only for unknown merchants and is labeled in the answer. The command line takes the same thing as `--category`.
|
|
168
171
|
|
|
172
|
+
**Claude Desktop, one click:** download [earnrate.mcpb](https://github.com/cwasley/earnrate/releases/latest/download/earnrate.mcpb) and open it; Claude Desktop installs it (Settings → Extensions). No terminal or Node install needed. Its one setting is where your wallet file lives, by default `~/.earnrate/wallet.yaml`, the same file the command line uses.
|
|
173
|
+
|
|
169
174
|
To add it to Claude Code:
|
|
170
175
|
|
|
171
176
|
```sh
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { CLI_NAME } from "../brand.js";
|
|
3
|
+
import { walletProblems } from "../data/integrity.js";
|
|
4
|
+
import { loadCatalog, walletPath } from "../data/load.js";
|
|
5
|
+
import { cheatSheet } from "../engine/cheat-sheet.js";
|
|
6
|
+
import { isValuationProfile, withValuationProfile } from "../engine/profiles.js";
|
|
7
|
+
import { displayPath, readWallet, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
|
|
8
|
+
export const CHEATSHEET_HELP = `Usage: ${CLI_NAME} cheatsheet [options]
|
|
9
|
+
|
|
10
|
+
The card in your wallet to use for each everyday kind of purchase this
|
|
11
|
+
quarter, with your caps, picks and activations as they are now.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
15
|
+
--date <YYYY-MM-DD> The day to check. Default: today.
|
|
16
|
+
--json Print the cheat sheet as JSON.
|
|
17
|
+
-h, --help Show this help.
|
|
18
|
+
`;
|
|
19
|
+
const pct = (n) => `${String(Number(n.toFixed(2)))}%`;
|
|
20
|
+
export function formatCheatSheet(sheet) {
|
|
21
|
+
const labelWidth = Math.max(...sheet.rows.map((r) => r.label.length));
|
|
22
|
+
const cardWidth = Math.max(...sheet.rows.map((r) => (r.cardName ?? 'none of your cards').length));
|
|
23
|
+
const lines = [`Your cheat sheet for ${sheet.quarter}`, ''];
|
|
24
|
+
for (const row of sheet.rows) {
|
|
25
|
+
lines.push(` ${row.label.padEnd(labelWidth)} ${(row.cardName ?? 'none of your cards').padEnd(cardWidth)} ${row.effectiveReturnPct === null ? '' : pct(row.effectiveReturnPct).padStart(5)}`.trimEnd());
|
|
26
|
+
if (row.note)
|
|
27
|
+
lines.push(` ${' '.repeat(labelWidth)} ! ${row.note}`);
|
|
28
|
+
}
|
|
29
|
+
if (sheet.notes.length > 0)
|
|
30
|
+
lines.push('', ...sheet.notes);
|
|
31
|
+
return `${lines.join('\n')}\n`;
|
|
32
|
+
}
|
|
33
|
+
export async function runCheatSheet(argv, io) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = parseArgs({
|
|
37
|
+
args: argv,
|
|
38
|
+
options: {
|
|
39
|
+
valuation: { type: 'string' },
|
|
40
|
+
date: { type: 'string' },
|
|
41
|
+
json: { type: 'boolean' },
|
|
42
|
+
help: { type: 'boolean', short: 'h' },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
io.stderr(`${e instanceof Error ? e.message : String(e)}\n${CHEATSHEET_HELP}`);
|
|
48
|
+
return USAGE_ERROR;
|
|
49
|
+
}
|
|
50
|
+
const { values } = parsed;
|
|
51
|
+
if (values.help) {
|
|
52
|
+
io.stdout(CHEATSHEET_HELP);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (values.valuation !== undefined && !isValuationProfile(values.valuation)) {
|
|
56
|
+
io.stderr(`Unknown valuation "${values.valuation}". Use flat or points-heavy.\n`);
|
|
57
|
+
return USAGE_ERROR;
|
|
58
|
+
}
|
|
59
|
+
if (values.date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(values.date)) {
|
|
60
|
+
io.stderr(`"${values.date}" isn't a date. Use YYYY-MM-DD.\n`);
|
|
61
|
+
return USAGE_ERROR;
|
|
62
|
+
}
|
|
63
|
+
const file = walletPath(io.env);
|
|
64
|
+
const loaded = await readWallet(file, io);
|
|
65
|
+
if (!loaded)
|
|
66
|
+
return RUNTIME_ERROR;
|
|
67
|
+
const catalog = await loadCatalog();
|
|
68
|
+
const problems = walletProblems(loaded, catalog);
|
|
69
|
+
if (problems.length > 0) {
|
|
70
|
+
io.stderr(`Your wallet at ${displayPath(file)} has problems:\n${problems.map((p) => ` - ${p}\n`).join('')}`);
|
|
71
|
+
return RUNTIME_ERROR;
|
|
72
|
+
}
|
|
73
|
+
const wallet = values.valuation !== undefined && isValuationProfile(values.valuation)
|
|
74
|
+
? withValuationProfile(loaded, values.valuation)
|
|
75
|
+
: loaded;
|
|
76
|
+
const sheet = cheatSheet(catalog, wallet, values.date ?? io.today);
|
|
77
|
+
io.stdout(values.json
|
|
78
|
+
? `${JSON.stringify(sheet, null, 2)}\n`
|
|
79
|
+
: formatCheatSheet(sheet));
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
package/dist/cli/run.js
CHANGED
|
@@ -12,6 +12,7 @@ import { llmEnabled, openLlmSession } from "../merchants/from-env.js";
|
|
|
12
12
|
import { resolveMerchant, withCallerCategory } from "../merchants/resolve.js";
|
|
13
13
|
import { packageVersion } from "../version.js";
|
|
14
14
|
import { parseCliArgs } from "./args.js";
|
|
15
|
+
import { runCheatSheet } from "./cheatsheet.js";
|
|
15
16
|
import { runBest, runSimulate } from "./explore.js";
|
|
16
17
|
import { runImport } from "./import.js";
|
|
17
18
|
import { runOffers } from "./offers.js";
|
|
@@ -38,6 +39,7 @@ Set up your wallet:
|
|
|
38
39
|
${CLI_NAME} memberships [names...] Show or set memberships (amazon-prime).
|
|
39
40
|
|
|
40
41
|
Other commands:
|
|
42
|
+
${CLI_NAME} cheatsheet Your best card for each category this quarter.
|
|
41
43
|
${CLI_NAME} import <statement.csv> --card <id> Replay a statement against your wallet.
|
|
42
44
|
${CLI_NAME} offers [--cash | --points] Rank welcome offers on cards you don't have.
|
|
43
45
|
${CLI_NAME} mcp Run the MCP server over stdio, for AI agents.
|
|
@@ -94,6 +96,8 @@ export async function run(argv, io) {
|
|
|
94
96
|
return runOffers(argv.slice(1), io);
|
|
95
97
|
if (argv[0] === 'best')
|
|
96
98
|
return runBest(argv.slice(1), io);
|
|
99
|
+
if (argv[0] === 'cheatsheet')
|
|
100
|
+
return runCheatSheet(argv.slice(1), io);
|
|
97
101
|
if (argv[0] === 'simulate')
|
|
98
102
|
return runSimulate(argv.slice(1), io);
|
|
99
103
|
const command = argv[0];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolveMerchantById } from "../merchants/resolve.js";
|
|
2
|
+
import { categoryResolution } from "./explore.js";
|
|
3
|
+
import { periodKey } from "./periods.js";
|
|
4
|
+
import { recommend } from "./recommend.js";
|
|
5
|
+
import { setupNotes } from "./setup.js";
|
|
6
|
+
export const CHEAT_SHEET_ITEMS = [
|
|
7
|
+
{ label: 'Groceries', category: 'grocery.supermarket' },
|
|
8
|
+
{ label: 'Dining', category: 'dining' },
|
|
9
|
+
{ label: 'Gas', category: 'gas' },
|
|
10
|
+
{ label: 'Amazon', merchant: 'amazon' },
|
|
11
|
+
{ label: 'Costco', merchant: 'costco' },
|
|
12
|
+
{ label: 'Travel', category: 'travel' },
|
|
13
|
+
{ label: 'Streaming', category: 'streaming' },
|
|
14
|
+
{ label: 'Drugstores', category: 'drugstore' },
|
|
15
|
+
{ label: 'Everything else', category: 'other' },
|
|
16
|
+
];
|
|
17
|
+
// The best card in the wallet for each everyday kind of purchase on `date`,
|
|
18
|
+
// with caps, picks and activations as the wallet has them.
|
|
19
|
+
export function cheatSheet(catalog, wallet, date, items = CHEAT_SHEET_ITEMS) {
|
|
20
|
+
const rows = items.map((item) => {
|
|
21
|
+
const merchant = 'merchant' in item
|
|
22
|
+
? resolveMerchantById(catalog, item.merchant, null)
|
|
23
|
+
: categoryResolution(catalog, item.category, null);
|
|
24
|
+
if (!merchant)
|
|
25
|
+
throw new Error(`unknown merchant '${item.label}'`);
|
|
26
|
+
const rec = recommend(catalog, wallet, {
|
|
27
|
+
merchant: merchant.input,
|
|
28
|
+
amount: null,
|
|
29
|
+
channel: null,
|
|
30
|
+
date,
|
|
31
|
+
country: 'US',
|
|
32
|
+
}, merchant);
|
|
33
|
+
const top = rec.ranked.find((r) => r.cardId === rec.decision?.cardId);
|
|
34
|
+
return {
|
|
35
|
+
label: item.label,
|
|
36
|
+
cardId: top?.cardId ?? null,
|
|
37
|
+
cardName: top ? (top.checkoutLabel ?? top.cardName) : null,
|
|
38
|
+
effectiveReturnPct: top?.effectiveReturnPct ?? null,
|
|
39
|
+
matchedRule: top?.matchedRule ?? null,
|
|
40
|
+
note: top?.warnings[0] ?? null,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const notes = [
|
|
44
|
+
...new Set(wallet.cards.flatMap((held) => setupNotes(catalog, wallet, held, date).map((n) => n.text))),
|
|
45
|
+
];
|
|
46
|
+
const [year, q] = (periodKey('quarter', date, null) ?? '').split('-');
|
|
47
|
+
return { quarter: `${q ?? ''} ${year ?? ''}`, date, rows, notes };
|
|
48
|
+
}
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -288,3 +288,16 @@ export const SpendingSimulationSchema = z.object({
|
|
|
288
288
|
gain: z.number(),
|
|
289
289
|
})),
|
|
290
290
|
});
|
|
291
|
+
export const CheatSheetSchema = z.object({
|
|
292
|
+
quarter: z.string(),
|
|
293
|
+
date: z.string(),
|
|
294
|
+
rows: z.array(z.object({
|
|
295
|
+
label: z.string(),
|
|
296
|
+
cardId: z.string().nullable(),
|
|
297
|
+
cardName: z.string().nullable(),
|
|
298
|
+
effectiveReturnPct: z.number().nullable(),
|
|
299
|
+
matchedRule: z.string().nullable(),
|
|
300
|
+
note: z.string().nullable(),
|
|
301
|
+
})),
|
|
302
|
+
notes: z.array(z.string()),
|
|
303
|
+
});
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { MCP_SERVER_NAME } from "../brand.js";
|
|
4
4
|
import { formatCardInfo, formatNotes } from "../cli/cards.js";
|
|
5
5
|
import { formatRecommendation } from "../cli/format.js";
|
|
6
|
+
import { formatCheatSheet } from "../cli/cheatsheet.js";
|
|
6
7
|
import { formatAdditions, formatCatalogSimulation, formatExploration, } from "../cli/explore.js";
|
|
7
8
|
import { formatOffers } from "../cli/offers.js";
|
|
8
9
|
import { walletProblems } from "../data/integrity.js";
|
|
@@ -10,6 +11,7 @@ import { loadWallet } from "../data/load.js";
|
|
|
10
11
|
import { withWalletLock, writeBonusSpent, writeCapBooking, } from "../data/wallet-write.js";
|
|
11
12
|
import { addCard, removeCard, setMemberships, updateCard, } from "../data/wallet-setup.js";
|
|
12
13
|
import { bonusStatus, countsTowardBonus } from "../engine/bonus.js";
|
|
14
|
+
import { cheatSheet } from "../engine/cheat-sheet.js";
|
|
13
15
|
import { ALL_PERSONAL_CARDS, exploreCards } from "../engine/explore.js";
|
|
14
16
|
import { compareOffers } from "../engine/offers.js";
|
|
15
17
|
import { VALUATION_PROFILES } from "../engine/profiles.js";
|
|
@@ -20,7 +22,7 @@ import { knownMemberships, searchCards } from "../engine/setup.js";
|
|
|
20
22
|
import { walletSummary } from "../engine/wallet-summary.js";
|
|
21
23
|
import { resolveMerchant, withCallerCategory } from "../merchants/resolve.js";
|
|
22
24
|
import { IsoDate } from "../schema/common.js";
|
|
23
|
-
import { CardSearchSchema, ExplorationSchema, MerchantResolutionSchema, SpendingSimulationSchema, RecommendationSchema, OfferComparisonSchema, RecordResultSchema, WalletChangeSchema, WalletSummarySchema, } from "./schemas.js";
|
|
25
|
+
import { CardSearchSchema, CheatSheetSchema, ExplorationSchema, MerchantResolutionSchema, SpendingSimulationSchema, RecommendationSchema, OfferComparisonSchema, RecordResultSchema, WalletChangeSchema, WalletSummarySchema, } from "./schemas.js";
|
|
24
26
|
const Merchant = z
|
|
25
27
|
.string()
|
|
26
28
|
.min(1)
|
|
@@ -41,6 +43,7 @@ const dollars = (n) => `$${n.toLocaleString('en-US', { maximumFractionDigits: 2
|
|
|
41
43
|
export function createEarnrateServer(deps) {
|
|
42
44
|
const server = new McpServer({ name: MCP_SERVER_NAME, version: deps.version }, { instructions: INSTRUCTIONS });
|
|
43
45
|
const { catalog } = deps;
|
|
46
|
+
const noWallet = `There's no wallet yet (${deps.walletFile}), which is normal before the user adds a card. Ask the user which credit cards they have, find each one with search_cards, and add it with add_card. best_cards_for and simulate_spending work without a wallet.`;
|
|
44
47
|
// Read on every call, so edits to the wallet file apply without a restart.
|
|
45
48
|
async function wallet() {
|
|
46
49
|
let loaded;
|
|
@@ -49,7 +52,7 @@ export function createEarnrateServer(deps) {
|
|
|
49
52
|
}
|
|
50
53
|
catch (e) {
|
|
51
54
|
if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {
|
|
52
|
-
return
|
|
55
|
+
return noWallet;
|
|
53
56
|
}
|
|
54
57
|
return `Couldn't read the wallet at ${deps.walletFile}: ${e instanceof Error ? e.message : String(e)}`;
|
|
55
58
|
}
|
|
@@ -58,6 +61,16 @@ export function createEarnrateServer(deps) {
|
|
|
58
61
|
? `The wallet at ${deps.walletFile} has problems:\n${problems.map((p) => `- ${p}`).join('\n')}`
|
|
59
62
|
: loaded;
|
|
60
63
|
}
|
|
64
|
+
// No wallet yet is the first-run state, not a failure: read-only tools
|
|
65
|
+
// answer from an empty wallet and say how to start one.
|
|
66
|
+
async function readableWallet() {
|
|
67
|
+
const w = await wallet();
|
|
68
|
+
if (typeof w !== 'string')
|
|
69
|
+
return { wallet: w, start: null };
|
|
70
|
+
return w === noWallet
|
|
71
|
+
? { wallet: { valuations: {}, memberships: [], cards: [] }, start: w }
|
|
72
|
+
: w;
|
|
73
|
+
}
|
|
61
74
|
// The caller's category is free and comes before the paid Claude fallback;
|
|
62
75
|
// neither ever overrides a merchant earnrate already knows.
|
|
63
76
|
async function resolve(merchant, channel, category) {
|
|
@@ -92,9 +105,10 @@ export function createEarnrateServer(deps) {
|
|
|
92
105
|
openWorldHint: deps.llm !== null,
|
|
93
106
|
},
|
|
94
107
|
}, async ({ merchant, amount, channel, date, category }) => {
|
|
95
|
-
const
|
|
96
|
-
if (typeof
|
|
97
|
-
return fail(
|
|
108
|
+
const read = await readableWallet();
|
|
109
|
+
if (typeof read === 'string')
|
|
110
|
+
return fail(read);
|
|
111
|
+
const w = read.wallet;
|
|
98
112
|
const purchase = {
|
|
99
113
|
merchant,
|
|
100
114
|
amount: amount ?? null,
|
|
@@ -104,6 +118,16 @@ export function createEarnrateServer(deps) {
|
|
|
104
118
|
};
|
|
105
119
|
const resolved = await resolve(merchant, purchase.channel, category ?? null);
|
|
106
120
|
const rec = recommend(catalog, w, purchase, resolved.merchant);
|
|
121
|
+
if (read.start !== null) {
|
|
122
|
+
const output = {
|
|
123
|
+
...rec,
|
|
124
|
+
warnings: [read.start, ...resolved.notices],
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: 'text', text: read.start }],
|
|
128
|
+
structuredContent: output,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
107
131
|
const output = {
|
|
108
132
|
...rec,
|
|
109
133
|
warnings: [...rec.warnings, ...resolved.notices],
|
|
@@ -146,10 +170,15 @@ export function createEarnrateServer(deps) {
|
|
|
146
170
|
outputSchema: WalletSummarySchema,
|
|
147
171
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
148
172
|
}, async () => {
|
|
149
|
-
const
|
|
150
|
-
if (typeof
|
|
151
|
-
return fail(
|
|
152
|
-
const summary = walletSummary(catalog,
|
|
173
|
+
const read = await readableWallet();
|
|
174
|
+
if (typeof read === 'string')
|
|
175
|
+
return fail(read);
|
|
176
|
+
const summary = walletSummary(catalog, read.wallet, deps.today());
|
|
177
|
+
if (read.start !== null)
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: 'text', text: read.start }],
|
|
180
|
+
structuredContent: summary,
|
|
181
|
+
};
|
|
153
182
|
const lines = summary.cards.map((c) => {
|
|
154
183
|
const caps = c.caps
|
|
155
184
|
.filter((cap) => cap.remaining !== null)
|
|
@@ -198,10 +227,10 @@ export function createEarnrateServer(deps) {
|
|
|
198
227
|
outputSchema: OfferComparisonSchema,
|
|
199
228
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
200
229
|
}, async (args) => {
|
|
201
|
-
const
|
|
202
|
-
if (typeof
|
|
203
|
-
return fail(
|
|
204
|
-
const comparison = compareOffers(catalog,
|
|
230
|
+
const read = await readableWallet();
|
|
231
|
+
if (typeof read === 'string')
|
|
232
|
+
return fail(read);
|
|
233
|
+
const comparison = compareOffers(catalog, read.wallet, {
|
|
205
234
|
kind: args.kind ?? 'any',
|
|
206
235
|
noAnnualFee: args.noAnnualFee ?? false,
|
|
207
236
|
business: args.business ?? false,
|
|
@@ -318,10 +347,11 @@ export function createEarnrateServer(deps) {
|
|
|
318
347
|
if (unknown.length > 0)
|
|
319
348
|
return fail(`earnrate doesn't know ${unknown.map((l) => `"${l.merchant.input}"`).join(', ')}. Give a category for those lines instead.`);
|
|
320
349
|
const options = { ...settings(args), start: deps.today() };
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
const read = args.compareToWallet ? await readableWallet() : null;
|
|
351
|
+
if (typeof read === 'string')
|
|
352
|
+
return fail(read);
|
|
353
|
+
if (read && read.start === null) {
|
|
354
|
+
const w = read.wallet;
|
|
325
355
|
const valued = args.valuation
|
|
326
356
|
? {
|
|
327
357
|
...w,
|
|
@@ -347,11 +377,12 @@ export function createEarnrateServer(deps) {
|
|
|
347
377
|
};
|
|
348
378
|
}
|
|
349
379
|
const result = simulateCatalog(catalog, lines, options);
|
|
380
|
+
const text = formatCatalogSimulation(result, catalog, false);
|
|
350
381
|
return {
|
|
351
382
|
content: [
|
|
352
383
|
{
|
|
353
384
|
type: 'text',
|
|
354
|
-
text:
|
|
385
|
+
text: read?.start ? `${read.start}\n\n${text}` : text,
|
|
355
386
|
},
|
|
356
387
|
],
|
|
357
388
|
structuredContent: {
|
|
@@ -362,6 +393,27 @@ export function createEarnrateServer(deps) {
|
|
|
362
393
|
},
|
|
363
394
|
};
|
|
364
395
|
});
|
|
396
|
+
server.registerTool('cheat_sheet', {
|
|
397
|
+
title: 'Cheat sheet',
|
|
398
|
+
description: "The card in the user's wallet to use for each everyday kind of purchase right now: groceries, dining, gas, Amazon, Costco, travel, streaming, drugstores and everything else, with caps, picks and activations as the wallet has them. Use it when the user asks which card to use for what, or wants a quick reference for the quarter. For one specific store, call best_card_for_purchase instead. Pass on the notes: they say what the wallet still needs, like this quarter's picks.",
|
|
399
|
+
inputSchema: z.object({ date: DateInput }),
|
|
400
|
+
outputSchema: CheatSheetSchema,
|
|
401
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
402
|
+
}, async (args) => {
|
|
403
|
+
const read = await readableWallet();
|
|
404
|
+
if (typeof read === 'string')
|
|
405
|
+
return fail(read);
|
|
406
|
+
const sheet = cheatSheet(catalog, read.wallet, args.date ?? deps.today());
|
|
407
|
+
if (read.start !== null)
|
|
408
|
+
return {
|
|
409
|
+
content: [{ type: 'text', text: read.start }],
|
|
410
|
+
structuredContent: { ...sheet, notes: [read.start] },
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
content: [{ type: 'text', text: formatCheatSheet(sheet) }],
|
|
414
|
+
structuredContent: sheet,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
365
417
|
server.registerTool('record_purchase', {
|
|
366
418
|
title: 'Record a purchase',
|
|
367
419
|
description: "Records a purchase the user has already made, so spending caps and welcome-bonus progress stay accurate for future recommendations. It updates the user's wallet file. Only call it after the purchase has happened, and only once per purchase: calling it twice counts the spend twice. Purchases with no cap and no open welcome bonus are acknowledged and nothing is written.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "earnrate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Picks the best credit card from your wallet for a merchant, and explains why",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"credit cards",
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
41
41
|
"prepack": "yarn build",
|
|
42
42
|
"smoke:pack": "node scripts/smoke-pack.ts",
|
|
43
|
-
"
|
|
43
|
+
"mcpb:pack": "node scripts/build-mcpb.ts",
|
|
44
|
+
"smoke:mcpb": "node scripts/smoke-mcpb.ts",
|
|
45
|
+
"site:build": "vite build site && node scripts/build-pages.ts",
|
|
44
46
|
"site:dev": "vite site",
|
|
45
47
|
"site:deploy": "yarn site:build && WRANGLER_SEND_METRICS=false wrangler deploy",
|
|
46
48
|
"test": "vitest run",
|
|
@@ -59,6 +61,7 @@
|
|
|
59
61
|
"zod": "^4.6.7"
|
|
60
62
|
},
|
|
61
63
|
"devDependencies": {
|
|
64
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
62
65
|
"@eslint/js": "^10.0.1",
|
|
63
66
|
"@modelcontextprotocol/client": "2.3.1",
|
|
64
67
|
"@types/node": "^24",
|