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.
Files changed (53) hide show
  1. package/README.md +52 -17
  2. package/data/CHANGELOG.md +8 -0
  3. package/data/VERSION +1 -1
  4. package/data/categories.yaml +22 -7
  5. package/dist/brand.js +2 -3
  6. package/dist/cli/args.js +67 -45
  7. package/dist/cli/cheatsheet.js +36 -35
  8. package/dist/cli/explore.js +47 -163
  9. package/dist/cli/import.js +3 -8
  10. package/dist/cli/offers.js +14 -56
  11. package/dist/cli/run.js +90 -20
  12. package/dist/cli/wallet.js +47 -6
  13. package/dist/data/file-store.js +95 -0
  14. package/dist/data/load.js +10 -2
  15. package/dist/data/store.js +92 -0
  16. package/dist/data/wallet-edit.js +93 -4
  17. package/dist/engine/bonus.js +15 -12
  18. package/dist/engine/cheat-sheet.js +59 -4
  19. package/dist/engine/evaluate.js +34 -10
  20. package/dist/engine/explore.js +16 -28
  21. package/dist/engine/offers.js +8 -21
  22. package/dist/engine/periods.js +9 -0
  23. package/dist/engine/recommend.js +17 -53
  24. package/dist/engine/record.js +34 -4
  25. package/dist/engine/simulate.js +33 -19
  26. package/dist/engine/text.js +53 -0
  27. package/dist/engine/transient.js +26 -0
  28. package/dist/engine/valuation.js +1 -1
  29. package/dist/engine/wallet-change.js +36 -15
  30. package/dist/engine/wallet-summary.js +4 -8
  31. package/dist/format/card-text.js +79 -0
  32. package/dist/format/cheat-sheet.js +19 -0
  33. package/dist/format/explore.js +114 -0
  34. package/dist/format/offers.js +33 -0
  35. package/dist/{cli/format.js → format/recommendation.js} +9 -11
  36. package/dist/format/wallet-yaml.js +45 -0
  37. package/dist/format/wallet.js +38 -0
  38. package/dist/import/analyze.js +17 -19
  39. package/dist/mcp/context.js +92 -0
  40. package/dist/mcp/inputs.js +148 -0
  41. package/dist/mcp/schemas.js +8 -2
  42. package/dist/mcp/serve.js +3 -1
  43. package/dist/mcp/server.js +43 -660
  44. package/dist/mcp/tools/account.js +71 -0
  45. package/dist/mcp/tools/answers.js +329 -0
  46. package/dist/mcp/tools/wallet.js +128 -0
  47. package/dist/merchants/from-env.js +1 -1
  48. package/dist/merchants/resolve.js +3 -3
  49. package/dist/schema/catalog.js +1 -0
  50. package/package.json +8 -3
  51. package/dist/data/wallet-setup.js +0 -60
  52. package/dist/data/wallet-write.js +0 -140
  53. /package/dist/{cli → format}/cards.js +0 -0
