earnrate 0.2.4 → 0.2.6
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 +18 -15
- package/data/CHANGELOG.md +79 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-blue-business-plus.yaml +14 -6
- package/data/cards/amex-blue-cash-everyday.yaml +12 -3
- package/data/cards/amex-blue-cash-preferred.yaml +17 -3
- package/data/cards/amex-gold.yaml +23 -3
- package/data/cards/amex-platinum.yaml +19 -3
- package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +15 -3
- package/data/cards/bank-of-america-atmos-rewards-business.yaml +19 -4
- package/data/cards/bank-of-america-atmos-rewards-summit.yaml +9 -2
- package/data/cards/bank-of-america-customized-cash-rewards.yaml +10 -4
- package/data/cards/bilt-obsidian.yaml +4 -3
- package/data/cards/capital-one-kohls-card.yaml +11 -2
- package/data/cards/capital-one-kohls-rewards-visa.yaml +8 -2
- package/data/cards/capital-one-savor.yaml +32 -10
- package/data/cards/capital-one-venture-x.yaml +19 -4
- package/data/cards/capital-one-venture.yaml +19 -5
- package/data/cards/chase-aeroplan.yaml +15 -3
- package/data/cards/chase-freedom-flex.yaml +50 -10
- package/data/cards/chase-freedom-unlimited.yaml +6 -2
- package/data/cards/chase-ihg-one-rewards-business.yaml +18 -2
- package/data/cards/chase-ihg-one-rewards-premier-select.yaml +14 -2
- package/data/cards/chase-ihg-one-rewards-premier.yaml +19 -2
- package/data/cards/chase-ihg-one-rewards.yaml +15 -2
- package/data/cards/chase-ink-business-cash.yaml +7 -3
- package/data/cards/chase-ink-business-preferred.yaml +9 -2
- package/data/cards/chase-prime-visa.yaml +24 -5
- package/data/cards/chase-sapphire-preferred.yaml +59 -6
- package/data/cards/chase-sapphire-reserve.yaml +12 -2
- package/data/cards/chase-southwest-performance-business.yaml +22 -4
- package/data/cards/chase-southwest-plus.yaml +13 -3
- package/data/cards/chase-southwest-premier-business.yaml +22 -5
- package/data/cards/chase-southwest-premier.yaml +13 -3
- package/data/cards/chase-southwest-priority.yaml +18 -2
- package/data/cards/chase-united-business.yaml +5 -2
- package/data/cards/chase-united-club-business.yaml +2 -2
- package/data/cards/chase-united-club.yaml +17 -4
- package/data/cards/chase-united-explorer.yaml +5 -2
- package/data/cards/chase-united-gateway.yaml +2 -2
- package/data/cards/chase-united-quest.yaml +12 -3
- package/data/cards/citi-aadvantage-business.yaml +10 -4
- package/data/cards/citi-aadvantage-executive.yaml +8 -8
- package/data/cards/citi-aadvantage-mileup.yaml +7 -4
- package/data/cards/citi-aadvantage-platinum-select.yaml +1 -1
- package/data/cards/citi-costco-anywhere-visa-business.yaml +6 -9
- package/data/cards/citi-costco-anywhere-visa.yaml +7 -10
- package/data/cards/citi-custom-cash.yaml +43 -12
- package/data/cards/citi-double-cash.yaml +9 -3
- package/data/cards/citi-my-best-buy-visa.yaml +6 -3
- package/data/cards/citi-strata-elite.yaml +12 -2
- package/data/cards/citi-strata-premier.yaml +24 -2
- package/data/cards/sofi-unlimited-2.yaml +17 -3
- package/data/cards/td-bank-nordstrom-credit-card.yaml +16 -3
- package/data/cards/td-bank-nordstrom-visa.yaml +23 -3
- package/data/cards/td-bank-target-circle-card.yaml +13 -4
- package/data/cards/usbank-cash-plus.yaml +29 -7
- package/data/cards/wells-fargo-active-cash.yaml +9 -2
- package/data/cards/wells-fargo-autograph.yaml +31 -7
- package/data/categories.yaml +12 -0
- package/data/currencies.yaml +2 -0
- package/data/issuers.yaml +32 -0
- package/data/memberships.yaml +33 -0
- package/data/merchants/overrides.yaml +25 -0
- package/data/merchants/seed.yaml +7 -0
- package/dist/cli/bin.js +10 -1
- package/dist/cli/cheatsheet.js +11 -6
- package/dist/cli/explore.js +17 -23
- package/dist/cli/import.js +10 -10
- package/dist/cli/offers.js +19 -8
- package/dist/cli/run.js +31 -20
- package/dist/cli/wallet.js +50 -5
- package/dist/data/file-store.js +9 -7
- package/dist/data/integrity.js +22 -2
- package/dist/data/load.js +13 -2
- package/dist/data/store.js +13 -22
- package/dist/data/wallet-edit.js +108 -8
- package/dist/engine/cheat-sheet.js +15 -19
- package/dist/engine/compare.js +10 -0
- package/dist/engine/evaluate.js +2 -4
- package/dist/engine/explore.js +13 -56
- package/dist/engine/memberships.js +41 -15
- package/dist/engine/offers.js +18 -5
- package/dist/engine/periods.js +18 -0
- package/dist/engine/profiles.js +8 -0
- package/dist/engine/recommend.js +23 -37
- package/dist/engine/setup.js +28 -40
- package/dist/engine/simulate.js +11 -10
- package/dist/engine/text.js +49 -23
- package/dist/engine/valuation.js +1 -1
- package/dist/engine/wallet-change.js +26 -11
- package/dist/engine/wallet-summary.js +85 -31
- package/dist/format/card-text.js +19 -36
- package/dist/format/cards.js +3 -3
- package/dist/format/explore.js +15 -16
- package/dist/format/offers.js +10 -15
- package/dist/format/recommendation.js +6 -6
- package/dist/format/wallet.js +5 -2
- package/dist/import/analyze.js +2 -1
- package/dist/mcp/context.js +61 -13
- package/dist/mcp/inputs.js +16 -7
- package/dist/mcp/schemas.js +9 -0
- package/dist/mcp/server.js +8 -1
- package/dist/mcp/tools/account.js +45 -17
- package/dist/mcp/tools/answers.js +84 -29
- package/dist/mcp/tools/wallet.js +109 -22
- package/dist/merchants/cache.js +0 -10
- package/dist/merchants/resolve.js +81 -4
- package/dist/schema/card.js +8 -12
- package/dist/schema/category.js +4 -0
- package/dist/schema/currency.js +2 -0
- package/dist/schema/issuer.js +8 -0
- package/dist/schema/membership.js +15 -0
- package/dist/schema/merchant.js +4 -0
- package/dist/schema/wallet.js +1 -1
- package/package.json +1 -1
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
# Issuer travel portals. Each issuer's own bonuses match them by id.
|
|
4
4
|
- id: chase-travel
|
|
5
|
+
kind: portal
|
|
5
6
|
name: Chase Travel
|
|
6
7
|
domains: [travelsecure.chase.com]
|
|
7
8
|
category: travel
|
|
8
9
|
- id: renowned-hotels-chase-travel
|
|
10
|
+
kind: booking
|
|
9
11
|
name: Renowned Hotels and Resorts (Chase Travel)
|
|
10
12
|
aliases: [renowned hotels, renowned hotels and resorts]
|
|
11
13
|
category: travel
|
|
12
14
|
note: Prepaid stays in the Renowned Hotels and Resorts collection for United cardmembers on Chase Travel.
|
|
13
15
|
- id: costco-travel
|
|
16
|
+
kind: portal
|
|
14
17
|
name: Costco Travel
|
|
15
18
|
domains: [costcotravel.com]
|
|
16
19
|
category: travel
|
|
@@ -25,6 +28,7 @@
|
|
|
25
28
|
domains: [aadvantagecars.com]
|
|
26
29
|
category: travel
|
|
27
30
|
- id: amex-travel
|
|
31
|
+
kind: portal
|
|
28
32
|
name: American Express Travel
|
|
29
33
|
aliases: [amex travel]
|
|
30
34
|
domains: [amextravel.com]
|
|
@@ -42,15 +46,18 @@
|
|
|
42
46
|
# bank's own bonuses match these merchants by id. Bonuses that depend on what's
|
|
43
47
|
# booked get one merchant per booking type.
|
|
44
48
|
- id: amex-travel-hotels
|
|
49
|
+
kind: booking
|
|
45
50
|
name: Hotel through Amex Travel
|
|
46
51
|
aliases: [amex travel hotel, amex travel hotels, hotel amex travel]
|
|
47
52
|
category: travel
|
|
48
53
|
note: Prepaid hotels only for the Amex bonuses.
|
|
49
54
|
- id: amex-travel-flights
|
|
55
|
+
kind: booking
|
|
50
56
|
name: Flight through Amex Travel
|
|
51
57
|
aliases: [amex travel flight, amex travel flights, flight amex travel]
|
|
52
58
|
category: travel
|
|
53
59
|
- id: airbnb-amex-travel
|
|
60
|
+
kind: booking
|
|
54
61
|
name: Airbnb through Amex Travel
|
|
55
62
|
aliases:
|
|
56
63
|
[
|
|
@@ -62,18 +69,22 @@
|
|
|
62
69
|
category: travel
|
|
63
70
|
note: Prepaid Airbnb stays booked in AmexTravel.com or the Amex Travel app, with your Airbnb account linked to Amex.
|
|
64
71
|
- id: amex-travel-car-rentals
|
|
72
|
+
kind: booking
|
|
65
73
|
name: Car rental through Amex Travel
|
|
66
74
|
aliases: [amex travel car rental, amex travel car]
|
|
67
75
|
category: travel
|
|
68
76
|
- id: amex-travel-cruises
|
|
77
|
+
kind: booking
|
|
69
78
|
name: Cruise through Amex Travel
|
|
70
79
|
aliases: [amex travel cruise]
|
|
71
80
|
category: travel
|
|
72
81
|
- id: usbank-travel-center
|
|
82
|
+
kind: portal
|
|
73
83
|
name: U.S. Bank Travel Center
|
|
74
84
|
aliases: [us bank travel center]
|
|
75
85
|
category: travel
|
|
76
86
|
- id: sofi-travel
|
|
87
|
+
kind: portal
|
|
77
88
|
name: SoFi Travel
|
|
78
89
|
category: travel
|
|
79
90
|
|
|
@@ -177,38 +188,45 @@
|
|
|
177
188
|
# Card issuers' travel and ticketing sites (see the Amex Travel note above).
|
|
178
189
|
- {
|
|
179
190
|
id: capital-one-travel,
|
|
191
|
+
kind: portal,
|
|
180
192
|
name: Capital One Travel,
|
|
181
193
|
aliases: [capital one travel],
|
|
182
194
|
category: travel,
|
|
183
195
|
}
|
|
184
196
|
- {
|
|
185
197
|
id: capital-one-travel-hotels,
|
|
198
|
+
kind: booking,
|
|
186
199
|
name: Hotel through Capital One Travel,
|
|
187
200
|
aliases: [capital one travel hotel],
|
|
188
201
|
category: travel,
|
|
189
202
|
}
|
|
190
203
|
- id: capital-one-travel-vacation-rentals
|
|
204
|
+
kind: booking
|
|
191
205
|
name: Vacation rental through Capital One Travel
|
|
192
206
|
aliases: [capital one travel vacation rental]
|
|
193
207
|
category: travel
|
|
194
208
|
- {
|
|
195
209
|
id: capital-one-travel-car-rentals,
|
|
210
|
+
kind: booking,
|
|
196
211
|
name: Car rental through Capital One Travel,
|
|
197
212
|
aliases: [capital one travel car rental],
|
|
198
213
|
category: travel,
|
|
199
214
|
}
|
|
200
215
|
- {
|
|
201
216
|
id: capital-one-travel-flights,
|
|
217
|
+
kind: booking,
|
|
202
218
|
name: Flight through Capital One Travel,
|
|
203
219
|
aliases: [capital one travel flight],
|
|
204
220
|
category: travel,
|
|
205
221
|
}
|
|
206
222
|
- id: capital-one-entertainment
|
|
223
|
+
kind: portal
|
|
207
224
|
name: Capital One Entertainment
|
|
208
225
|
aliases: [capital one entertainment tickets]
|
|
209
226
|
category: entertainment
|
|
210
227
|
- {
|
|
211
228
|
id: citi-travel,
|
|
229
|
+
kind: portal,
|
|
212
230
|
name: Citi Travel,
|
|
213
231
|
aliases: [citi travel],
|
|
214
232
|
domains: [cititravel.com],
|
|
@@ -216,42 +234,49 @@
|
|
|
216
234
|
}
|
|
217
235
|
- {
|
|
218
236
|
id: citi-travel-hotels,
|
|
237
|
+
kind: booking,
|
|
219
238
|
name: Hotel through Citi Travel,
|
|
220
239
|
aliases: [citi travel hotel],
|
|
221
240
|
category: travel,
|
|
222
241
|
}
|
|
223
242
|
- {
|
|
224
243
|
id: citi-travel-car-rentals,
|
|
244
|
+
kind: booking,
|
|
225
245
|
name: Car rental through Citi Travel,
|
|
226
246
|
aliases: [citi travel car rental],
|
|
227
247
|
category: travel,
|
|
228
248
|
}
|
|
229
249
|
- {
|
|
230
250
|
id: citi-travel-attractions,
|
|
251
|
+
kind: booking,
|
|
231
252
|
name: Attraction through Citi Travel,
|
|
232
253
|
aliases: [citi travel attraction],
|
|
233
254
|
category: travel,
|
|
234
255
|
}
|
|
235
256
|
- {
|
|
236
257
|
id: citi-travel-flights,
|
|
258
|
+
kind: booking,
|
|
237
259
|
name: Flight through Citi Travel,
|
|
238
260
|
aliases: [citi travel flight],
|
|
239
261
|
category: travel,
|
|
240
262
|
}
|
|
241
263
|
- {
|
|
242
264
|
id: bilt-travel,
|
|
265
|
+
kind: portal,
|
|
243
266
|
name: Bilt Travel,
|
|
244
267
|
aliases: [bilt travel],
|
|
245
268
|
category: travel,
|
|
246
269
|
}
|
|
247
270
|
- {
|
|
248
271
|
id: bilt-travel-hotels,
|
|
272
|
+
kind: booking,
|
|
249
273
|
name: Hotel through Bilt Travel,
|
|
250
274
|
aliases: [bilt travel hotel],
|
|
251
275
|
category: travel,
|
|
252
276
|
}
|
|
253
277
|
- {
|
|
254
278
|
id: bilt-travel-flights,
|
|
279
|
+
kind: booking,
|
|
255
280
|
name: Flight through Bilt Travel,
|
|
256
281
|
aliases: [bilt travel flight],
|
|
257
282
|
category: travel,
|
package/data/merchants/seed.yaml
CHANGED
|
@@ -104,6 +104,13 @@
|
|
|
104
104
|
category: streaming,
|
|
105
105
|
}
|
|
106
106
|
- { id: max, name: Max, aliases: [hbo max], category: streaming }
|
|
107
|
+
- { id: peacock, name: Peacock, aliases: [peacock tv], category: streaming }
|
|
108
|
+
- {
|
|
109
|
+
id: showtime,
|
|
110
|
+
name: Showtime,
|
|
111
|
+
aliases: [paramount plus with showtime],
|
|
112
|
+
category: streaming,
|
|
113
|
+
}
|
|
107
114
|
- { id: spotify, name: Spotify, category: streaming }
|
|
108
115
|
- {
|
|
109
116
|
id: apple-tv,
|
package/dist/cli/bin.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { styleText } from 'node:util';
|
|
3
|
+
import { CLI_NAME, WALLET_ENV_VAR } from "../brand.js";
|
|
3
4
|
import { localToday } from "../engine/periods.js";
|
|
4
5
|
import { run } from "./run.js";
|
|
5
|
-
if (process.argv[2] === 'mcp') {
|
|
6
|
+
if (process.argv[2] === 'mcp' && /^(-h|--help)$/.test(process.argv[3] ?? '')) {
|
|
7
|
+
process.stdout.write(`Usage: ${CLI_NAME} mcp
|
|
8
|
+
|
|
9
|
+
Runs earnrate as an MCP server over stdio, for an AI assistant to start. It
|
|
10
|
+
uses the same wallet file as the command line (${WALLET_ENV_VAR}, or
|
|
11
|
+
~/.earnrate/wallet.yaml).
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
else if (process.argv[2] === 'mcp') {
|
|
6
15
|
const { serveMcp } = await import("../mcp/serve.js");
|
|
7
16
|
await serveMcp(process.env);
|
|
8
17
|
}
|
package/dist/cli/cheatsheet.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ALL_PERSONAL_CARDS } from "../engine/explore.js";
|
|
|
6
6
|
import { isValuationProfile, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
7
7
|
import { formatCheatSheet } from "../format/cheat-sheet.js";
|
|
8
8
|
import { cardList, dateProblem, valuationProblem } from "./args.js";
|
|
9
|
-
import { cardsToUse, NO_WALLET_HINT, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
|
|
9
|
+
import { cardsToUse, NO_WALLET_HINT, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
10
10
|
const CHEATSHEET_HELP = `Usage: ${CLI_NAME} cheatsheet [options]
|
|
11
11
|
|
|
12
12
|
The card to use for each everyday kind of purchase this quarter: from your
|
|
@@ -15,6 +15,7 @@ name with --cards, or else from every card earnrate knows.
|
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
17
|
--cards <ids> Use these cards instead of your wallet, comma separated.
|
|
18
|
+
--every-card Pick from every card earnrate knows, even with a wallet.
|
|
18
19
|
--member <name> A membership you have, like amazon-prime or costco. Repeatable.
|
|
19
20
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
20
21
|
--date <YYYY-MM-DD> The day to check. Default: today.
|
|
@@ -30,6 +31,7 @@ export async function runCheatSheet(argv, io) {
|
|
|
30
31
|
valuation: { type: 'string' },
|
|
31
32
|
date: { type: 'string' },
|
|
32
33
|
cards: { type: 'string' },
|
|
34
|
+
'every-card': { type: 'boolean' },
|
|
33
35
|
member: { type: 'string', multiple: true },
|
|
34
36
|
json: { type: 'boolean' },
|
|
35
37
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -45,7 +47,9 @@ export async function runCheatSheet(argv, io) {
|
|
|
45
47
|
io.stdout(CHEATSHEET_HELP);
|
|
46
48
|
return 0;
|
|
47
49
|
}
|
|
48
|
-
const problem =
|
|
50
|
+
const problem = values['every-card'] && values.cards !== undefined
|
|
51
|
+
? 'Pick --cards or --every-card, not both.'
|
|
52
|
+
: (valuationProblem(values.valuation) ?? dateProblem(values.date));
|
|
49
53
|
if (problem !== null) {
|
|
50
54
|
io.stderr(`${problem}\n`);
|
|
51
55
|
return USAGE_ERROR;
|
|
@@ -58,17 +62,18 @@ export async function runCheatSheet(argv, io) {
|
|
|
58
62
|
if (loaded === null)
|
|
59
63
|
return RUNTIME_ERROR;
|
|
60
64
|
const date = values.date ?? io.today;
|
|
61
|
-
if (loaded === 'none') {
|
|
65
|
+
if (loaded === 'none' || values['every-card']) {
|
|
66
|
+
const saved = await savedSettings(io);
|
|
62
67
|
const sheet = catalogCheatSheet(catalog, date, {
|
|
63
68
|
filters: ALL_PERSONAL_CARDS,
|
|
64
|
-
memberships: values.member ??
|
|
69
|
+
memberships: values.member ?? saved.memberships,
|
|
65
70
|
valuations: values.valuation !== undefined && isValuationProfile(values.valuation)
|
|
66
71
|
? VALUATION_PROFILES[values.valuation]
|
|
67
|
-
:
|
|
72
|
+
: saved.valuations,
|
|
68
73
|
});
|
|
69
74
|
io.stdout(values.json
|
|
70
75
|
? `${JSON.stringify(sheet, null, 2)}\n`
|
|
71
|
-
: `${formatCheatSheet(sheet)}
|
|
76
|
+
: `${formatCheatSheet(sheet)}${loaded === 'none' ? `\n${NO_WALLET_HINT}\n` : ''}`);
|
|
72
77
|
return 0;
|
|
73
78
|
}
|
|
74
79
|
const wallet = values.valuation !== undefined && isValuationProfile(values.valuation)
|
package/dist/cli/explore.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { CLI_NAME } from "../brand.js";
|
|
3
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
4
|
-
import { ALL_PERSONAL_CARDS, exploreCards,
|
|
5
|
-
import { isValuationProfile, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
6
|
-
import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor,
|
|
4
|
+
import { ALL_PERSONAL_CARDS, exploreCards, } from "../engine/explore.js";
|
|
5
|
+
import { isValuationProfile, profileText, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
6
|
+
import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor, typicalSpend, TYPICAL_SPEND_NOTE, } from "../engine/simulate.js";
|
|
7
7
|
import { formatAdditions, formatCatalogSimulation, formatExploration, } from "../format/explore.js";
|
|
8
8
|
import { BOTH_CHANNELS, cardList, channelOf, dateProblem, membershipProblem, splitAmount, valuationProblem, } from "./args.js";
|
|
9
|
-
import { cardsToUse, RUNTIME_ERROR, USAGE_ERROR } from "./run.js";
|
|
9
|
+
import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
10
10
|
const FILTER_OPTIONS = ` --cash, --points Cash back cards only, or points and miles cards only.
|
|
11
11
|
--no-fee Cards with no annual fee.
|
|
12
12
|
--issuer <id> One issuer's cards, e.g. chase or amex.
|
|
13
13
|
--business Business cards instead of personal ones.
|
|
14
14
|
--closed Include cards the issuer no longer takes applications for.
|
|
15
15
|
--member <name> A membership you have, e.g. amazon-prime or costco. Repeatable.
|
|
16
|
-
--valuation <profile> flat (
|
|
17
|
-
(
|
|
16
|
+
--valuation <profile> flat (${profileText('flat')}) or points-heavy
|
|
17
|
+
(${profileText('points-heavy')}), instead of your
|
|
18
|
+
saved point values.`;
|
|
18
19
|
const BEST_HELP = `Usage: ${CLI_NAME} best <merchant or category> [amount] [options]
|
|
19
20
|
|
|
20
21
|
Ranks every card in earnrate's catalog for one purchase, not just yours. Each
|
|
@@ -67,7 +68,7 @@ const FILTER_FLAGS = {
|
|
|
67
68
|
json: { type: 'boolean' },
|
|
68
69
|
help: { type: 'boolean', short: 'h' },
|
|
69
70
|
};
|
|
70
|
-
function settingsFrom(values, catalog) {
|
|
71
|
+
function settingsFrom(values, catalog, saved) {
|
|
71
72
|
if (values.cash && values.points)
|
|
72
73
|
return 'Pick --cash or --points, not both.';
|
|
73
74
|
const problem = valuationProblem(values.valuation) ??
|
|
@@ -86,10 +87,10 @@ function settingsFrom(values, catalog) {
|
|
|
86
87
|
issuer: values.issuer ?? null,
|
|
87
88
|
includeClosed: values.closed ?? false,
|
|
88
89
|
},
|
|
89
|
-
memberships: values.member ??
|
|
90
|
+
memberships: values.member ?? saved.memberships,
|
|
90
91
|
valuations: values.valuation !== undefined && isValuationProfile(values.valuation)
|
|
91
92
|
? VALUATION_PROFILES[values.valuation]
|
|
92
|
-
:
|
|
93
|
+
: saved.valuations,
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
export async function runBest(argv, io) {
|
|
@@ -135,22 +136,21 @@ export async function runBest(argv, io) {
|
|
|
135
136
|
return USAGE_ERROR;
|
|
136
137
|
}
|
|
137
138
|
const catalog = await loadCatalog();
|
|
138
|
-
const settings = settingsFrom(values, catalog);
|
|
139
|
+
const settings = settingsFrom(values, catalog, await savedSettings(io));
|
|
139
140
|
if (typeof settings === 'string') {
|
|
140
141
|
io.stderr(`${settings}\n`);
|
|
141
142
|
return USAGE_ERROR;
|
|
142
143
|
}
|
|
143
|
-
const category = findCategory(catalog, text);
|
|
144
144
|
const result = exploreCards(catalog, {
|
|
145
|
-
merchant:
|
|
146
|
-
category,
|
|
145
|
+
merchant: text,
|
|
146
|
+
category: null,
|
|
147
147
|
amount,
|
|
148
148
|
channel: channelOf(values),
|
|
149
149
|
date: values.date ?? io.today,
|
|
150
150
|
}, settings);
|
|
151
151
|
io.stdout(values.json
|
|
152
152
|
? `${JSON.stringify(result, null, 2)}\n`
|
|
153
|
-
: formatExploration(result, values.all ?? false));
|
|
153
|
+
: formatExploration(result, catalog, values.all ?? false));
|
|
154
154
|
return 0;
|
|
155
155
|
}
|
|
156
156
|
function parseSpend(catalog, args) {
|
|
@@ -159,13 +159,7 @@ function parseSpend(catalog, args) {
|
|
|
159
159
|
const match = /^(.+)=\$?(\d+(?:\.\d+)?)$/.exec(arg);
|
|
160
160
|
if (!match?.[1] || match[2] === undefined)
|
|
161
161
|
return `"${arg}" isn't <what>=<dollars a month>, e.g. groceries=600.`;
|
|
162
|
-
|
|
163
|
-
const category = findCategory(catalog, what);
|
|
164
|
-
lines.push({
|
|
165
|
-
merchant: category === null ? what : null,
|
|
166
|
-
category,
|
|
167
|
-
monthly: Number(match[2]),
|
|
168
|
-
});
|
|
162
|
+
lines.push(spendLineFor(catalog, match[1].replace(/[-_]/g, ' '), Number(match[2])));
|
|
169
163
|
}
|
|
170
164
|
if (lines.length === 0)
|
|
171
165
|
return `Give your monthly spending, e.g. ${CLI_NAME} simulate groceries=600 dining=300 other=1000.`;
|
|
@@ -195,14 +189,14 @@ export async function runSimulate(argv, io) {
|
|
|
195
189
|
return 0;
|
|
196
190
|
}
|
|
197
191
|
const catalog = await loadCatalog();
|
|
198
|
-
const settings = settingsFrom(values, catalog);
|
|
192
|
+
const settings = settingsFrom(values, catalog, await savedSettings(io));
|
|
199
193
|
if (typeof settings === 'string') {
|
|
200
194
|
io.stderr(`${settings}\n`);
|
|
201
195
|
return USAGE_ERROR;
|
|
202
196
|
}
|
|
203
197
|
const typical = positionals.length === 0;
|
|
204
198
|
const spend = typical
|
|
205
|
-
?
|
|
199
|
+
? typicalSpend(catalog)
|
|
206
200
|
: parseSpend(catalog, positionals);
|
|
207
201
|
if (typeof spend === 'string') {
|
|
208
202
|
io.stderr(`${spend}\n`);
|
package/dist/cli/import.js
CHANGED
|
@@ -12,6 +12,7 @@ import { cardValuation } from "../engine/valuation.js";
|
|
|
12
12
|
import { analyzeStatement, } from "../import/analyze.js";
|
|
13
13
|
import { parseStatement } from "../import/statement.js";
|
|
14
14
|
import { displayPath, readWallet, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
|
|
15
|
+
import { currencyName, dollarsAndCents } from "../engine/text.js";
|
|
15
16
|
const IMPORT_HELP = `Usage: ${CLI_NAME} import <statement.csv> --card <card-id> [--apply]
|
|
16
17
|
${CLI_NAME} import <statement.csv>:<card-id> [<statement.csv>:<card-id> ...]
|
|
17
18
|
|
|
@@ -44,7 +45,6 @@ const LedgerSchema = z.strictObject({
|
|
|
44
45
|
const fingerprint = ({ cardId, line }, kind = 'cap') => createHash('sha256')
|
|
45
46
|
.update(`${kind === 'bonus' ? 'bonus|' : ''}${cardId}|${line.date}|${String(line.amount)}|${line.description}`)
|
|
46
47
|
.digest('hex');
|
|
47
|
-
const usd = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
48
48
|
// "statement.csv:chase-sapphire-preferred" names the card inline.
|
|
49
49
|
function splitTarget(arg) {
|
|
50
50
|
const m = /^(.+):([a-z0-9]+(?:-[a-z0-9]+)*)$/.exec(arg);
|
|
@@ -64,7 +64,7 @@ function valuationNote(catalog, wallet, cardIds, profile) {
|
|
|
64
64
|
}
|
|
65
65
|
if (values.size === 0)
|
|
66
66
|
return 'Cash back counted as cash.';
|
|
67
|
-
const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${catalog
|
|
67
|
+
const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${currencyName(catalog, id)}`);
|
|
68
68
|
const list = parts.length > 1
|
|
69
69
|
? `${parts.slice(0, -1).join(', ')} and ${parts.at(-1) ?? ''}`
|
|
70
70
|
: (parts[0] ?? '');
|
|
@@ -77,20 +77,20 @@ function formatAnalysis(a, labelOf, valuation, categoryName) {
|
|
|
77
77
|
several
|
|
78
78
|
? `${String(a.byCard.length)} statements (${names.join(', ')}) · ${a.from} to ${a.to}`
|
|
79
79
|
: `${names[0] ?? ''} statement · ${a.from} to ${a.to}`,
|
|
80
|
-
`${String(a.purchases)} purchases, ${
|
|
80
|
+
`${String(a.purchases)} purchases, ${dollarsAndCents(a.spend)}${a.refunds > 0 ? ` (refunds and credits: ${dollarsAndCents(a.refunds)})` : ''}`,
|
|
81
81
|
'',
|
|
82
82
|
];
|
|
83
83
|
if (several) {
|
|
84
|
-
out.push(`Earned on the cards you used: about ${
|
|
84
|
+
out.push(`Earned on the cards you used: about ${dollarsAndCents(a.earned)}`);
|
|
85
85
|
const width = Math.max(...names.map((n) => n.length));
|
|
86
86
|
for (const t of a.byCard) {
|
|
87
|
-
out.push(` ${labelOf(t.cardId).padEnd(width)} ${String(t.purchases).padStart(4)} × ${
|
|
87
|
+
out.push(` ${labelOf(t.cardId).padEnd(width)} ${String(t.purchases).padStart(4)} × ${dollarsAndCents(t.spend).padStart(11)} ${dollarsAndCents(t.earned)}`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
91
|
-
out.push(`Earned on ${names[0] ?? ''}: about ${
|
|
91
|
+
out.push(`Earned on ${names[0] ?? ''}: about ${dollarsAndCents(a.earned)}`);
|
|
92
92
|
}
|
|
93
|
-
out.push(`With earnrate's picks: about ${
|
|
93
|
+
out.push(`With earnrate's picks: about ${dollarsAndCents(a.best)} (+${dollarsAndCents(a.best - a.earned)})`, valuation);
|
|
94
94
|
if (a.missed.length > 0) {
|
|
95
95
|
out.push('', 'Where another card would have earned more:');
|
|
96
96
|
const shown = a.missed.slice(0, 10);
|
|
@@ -99,7 +99,7 @@ function formatAnalysis(a, labelOf, valuation, categoryName) {
|
|
|
99
99
|
const cardWidth = Math.max(...shown.map((m) => labelOf(m.usedCardId).length));
|
|
100
100
|
for (const m of shown) {
|
|
101
101
|
const used = several ? `${labelOf(m.usedCardId).padEnd(cardWidth)} ` : '';
|
|
102
|
-
out.push(` ${name(m).slice(0, width).padEnd(width)} ${used}${String(m.count).padStart(3)} × ${
|
|
102
|
+
out.push(` ${name(m).slice(0, width).padEnd(width)} ${used}${String(m.count).padStart(3)} × ${dollarsAndCents(m.spend).padStart(10)} ${dollarsAndCents(m.earned)} → ${m.bestCard} ${dollarsAndCents(m.best)} (+${dollarsAndCents(m.best - m.earned)})`);
|
|
103
103
|
}
|
|
104
104
|
if (a.missed.length > 10)
|
|
105
105
|
out.push(` …and ${String(a.missed.length - 10)} more merchants`);
|
|
@@ -117,7 +117,7 @@ function formatAnalysis(a, labelOf, valuation, categoryName) {
|
|
|
117
117
|
const last = new Map();
|
|
118
118
|
for (const { cardId, booking } of a.bookings)
|
|
119
119
|
last.set(several ? `${labelOf(cardId)} ${booking.capKey}` : booking.capKey, booking.after);
|
|
120
|
-
out.push(`Cap usage for the current period: ${[...last].map(([k, v]) => `${k} ${
|
|
120
|
+
out.push(`Cap usage for the current period: ${[...last].map(([k, v]) => `${k} ${dollarsAndCents(v)}`).join(', ')}.`);
|
|
121
121
|
}
|
|
122
122
|
else if (a.alreadyImported > 0) {
|
|
123
123
|
out.push(`Caps: nothing new to record. ${String(a.alreadyImported)} purchase(s) were recorded by an earlier import.`);
|
|
@@ -129,7 +129,7 @@ function formatAnalysis(a, labelOf, valuation, categoryName) {
|
|
|
129
129
|
out.push('Caps: nothing new to record.');
|
|
130
130
|
}
|
|
131
131
|
for (const b of a.bonuses) {
|
|
132
|
-
out.push(`Welcome bonus on ${labelOf(b.cardId)}: ${
|
|
132
|
+
out.push(`Welcome bonus on ${labelOf(b.cardId)}: ${dollarsAndCents(b.before)} → ${dollarsAndCents(b.after)} of ${dollarsAndCents(b.spend)}${b.deadline ? ` by ${b.deadline}` : ''}.`);
|
|
133
133
|
}
|
|
134
134
|
return `${out.join('\n')}\n`;
|
|
135
135
|
}
|
package/dist/cli/offers.js
CHANGED
|
@@ -3,13 +3,15 @@ import { CLI_NAME } from "../brand.js";
|
|
|
3
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
4
4
|
import { compareOffers } from "../engine/offers.js";
|
|
5
5
|
import { isValuationProfile, withValuationProfile } from "../engine/profiles.js";
|
|
6
|
+
import { currencyName } from "../engine/text.js";
|
|
6
7
|
import { formatOffers } from "../format/offers.js";
|
|
7
|
-
import { valuationProblem } from "./args.js";
|
|
8
|
-
import { cardsToUse, RUNTIME_ERROR, USAGE_ERROR } from "./run.js";
|
|
8
|
+
import { cardList, valuationProblem } from "./args.js";
|
|
9
|
+
import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
9
10
|
const OFFERS_HELP = `Usage: ${CLI_NAME} offers [--cash | --points] [--no-fee] [--business] [options]
|
|
10
11
|
|
|
11
12
|
Ranks the welcome offers on cards you don't have by first-year value: the
|
|
12
|
-
bonus at your point values minus the first year's annual fee
|
|
13
|
+
bonus at your point values minus the first year's annual fee and any
|
|
14
|
+
membership the card needs that you don't have.
|
|
13
15
|
|
|
14
16
|
Options:
|
|
15
17
|
--cash Cash back cards only.
|
|
@@ -17,6 +19,8 @@ Options:
|
|
|
17
19
|
--no-fee Cards with no annual fee.
|
|
18
20
|
--business Business cards instead of personal ones.
|
|
19
21
|
--held Include cards already in your wallet.
|
|
22
|
+
--cards <ids> Cards you have, instead of your wallet, comma separated.
|
|
23
|
+
--member <name> A membership you have, in place of the saved ones.
|
|
20
24
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
21
25
|
--all List every offer, not just the top 10.
|
|
22
26
|
--json Print the full comparison as JSON.
|
|
@@ -33,6 +37,8 @@ export async function runOffers(argv, io) {
|
|
|
33
37
|
'no-fee': { type: 'boolean' },
|
|
34
38
|
business: { type: 'boolean' },
|
|
35
39
|
held: { type: 'boolean' },
|
|
40
|
+
cards: { type: 'string' },
|
|
41
|
+
member: { type: 'string', multiple: true },
|
|
36
42
|
valuation: { type: 'string' },
|
|
37
43
|
all: { type: 'boolean' },
|
|
38
44
|
json: { type: 'boolean' },
|
|
@@ -60,12 +66,18 @@ export async function runOffers(argv, io) {
|
|
|
60
66
|
}
|
|
61
67
|
const catalog = await loadCatalog();
|
|
62
68
|
const found = await cardsToUse(catalog, walletPath(io.env), io, {
|
|
63
|
-
cards:
|
|
64
|
-
members: [],
|
|
69
|
+
cards: cardList(values.cards),
|
|
70
|
+
members: values.member ?? [],
|
|
65
71
|
});
|
|
66
72
|
if (found === null)
|
|
67
73
|
return RUNTIME_ERROR;
|
|
68
|
-
const loaded = found === 'none'
|
|
74
|
+
const loaded = found === 'none'
|
|
75
|
+
? await savedSettings(io).then((saved) => ({
|
|
76
|
+
valuations: saved.valuations,
|
|
77
|
+
memberships: values.member ?? saved.memberships,
|
|
78
|
+
cards: [],
|
|
79
|
+
}))
|
|
80
|
+
: found;
|
|
69
81
|
const wallet = values.valuation && isValuationProfile(values.valuation)
|
|
70
82
|
? withValuationProfile(loaded, values.valuation)
|
|
71
83
|
: loaded;
|
|
@@ -79,7 +91,6 @@ export async function runOffers(argv, io) {
|
|
|
79
91
|
io.stdout(`${JSON.stringify(comparison, null, 2)}\n`);
|
|
80
92
|
return 0;
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
io.stdout(formatOffers(comparison, currencyName, values.all ?? false));
|
|
94
|
+
io.stdout(formatOffers(comparison, (id) => currencyName(catalog, id), values.all ?? false));
|
|
84
95
|
return 0;
|
|
85
96
|
}
|
package/dist/cli/run.js
CHANGED
|
@@ -5,8 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { CLI_NAME, CONFIG_DIR, LLM_ENV_VAR, LLM_MAX_CALLS_ENV_VAR, LLM_MODEL_ENV_VAR, MERCHANT_CACHE_ENV_VAR, WALLET_ENV_VAR, } from "../brand.js";
|
|
6
6
|
import { walletProblems } from "../data/integrity.js";
|
|
7
7
|
import { loadCatalog, loadWallet, walletPath } from "../data/load.js";
|
|
8
|
-
import { ALL_PERSONAL_CARDS,
|
|
9
|
-
import { VALUATION_PROFILES, withValuationProfile } from "../engine/profiles.js";
|
|
8
|
+
import { ALL_PERSONAL_CARDS, exploreCards } from "../engine/explore.js";
|
|
9
|
+
import { profileText, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
10
10
|
import { recommend } from "../engine/recommend.js";
|
|
11
11
|
import { walletFromCards } from "../engine/transient.js";
|
|
12
12
|
import { formatExploration } from "../format/explore.js";
|
|
@@ -14,7 +14,7 @@ import { formatRecommendation, plain, } from "../format/recommendation.js";
|
|
|
14
14
|
import { DEFAULT_LLM_MODEL } from "../merchants/anthropic.js";
|
|
15
15
|
import { DEFAULT_MAX_CALLS } from "../merchants/fallback.js";
|
|
16
16
|
import { llmEnabled, openLlmSession } from "../merchants/from-env.js";
|
|
17
|
-
import {
|
|
17
|
+
import { resolvePlace, withCallerCategory } from "../merchants/resolve.js";
|
|
18
18
|
import { packageVersion } from "../version.js";
|
|
19
19
|
import { membershipProblem, parseCliArgs } from "./args.js";
|
|
20
20
|
import { runCheatSheet } from "./cheatsheet.js";
|
|
@@ -43,6 +43,7 @@ Save your cards (optional):
|
|
|
43
43
|
${CLI_NAME} update <card-id> [--pick ...] Change picks, label or activations.
|
|
44
44
|
${CLI_NAME} remove <card-id> Remove a card.
|
|
45
45
|
${CLI_NAME} memberships [names...] Show or set memberships (amazon-prime, costco).
|
|
46
|
+
${CLI_NAME} values [UR=1.5 ...] Show or set what your points are worth.
|
|
46
47
|
|
|
47
48
|
Other commands:
|
|
48
49
|
${CLI_NAME} cheatsheet The best card for each category this quarter.
|
|
@@ -53,7 +54,8 @@ Other commands:
|
|
|
53
54
|
|
|
54
55
|
Options:
|
|
55
56
|
--online, --in-store How you're paying. Default: inferred from the merchant.
|
|
56
|
-
--valuation <profile> flat (
|
|
57
|
+
--valuation <profile> flat (${profileText('flat')}) or points-heavy (${profileText('points-heavy')}),
|
|
58
|
+
instead of your saved point values.
|
|
57
59
|
Default: the values in your wallet.
|
|
58
60
|
--all List every card, not just the top three.
|
|
59
61
|
--json Print the full recommendation as JSON.
|
|
@@ -61,7 +63,7 @@ Options:
|
|
|
61
63
|
--country <code> Where you're paying. Default: US.
|
|
62
64
|
--cards <ids> Answer for these cards instead of your wallet, comma
|
|
63
65
|
separated: --cards amex-gold,chase-prime-visa.
|
|
64
|
-
--member <name> A membership you have,
|
|
66
|
+
--member <name> A membership you have, in place of the saved ones.
|
|
65
67
|
--category <id> For a merchant earnrate doesn't know: its category,
|
|
66
68
|
e.g. grocery.supermarket. Ignored for known merchants.
|
|
67
69
|
--llm, --no-llm Ask Claude to classify merchants earnrate doesn't know.
|
|
@@ -85,7 +87,9 @@ export function displayPath(file) {
|
|
|
85
87
|
return file.startsWith(`${home}/`) ? `~${file.slice(home.length)}` : file;
|
|
86
88
|
}
|
|
87
89
|
// The cards to answer for: those named with --cards, else the wallet file.
|
|
88
|
-
// 'none' means no
|
|
90
|
+
// 'none' means no cards saved or named, which answers from every card.
|
|
91
|
+
// Named cards use the saved memberships (unless --member names some) and the
|
|
92
|
+
// saved point values, as the MCP tools do.
|
|
89
93
|
export async function cardsToUse(catalog, file, io, named) {
|
|
90
94
|
const problem = membershipProblem(catalog, named.members);
|
|
91
95
|
if (problem !== null) {
|
|
@@ -93,15 +97,16 @@ export async function cardsToUse(catalog, file, io, named) {
|
|
|
93
97
|
return null;
|
|
94
98
|
}
|
|
95
99
|
if (named.cards !== null) {
|
|
100
|
+
const saved = await savedSettings(io);
|
|
96
101
|
const wallet = walletFromCards(catalog, {
|
|
97
102
|
cards: named.cards.map((id) => ({ id })),
|
|
98
|
-
memberships: named.members,
|
|
103
|
+
memberships: named.members.length > 0 ? named.members : saved.memberships,
|
|
99
104
|
}, io.today);
|
|
100
105
|
if (typeof wallet === 'string') {
|
|
101
106
|
io.stderr(`${wallet}\n`);
|
|
102
107
|
return null;
|
|
103
108
|
}
|
|
104
|
-
return wallet;
|
|
109
|
+
return { ...wallet, valuations: saved.valuations };
|
|
105
110
|
}
|
|
106
111
|
try {
|
|
107
112
|
await access(file);
|
|
@@ -117,9 +122,21 @@ export async function cardsToUse(catalog, file, io, named) {
|
|
|
117
122
|
io.stderr(`Your wallet at ${displayPath(file)} has problems:\n${problems.map((p) => ` - ${p}\n`).join('')}`);
|
|
118
123
|
return null;
|
|
119
124
|
}
|
|
120
|
-
return loaded;
|
|
125
|
+
return loaded.cards.length === 0 ? 'none' : loaded;
|
|
121
126
|
}
|
|
122
127
|
export const NO_WALLET_HINT = `These are every card earnrate knows. For the best of your own cards, name them with --cards amex-gold,chase-prime-visa, or save them: ${CLI_NAME} add <card-id> (${CLI_NAME} cards finds ids).`;
|
|
128
|
+
// The point values and memberships saved in the wallet, which every answer
|
|
129
|
+
// uses unless --valuation or --member says otherwise. No wallet, or one that
|
|
130
|
+
// can't be read, has none.
|
|
131
|
+
export async function savedSettings(io) {
|
|
132
|
+
try {
|
|
133
|
+
const { valuations, memberships } = await loadWallet(walletPath(io.env));
|
|
134
|
+
return { valuations, memberships };
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { valuations: {}, memberships: [] };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
123
140
|
export async function readWallet(file, io) {
|
|
124
141
|
try {
|
|
125
142
|
return await loadWallet(file);
|
|
@@ -180,14 +197,7 @@ export async function run(argv, io) {
|
|
|
180
197
|
io.stderr(`Unknown category "${options.category}". Use one of: ${catalog.categories.map((c) => c.id).join(', ')}.\n`);
|
|
181
198
|
return USAGE_ERROR;
|
|
182
199
|
}
|
|
183
|
-
|
|
184
|
-
const resolvedName = resolveMerchant(catalog, purchase.merchant, purchase.channel);
|
|
185
|
-
const named = resolvedName.source === 'none' && options.category === null
|
|
186
|
-
? findCategory(catalog, purchase.merchant)
|
|
187
|
-
: null;
|
|
188
|
-
let merchant = named === null
|
|
189
|
-
? withCallerCategory(resolvedName, options.category)
|
|
190
|
-
: categoryResolution(catalog, named, purchase.channel);
|
|
200
|
+
let merchant = withCallerCategory(resolvePlace(catalog, purchase.merchant, purchase.channel), options.category);
|
|
191
201
|
const notices = [];
|
|
192
202
|
if (llmOn && merchant.source === 'none') {
|
|
193
203
|
const opened = await openLlmSession({
|
|
@@ -205,6 +215,7 @@ export async function run(argv, io) {
|
|
|
205
215
|
notices.push(...resolved.notices);
|
|
206
216
|
}
|
|
207
217
|
if (loaded === 'none') {
|
|
218
|
+
const saved = await savedSettings(io);
|
|
208
219
|
const explored = exploreCards(catalog, {
|
|
209
220
|
merchant: purchase.merchant,
|
|
210
221
|
category: null,
|
|
@@ -213,16 +224,16 @@ export async function run(argv, io) {
|
|
|
213
224
|
date: purchase.date,
|
|
214
225
|
}, {
|
|
215
226
|
filters: ALL_PERSONAL_CARDS,
|
|
216
|
-
memberships: options.members,
|
|
227
|
+
memberships: options.members.length > 0 ? options.members : saved.memberships,
|
|
217
228
|
valuations: options.valuation
|
|
218
229
|
? VALUATION_PROFILES[options.valuation]
|
|
219
|
-
:
|
|
230
|
+
: saved.valuations,
|
|
220
231
|
}, merchant);
|
|
221
232
|
if (options.json) {
|
|
222
233
|
io.stdout(`${JSON.stringify(explored, null, 2)}\n`);
|
|
223
234
|
}
|
|
224
235
|
else {
|
|
225
|
-
io.stdout(`${formatExploration(explored, options.all)}\n${NO_WALLET_HINT}\n`);
|
|
236
|
+
io.stdout(`${formatExploration(explored, catalog, options.all)}\n${NO_WALLET_HINT}\n`);
|
|
226
237
|
}
|
|
227
238
|
for (const n of notices)
|
|
228
239
|
io.stderr(`${n}\n`);
|