@warmio/mcp 11.1.0 → 11.2.0

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.
@@ -0,0 +1,281 @@
1
+ export const SPENDING_APP_URI = 'ui://warm/spending-v1.html';
2
+ export const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app';
3
+ export const SPENDING_APP_TOOL = 'get_transactions';
4
+ export const spendingAppToolMeta = {
5
+ ui: { resourceUri: SPENDING_APP_URI },
6
+ 'ui/resourceUri': SPENDING_APP_URI,
7
+ };
8
+ const darkTokens = `
9
+ --bg: var(--color-background-primary, #1a1a1a);
10
+ --fg: var(--color-text-primary, #f2f2f2);
11
+ --muted: var(--color-text-secondary, #a0a0a0);
12
+ --line: var(--color-border-tertiary, rgba(255, 255, 255, 0.1));
13
+ --bar: var(--color-text-primary, #f2f2f2);
14
+ --track: var(--color-background-secondary, rgba(255, 255, 255, 0.08));
15
+ `;
16
+ const styles = `
17
+ :root {
18
+ color-scheme: light dark;
19
+ --bg: var(--color-background-primary, #ffffff);
20
+ --fg: var(--color-text-primary, #141414);
21
+ --muted: var(--color-text-secondary, #6b6b6b);
22
+ --line: var(--color-border-tertiary, rgba(0, 0, 0, 0.08));
23
+ --bar: var(--color-text-primary, #141414);
24
+ --track: var(--color-background-secondary, rgba(0, 0, 0, 0.05));
25
+ --radius: var(--border-radius-md, 8px);
26
+ --font: var(--font-sans, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif);
27
+ }
28
+ @media (prefers-color-scheme: dark) {
29
+ :root:not([data-theme='light']) {${darkTokens}}
30
+ }
31
+ :root[data-theme='dark'] {${darkTokens}}
32
+ * { box-sizing: border-box; }
33
+ html, body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--font); }
34
+ body { padding: 16px; font-size: 14px; line-height: 20px; }
35
+ .head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 12px; }
36
+ .label { color: var(--muted); font-size: 13px; }
37
+ .total { font-size: 24px; line-height: 32px; font-weight: 600; font-variant-numeric: tabular-nums; }
38
+ .rows { display: flex; flex-direction: column; }
39
+ .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; padding: 8px; margin: 0 -8px; border: 0; border-radius: var(--radius); background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
40
+ .row:hover { background: var(--track); }
41
+ .row:focus-visible { outline: 2px solid var(--fg); outline-offset: -2px; }
42
+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
43
+ .amount { font-variant-numeric: tabular-nums; }
44
+ .track { grid-column: 1 / -1; height: 6px; border-radius: 3px; background: var(--track); overflow: hidden; }
45
+ .fill { height: 100%; border-radius: 3px; background: var(--bar); }
46
+ .empty { color: var(--muted); }
47
+ .foot { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
48
+ `;
49
+ const script = `
50
+ (() => {
51
+ const root = document.getElementById('app');
52
+ let nextId = 1;
53
+ const pending = new Map();
54
+ let context = null;
55
+
56
+ const post = (message) => window.parent.postMessage({ jsonrpc: '2.0', ...message }, '*');
57
+ const request = (method, params) => new Promise((resolve, reject) => {
58
+ const id = nextId++;
59
+ pending.set(id, { resolve, reject });
60
+ post({ id, method, params });
61
+ });
62
+ const notify = (method, params) => post({ method, params });
63
+
64
+ const applyHost = (host) => {
65
+ if (!host) return;
66
+ if (host.theme === 'light' || host.theme === 'dark') {
67
+ document.documentElement.dataset.theme = host.theme;
68
+ document.documentElement.style.colorScheme = host.theme;
69
+ }
70
+ const variables = host.styles && host.styles.variables;
71
+ if (variables) {
72
+ for (const [key, value] of Object.entries(variables)) {
73
+ if (typeof value === 'string' && key.startsWith('--')) {
74
+ document.documentElement.style.setProperty(key, value);
75
+ }
76
+ }
77
+ }
78
+ };
79
+
80
+ const formatMoney = (value, currency) => {
81
+ try {
82
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency, maximumFractionDigits: 0 }).format(value);
83
+ } catch {
84
+ return Math.round(value).toLocaleString();
85
+ }
86
+ };
87
+
88
+ const monthLabel = (month) => {
89
+ const [year, mon] = month.split('-').map(Number);
90
+ if (!year || !mon) return month;
91
+ return new Date(Date.UTC(year, mon - 1, 1)).toLocaleDateString(undefined, { month: 'long', year: 'numeric', timeZone: 'UTC' });
92
+ };
93
+
94
+ const describe = (data) => {
95
+ if (Array.isArray(data.periods)) {
96
+ const months = data.periods.map((period) => period.month).filter(Boolean);
97
+ const items = data.periods.flatMap((period) => period.items || []);
98
+ const label = months.length > 1 ? monthLabel(months[0]) + ' to ' + monthLabel(months[months.length - 1]) : monthLabel(months[0] || '');
99
+ return { items, label, scope: label };
100
+ }
101
+ if (typeof data.month === 'string') {
102
+ const label = monthLabel(data.month);
103
+ return { items: data.items || [], label, scope: label };
104
+ }
105
+ return { items: data.items || [], label: 'Last 10 days', scope: 'the last 10 days' };
106
+ };
107
+
108
+ const excluded = new Set(['INCOME', 'TRANSFER_IN', 'TRANSFER_OUT', 'LOAN_PAYMENTS', 'BANK_FEES']);
109
+
110
+ const categoryLabel = (category) => {
111
+ if (!category) return 'Uncategorized';
112
+ const words = category.toLowerCase().split('_').join(' ');
113
+ return words.charAt(0).toUpperCase() + words.slice(1);
114
+ };
115
+
116
+ const counted = (item) => {
117
+ if (!item) return false;
118
+ if (item.transfers_interpretation_version === 1 && item.is_internal_transfer === true) return false;
119
+ if (typeof item.category === 'string' && excluded.has(item.category)) return false;
120
+ return Number.isFinite(Number(item.amount));
121
+ };
122
+
123
+ const currencyOf = (item) => (typeof item.currency === 'string' && item.currency ? item.currency : 'USD');
124
+
125
+ const spendingRows = (items, currency) => {
126
+ const totals = new Map();
127
+ for (const item of items) {
128
+ if (currencyOf(item) !== currency) continue;
129
+ const category = typeof item.category === 'string' && item.category ? item.category : '';
130
+ totals.set(category, (totals.get(category) || 0) + Number(item.amount));
131
+ }
132
+ return [...totals.entries()]
133
+ .filter((entry) => entry[1] > 0.005)
134
+ .map((entry) => [categoryLabel(entry[0]), entry[1]])
135
+ .sort((a, b) => b[1] - a[1]);
136
+ };
137
+
138
+ const summarize = (items) => {
139
+ const kept = items.filter(counted);
140
+ const currencies = [...new Set(kept.map(currencyOf))];
141
+ let currency = 'USD';
142
+ let sorted = [];
143
+ let total = 0;
144
+ for (const candidate of currencies) {
145
+ const rows = spendingRows(kept, candidate);
146
+ const sum = rows.reduce((acc, entry) => acc + entry[1], 0);
147
+ if (sum > total) {
148
+ currency = candidate;
149
+ sorted = rows;
150
+ total = sum;
151
+ }
152
+ }
153
+ const top = sorted.slice(0, 6);
154
+ const rest = sorted.slice(6).reduce((acc, entry) => acc + entry[1], 0);
155
+ if (rest > 0) top.push(['Everything else', rest]);
156
+ return { rows: top, total, currency, mixed: currencies.length > 1 };
157
+ };
158
+
159
+ const element = (tag, className, text) => {
160
+ const node = document.createElement(tag);
161
+ if (className) node.className = className;
162
+ if (text !== undefined) node.textContent = text;
163
+ return node;
164
+ };
165
+
166
+ const ask = (text) => {
167
+ request('ui/message', { role: 'user', content: [{ type: 'text', text }] }).catch(() => undefined);
168
+ };
169
+
170
+ const render = (data) => {
171
+ root.replaceChildren();
172
+ if (!data || typeof data !== 'object' || data.error) {
173
+ root.append(element('div', 'empty', 'Warm could not load this spending.'));
174
+ return;
175
+ }
176
+ const view = describe(data);
177
+ const summary = summarize(view.items);
178
+ const head = element('div', 'head');
179
+ const left = element('div');
180
+ left.append(element('div', 'label', 'Spending · ' + view.label));
181
+ left.append(element('div', 'total', formatMoney(summary.total, summary.currency)));
182
+ head.append(left);
183
+ root.append(head);
184
+ if (summary.rows.length === 0) {
185
+ root.append(element('div', 'empty', 'No spending in this period.'));
186
+ return;
187
+ }
188
+ const max = summary.rows[0][1] || 1;
189
+ const rows = element('div', 'rows');
190
+ for (const [category, amount] of summary.rows) {
191
+ const row = element('button', 'row');
192
+ row.type = 'button';
193
+ row.append(element('span', 'name', category));
194
+ row.append(element('span', 'amount', formatMoney(amount, summary.currency)));
195
+ const track = element('span', 'track');
196
+ const fill = element('span', 'fill');
197
+ fill.style.display = 'block';
198
+ fill.style.width = Math.max(2, Math.round((amount / max) * 100)) + '%';
199
+ track.append(fill);
200
+ row.append(track);
201
+ row.addEventListener('click', () => {
202
+ const subject = category === 'Everything else' ? 'my smaller spending categories' : 'my ' + category.toLowerCase() + ' spending';
203
+ ask('Break down ' + subject + ' for ' + view.scope + '.');
204
+ });
205
+ rows.append(row);
206
+ }
207
+ root.append(rows);
208
+ const note = 'From Warm · Net of refunds. Income, transfers, loan payments and bank fees are left out' + (summary.mixed ? '. Only ' + summary.currency + ' is shown' : '');
209
+ root.append(element('div', 'foot', note));
210
+ };
211
+
212
+ const reportSize = () => {
213
+ const height = Math.ceil(document.documentElement.getBoundingClientRect().height);
214
+ notify('ui/notifications/size-changed', { width: Math.ceil(window.innerWidth), height });
215
+ };
216
+
217
+ window.addEventListener('message', (event) => {
218
+ if (event.source !== window.parent) return;
219
+ const message = event.data;
220
+ if (!message || message.jsonrpc !== '2.0') return;
221
+ if (message.id !== undefined && pending.has(message.id) && !message.method) {
222
+ const entry = pending.get(message.id);
223
+ pending.delete(message.id);
224
+ if (message.error) entry.reject(message.error);
225
+ else entry.resolve(message.result);
226
+ return;
227
+ }
228
+ if (message.method === 'ui/notifications/tool-result') {
229
+ const params = message.params || {};
230
+ render(params.isError ? { error: true } : params.structuredContent);
231
+ return;
232
+ }
233
+ if (message.method === 'ui/notifications/host-context-changed') {
234
+ context = { ...(context || {}), ...(message.params || {}) };
235
+ applyHost(context);
236
+ return;
237
+ }
238
+ if (message.method === 'ui/notifications/tool-cancelled') {
239
+ render({ error: true });
240
+ return;
241
+ }
242
+ if ((message.method === 'ui/resource-teardown' || message.method === 'ping') && message.id !== undefined) {
243
+ post({ id: message.id, result: {} });
244
+ return;
245
+ }
246
+ if (message.method && message.id !== undefined) {
247
+ post({ id: message.id, error: { code: -32601, message: 'Method not found' } });
248
+ }
249
+ });
250
+
251
+ new ResizeObserver(reportSize).observe(document.documentElement);
252
+
253
+ request('ui/initialize', {
254
+ appInfo: { name: 'Warm spending', version: '1.0.0' },
255
+ appCapabilities: {},
256
+ protocolVersion: '2026-01-26',
257
+ })
258
+ .then((result) => {
259
+ context = (result && result.hostContext) || null;
260
+ applyHost(context);
261
+ notify('ui/notifications/initialized', {});
262
+ reportSize();
263
+ })
264
+ .catch(() => undefined);
265
+ })();
266
+ `;
267
+ export const spendingAppHtml = `<!doctype html>
268
+ <html lang="en">
269
+ <head>
270
+ <meta charset="utf-8">
271
+ <meta name="viewport" content="width=device-width, initial-scale=1">
272
+ <title>Warm spending</title>
273
+ <style>${styles}</style>
274
+ </head>
275
+ <body>
276
+ <div id="app"><div class="empty">Loading spending…</div></div>
277
+ <script>${script}</script>
278
+ </body>
279
+ </html>
280
+ `;
281
+ //# sourceMappingURL=spending-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spending-app.js","sourceRoot":"","sources":["../src/spending-app.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAC7D,MAAM,CAAC,MAAM,iBAAiB,GAAG,2BAA2B,CAAC;AAC7D,MAAM,CAAC,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAEpD,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,EAAE,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE;IACrC,gBAAgB,EAAE,gBAAgB;CAC1B,CAAC;AAEX,MAAM,UAAU,GAAG;;;;;;;CAOlB,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;qCAasB,UAAU;;4BAEnB,UAAU;;;;;;;;;;;;;;;;;CAiBrC,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyNd,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;SAMtB,MAAM;;;;UAIL,MAAM;;;CAGf,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "11.1.0",
3
+ "version": "11.2.0",
4
4
  "description": "Warm local stdio MCP server",
5
5
  "type": "module",
6
6
  "engines": {
@@ -60,7 +60,7 @@
60
60
  "dependencies": {
61
61
  "@cfworker/json-schema": "4.1.1",
62
62
  "@modelcontextprotocol/sdk": "^1.32.1",
63
- "@warmio/contracts": "3.1.0",
63
+ "@warmio/contracts": "3.1.1",
64
64
  "dotenv": "^17.2.3",
65
65
  "jsonc-parser": "^3.3.1",
66
66
  "zod": "^3.25.76"