@@ -1,140 +0,0 @@
1
- import { isDeepStrictEqual } from 'node:util';
2
- import { readFile, rename, writeFile } from 'node:fs/promises';
3
- import { isMap, isSeq, parseDocument } from 'yaml';
4
- import { WalletSchema } from "../schema/wallet.js";
5
- const lineStart = (text, at) => text.lastIndexOf('\n', at - 1) + 1;
6
- function afterLine(text, at) {
7
- const end = text.indexOf('\n', at);
8
- return end === -1 ? text.length : end + 1;
9
- }
10
- // Inserts whole lines at a line boundary, adding the missing newline when the
11
- // file's last line has none.
12
- function insertLines(text, at, lines) {
13
- const head = text.slice(0, at);
14
- const gap = head.length > 0 && !head.endsWith('\n') ? '\n' : '';
15
- return `${head}${gap}${lines}${text.slice(at)}`;
16
- }
17
- const column = (text, at) => at - lineStart(text, at);
18
- function range(node) {
19
- const r = node?.range;
20
- if (!r)
21
- throw new Error('wallet YAML node has no source position');
22
- return r;
23
- }
24
- function flowUsage(booking) {
25
- const period = /^\d+$/.test(booking.period)
26
- ? `"${booking.period}"`
27
- : booking.period;
28
- return `{ period: ${period}, spent: ${String(booking.after)} }`;
29
- }
30
- // Splices one line into the file's text instead of re-serializing the YAML,
31
- // so comments, ordering and formatting elsewhere stay byte-for-byte the same.
32
- function spliceBooking(text, booking) {
33
- const doc = parseDocument(text);
34
- const cards = doc.get('cards', true);
35
- if (!isSeq(cards))
36
- throw new Error('"cards" isn\'t a list');
37
- const card = cards.items.find((item) => isMap(item) && item.get('id') === booking.cardId);
38
- if (!isMap(card))
39
- throw new Error(`no card '${booking.cardId}'`);
40
- const value = flowUsage(booking);
41
- const usage = card.get('cap_usage', true);
42
- if (isMap(usage) && !usage.flow && usage.items.length > 0) {
43
- const existing = usage.items.find((p) => p.key?.value === booking.capKey);
44
- if (existing) {
45
- const [start, end] = range(existing.value);
46
- return text.slice(0, start) + value + text.slice(end);
47
- }
48
- const first = usage.items[0];
49
- const indent = ' '.repeat(column(text, range(first?.key)[0]));
50
- const at = afterLine(text, range(usage)[1] - 1);
51
- return insertLines(text, at, `${indent}${booking.capKey}: ${value}\n`);
52
- }
53
- if (usage !== undefined) {
54
- throw new Error(`card '${booking.cardId}' has a cap_usage that isn't a block map; edit it by hand`);
55
- }
56
- const firstKey = card.items[0]?.key;
57
- const indent = ' '.repeat(column(text, range(firstKey)[0]));
58
- const at = afterLine(text, range(card)[1] - 1);
59
- return insertLines(text, at, `${indent}cap_usage:\n${indent} ${booking.capKey}: ${value}\n`);
60
- }
61
- function spliceBonusSpent(text, cardId, spent) {
62
- const doc = parseDocument(text);
63
- const cards = doc.get('cards', true);
64
- if (!isSeq(cards))
65
- throw new Error('"cards" isn\'t a list');
66
- const card = cards.items.find((item) => isMap(item) && item.get('id') === cardId);
67
- if (!isMap(card))
68
- throw new Error(`no card '${cardId}'`);
69
- const bonus = card.get('bonus', true);
70
- if (!isMap(bonus))
71
- throw new Error(`card '${cardId}' has no bonus`);
72
- const value = String(spent);
73
- const existing = bonus.items.find((p) => p.key?.value === 'spent');
74
- if (existing) {
75
- const [start, end] = range(existing.value);
76
- return text.slice(0, start) + value + text.slice(end);
77
- }
78
- if (bonus.flow) {
79
- const close = text.lastIndexOf('}', range(bonus)[1]);
80
- const before = text.slice(0, close).trimEnd();
81
- return `${before}, spent: ${value} ${text.slice(close)}`;
82
- }
83
- const first = bonus.items[0];
84
- const indent = ' '.repeat(column(text, range(first?.key)[0]));
85
- const at = afterLine(text, range(bonus)[1] - 1);
86
- return insertLines(text, at, `${indent}spent: ${value}\n`);
87
- }
88
- // Sets how much has been spent toward a card's welcome bonus.
89
- export async function writeBonusSpent(file, cardId, spent) {
90
- const before = await readFile(file, 'utf8');
91
- const after = spliceBonusSpent(before, cardId, spent);
92
- const oldWallet = WalletSchema.parse(parseDocument(before).toJS());
93
- const parsed = WalletSchema.safeParse(parseDocument(after).toJS());
94
- const expected = {
95
- ...oldWallet,
96
- cards: oldWallet.cards.map((c) => c.id === cardId && c.bonus ? { ...c, bonus: { ...c.bonus, spent } } : c),
97
- };
98
- if (!parsed.success || !isDeepStrictEqual(parsed.data, expected)) {
99
- throw new Error(`couldn't update ${file} safely; set the bonus's spent to ${String(spent)} by hand`);
100
- }
101
- const tmp = `${file}.${String(process.pid)}.tmp`;
102
- await writeFile(tmp, after);
103
- await rename(tmp, file);
104
- }
105
- const queues = new Map();
106
- // Runs `task` after every earlier task for the same file has settled, so a
107
- // read-modify-write of the wallet never interleaves with another.
108
- export function withWalletLock(file, task) {
109
- const previous = queues.get(file) ?? Promise.resolve();
110
- const run = previous.then(task, task);
111
- queues.set(file, run.catch(() => undefined));
112
- return run;
113
- }
114
- export async function writeCapBooking(file, booking) {
115
- const before = await readFile(file, 'utf8');
116
- const after = spliceBooking(before, booking);
117
- const oldWallet = WalletSchema.parse(parseDocument(before).toJS());
118
- const parsed = WalletSchema.safeParse(parseDocument(after).toJS());
119
- const expected = {
120
- ...oldWallet,
121
- cards: oldWallet.cards.map((c) => c.id === booking.cardId
122
- ? {
123
- ...c,
124
- cap_usage: {
125
- ...c.cap_usage,
126
- [booking.capKey]: {
127
- period: booking.period,
128
- spent: booking.after,
129
- },
130
- },
131
- }
132
- : c),
133
- };
134
- if (!parsed.success || !isDeepStrictEqual(parsed.data, expected)) {
135
- throw new Error(`couldn't update ${file} safely; add the cap usage by hand: ${booking.capKey}: ${flowUsage(booking)}`);
136
- }
137
- const tmp = `${file}.${String(process.pid)}.tmp`;
138
- await writeFile(tmp, after);
139
- await rename(tmp, file);
140
- }
File without changes