creditkarma-mcp 2.0.6 → 2.0.8

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 CHANGED
@@ -188,6 +188,10 @@ npm run test:coverage # coverage report (CI enforces 100% on src/**)
188
188
 
189
189
  Versions are bumped automatically by the **Tag & Bump** GitHub Action (`.github/workflows/tag-and-bump.yml`). Do not bump manually.
190
190
 
191
+ ### Pull requests
192
+
193
+ Changes land via PR, including for solo work — release notes are generated from merged PRs only (config in `.github/release.yml`). Apply one of these labels to every PR: `enhancement`, `bug`, `security`, `refactor`, `documentation`, `test`, `dependencies`, `ci`, or `ignore-for-release` (excludes from notes). The PR title becomes the changelog bullet, so write it like a user-facing entry.
194
+
191
195
  ### Project structure
192
196
 
193
197
  ```
package/SKILL.md CHANGED
@@ -149,3 +149,4 @@ sync_state (key, value)
149
149
  - Amounts: negative = expense/debit, positive = credit/income
150
150
  - `ck_query_sql` only allows SELECT — no writes to Credit Karma data
151
151
  - Sync saves a resume cursor — interrupted syncs can be resumed automatically
152
+ - `accounts.id` is a synthesized stable key in the form `<provider>|<last4>` (e.g. `Citi|2630`, `Ally|7133`) because CK's API returns empty `account.id` strings. The same card under two provider-name spellings shows as two rows.
@@ -0,0 +1,11 @@
1
+ export function deriveAccountId(account) {
2
+ if (account.id && account.id.trim() !== '')
3
+ return account.id;
4
+ const provider = (account.providerName ?? '').trim();
5
+ const last4 = extractLast4(account.accountTypeAndNumberDisplay ?? '');
6
+ return `${provider}|${last4}`;
7
+ }
8
+ function extractLast4(display) {
9
+ const m = display.match(/\(\.\.([^)]+)\)/);
10
+ return m?.[1] ?? display;
11
+ }
package/dist/bundle.js CHANGED
@@ -31118,6 +31118,20 @@ function sleep(ms) {
31118
31118
  import { DatabaseSync } from "node:sqlite";
31119
31119
  import { mkdirSync } from "fs";
31120
31120
  import { dirname as dirname2 } from "path";
31121
+
31122
+ // src/accountId.ts
31123
+ function deriveAccountId(account) {
31124
+ if (account.id && account.id.trim() !== "") return account.id;
31125
+ const provider = (account.providerName ?? "").trim();
31126
+ const last4 = extractLast4(account.accountTypeAndNumberDisplay ?? "");
31127
+ return `${provider}|${last4}`;
31128
+ }
31129
+ function extractLast4(display) {
31130
+ const m = display.match(/\(\.\.([^)]+)\)/);
31131
+ return m?.[1] ?? display;
31132
+ }
31133
+
31134
+ // src/db.ts
31121
31135
  var CURRENT_VERSION = 1;
31122
31136
  var MIGRATIONS = {
31123
31137
  1: `
@@ -31236,6 +31250,44 @@ function getSyncState(db, key) {
31236
31250
  function setSyncState(db, key, value) {
31237
31251
  db.prepare("INSERT INTO sync_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
31238
31252
  }
31253
+ function backfillAccountIds(db) {
31254
+ const rows = db.prepare("SELECT id, raw_json FROM transactions WHERE account_id IS NULL OR account_id = ''").all();
31255
+ if (rows.length === 0) return { txsUpdated: 0, accountsCreated: 0 };
31256
+ const accounts = /* @__PURE__ */ new Map();
31257
+ const updates = [];
31258
+ for (const row of rows) {
31259
+ if (!row.raw_json) continue;
31260
+ let parsed;
31261
+ try {
31262
+ parsed = JSON.parse(row.raw_json);
31263
+ } catch {
31264
+ continue;
31265
+ }
31266
+ if (!parsed.account) continue;
31267
+ const accountId = deriveAccountId(parsed.account);
31268
+ accounts.set(accountId, {
31269
+ id: accountId,
31270
+ name: parsed.account.name ?? "",
31271
+ type: parsed.account.type ?? null,
31272
+ providerName: parsed.account.providerName ?? null,
31273
+ display: parsed.account.accountTypeAndNumberDisplay ?? null
31274
+ });
31275
+ updates.push({ txId: row.id, accountId });
31276
+ }
31277
+ if (updates.length === 0) return { txsUpdated: 0, accountsCreated: 0 };
31278
+ db.exec("BEGIN");
31279
+ try {
31280
+ for (const acct of accounts.values()) upsertAccount(db, acct);
31281
+ const stmt = db.prepare("UPDATE transactions SET account_id = ? WHERE id = ?");
31282
+ for (const u of updates) stmt.run(u.accountId, u.txId);
31283
+ db.prepare("DELETE FROM accounts WHERE id = ''").run();
31284
+ db.exec("COMMIT");
31285
+ } catch (err) {
31286
+ db.exec("ROLLBACK");
31287
+ throw err;
31288
+ }
31289
+ return { txsUpdated: updates.length, accountsCreated: accounts.size };
31290
+ }
31239
31291
 
31240
31292
  // src/tools/auth.ts
31241
31293
  import { readFileSync as readFileSync2, writeFileSync, existsSync } from "fs";
@@ -31327,8 +31379,9 @@ async function handleSyncTransactions(args, ctx) {
31327
31379
  try {
31328
31380
  for (const tx of page.transactions) {
31329
31381
  const exists = ctx.db.prepare("SELECT id FROM transactions WHERE id = ?").get(tx.id);
31382
+ const accountId = deriveAccountId(tx.account);
31330
31383
  upsertAccount(ctx.db, {
31331
- id: tx.account.id,
31384
+ id: accountId,
31332
31385
  name: tx.account.name,
31333
31386
  type: tx.account.type,
31334
31387
  providerName: tx.account.providerName,
@@ -31342,7 +31395,7 @@ async function handleSyncTransactions(args, ctx) {
31342
31395
  description: tx.description,
31343
31396
  status: tx.status,
31344
31397
  amount: tx.amount.value,
31345
- accountId: tx.account.id,
31398
+ accountId,
31346
31399
  categoryId: tx.category?.id ?? null,
31347
31400
  merchantId: tx.merchant?.id ?? null,
31348
31401
  rawJson: JSON.stringify(tx)
@@ -31712,13 +31765,18 @@ async function main() {
31712
31765
  if (refreshToken && isJwtExpired(refreshToken)) {
31713
31766
  console.error("[creditkarma-mcp] Warning: refresh token in CK_COOKIES has expired. Run `npm run auth` (or call ck_set_session) to capture fresh credentials.");
31714
31767
  }
31768
+ const db = initDb(dbPath);
31769
+ const repaired = backfillAccountIds(db);
31770
+ if (repaired.txsUpdated > 0) {
31771
+ console.error(`[creditkarma-mcp] Repaired ${repaired.txsUpdated} transactions across ${repaired.accountsCreated} accounts (CK returned empty account.id for legacy rows).`);
31772
+ }
31715
31773
  const ctx = {
31716
31774
  client: new CreditKarmaClient(token, refreshToken, cookies),
31717
- db: initDb(dbPath),
31775
+ db,
31718
31776
  mcpJsonPath
31719
31777
  };
31720
31778
  const server = new McpServer(
31721
- { name: "creditkarma-mcp", version: "2.0.6" }
31779
+ { name: "creditkarma-mcp", version: "2.0.8" }
31722
31780
  );
31723
31781
  registerAuthTools(server, ctx);
31724
31782
  registerSyncTools(server, ctx);
package/dist/db.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import { mkdirSync } from 'fs';
3
3
  import { dirname } from 'path';
4
+ import { deriveAccountId } from './accountId.js';
4
5
  const CURRENT_VERSION = 1;
5
6
  const MIGRATIONS = {
6
7
  1: `
@@ -114,3 +115,59 @@ export function setSyncState(db, key, value) {
114
115
  db.prepare('INSERT INTO sync_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
115
116
  .run(key, value);
116
117
  }
118
+ /**
119
+ * Repair transactions whose `account_id` is `''` (or NULL) by re-deriving the
120
+ * id from each transaction's `raw_json.account` and rebuilding the accounts
121
+ * table. Idempotent — returns zero counts if there's nothing to fix.
122
+ *
123
+ * Needed because CK's `transactionsHub` historically returned empty
124
+ * `account.id` strings, collapsing every account into a single row.
125
+ */
126
+ export function backfillAccountIds(db) {
127
+ const rows = db
128
+ .prepare("SELECT id, raw_json FROM transactions WHERE account_id IS NULL OR account_id = ''")
129
+ .all();
130
+ if (rows.length === 0)
131
+ return { txsUpdated: 0, accountsCreated: 0 };
132
+ const accounts = new Map();
133
+ const updates = [];
134
+ for (const row of rows) {
135
+ if (!row.raw_json)
136
+ continue;
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(row.raw_json);
140
+ }
141
+ catch {
142
+ continue;
143
+ }
144
+ if (!parsed.account)
145
+ continue;
146
+ const accountId = deriveAccountId(parsed.account);
147
+ accounts.set(accountId, {
148
+ id: accountId,
149
+ name: parsed.account.name ?? '',
150
+ type: parsed.account.type ?? null,
151
+ providerName: parsed.account.providerName ?? null,
152
+ display: parsed.account.accountTypeAndNumberDisplay ?? null,
153
+ });
154
+ updates.push({ txId: row.id, accountId });
155
+ }
156
+ if (updates.length === 0)
157
+ return { txsUpdated: 0, accountsCreated: 0 };
158
+ db.exec('BEGIN');
159
+ try {
160
+ for (const acct of accounts.values())
161
+ upsertAccount(db, acct);
162
+ const stmt = db.prepare('UPDATE transactions SET account_id = ? WHERE id = ?');
163
+ for (const u of updates)
164
+ stmt.run(u.accountId, u.txId);
165
+ db.prepare("DELETE FROM accounts WHERE id = ''").run();
166
+ db.exec('COMMIT');
167
+ }
168
+ catch (err) {
169
+ db.exec('ROLLBACK');
170
+ throw err;
171
+ }
172
+ return { txsUpdated: updates.length, accountsCreated: accounts.size };
173
+ }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { homedir } from 'os';
4
4
  import { join, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { CreditKarmaClient, isJwtExpired, extractCookieValue } from './client.js';
7
- import { initDb } from './db.js';
7
+ import { initDb, backfillAccountIds } from './db.js';
8
8
  import { registerAuthTools } from './tools/auth.js';
9
9
  import { registerSyncTools } from './tools/sync.js';
10
10
  import { registerQueryTools } from './tools/query.js';
@@ -53,12 +53,17 @@ async function main() {
53
53
  if (refreshToken && isJwtExpired(refreshToken)) {
54
54
  console.error('[creditkarma-mcp] Warning: refresh token in CK_COOKIES has expired. Run `npm run auth` (or call ck_set_session) to capture fresh credentials.');
55
55
  }
56
+ const db = initDb(dbPath);
57
+ const repaired = backfillAccountIds(db);
58
+ if (repaired.txsUpdated > 0) {
59
+ console.error(`[creditkarma-mcp] Repaired ${repaired.txsUpdated} transactions across ${repaired.accountsCreated} accounts (CK returned empty account.id for legacy rows).`);
60
+ }
56
61
  const ctx = {
57
62
  client: new CreditKarmaClient(token, refreshToken, cookies),
58
- db: initDb(dbPath),
63
+ db,
59
64
  mcpJsonPath
60
65
  };
61
- const server = new McpServer({ name: 'creditkarma-mcp', version: '2.0.6' });
66
+ const server = new McpServer({ name: 'creditkarma-mcp', version: '2.0.8' });
62
67
  registerAuthTools(server, ctx);
63
68
  registerSyncTools(server, ctx);
64
69
  registerQueryTools(server, ctx);
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { upsertAccount, upsertCategory, upsertMerchant, upsertTransaction, getSyncState, setSyncState } from '../db.js';
3
+ import { deriveAccountId } from '../accountId.js';
3
4
  export async function handleSyncTransactions(args, ctx) {
4
5
  // Auto-refresh if token expired and we have a refresh token
5
6
  if (ctx.client.isTokenExpired() || !ctx.client.getToken()) {
@@ -50,8 +51,9 @@ export async function handleSyncTransactions(args, ctx) {
50
51
  const exists = ctx.db
51
52
  .prepare('SELECT id FROM transactions WHERE id = ?')
52
53
  .get(tx.id);
54
+ const accountId = deriveAccountId(tx.account);
53
55
  upsertAccount(ctx.db, {
54
- id: tx.account.id, name: tx.account.name, type: tx.account.type,
56
+ id: accountId, name: tx.account.name, type: tx.account.type,
55
57
  providerName: tx.account.providerName, display: tx.account.accountTypeAndNumberDisplay
56
58
  });
57
59
  if (tx.category)
@@ -60,7 +62,7 @@ export async function handleSyncTransactions(args, ctx) {
60
62
  upsertMerchant(ctx.db, { id: tx.merchant.id, name: tx.merchant.name });
61
63
  upsertTransaction(ctx.db, {
62
64
  id: tx.id, date: tx.date, description: tx.description, status: tx.status,
63
- amount: tx.amount.value, accountId: tx.account.id,
65
+ amount: tx.amount.value, accountId,
64
66
  categoryId: tx.category?.id ?? null,
65
67
  merchantId: tx.merchant?.id ?? null,
66
68
  rawJson: JSON.stringify(tx)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "creditkarma-mcp",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "mcpName": "io.github.chrischall/creditkarma-mcp",
5
5
  "description": "MCP server for Credit Karma — natural-language access to your transactions, spending, and accounts",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38,6 +38,8 @@
38
38
  "start": "node dist/index.js",
39
39
  "dev": "node --env-file=.env dist/index.js",
40
40
  "auth": "node scripts/setup-auth.mjs",
41
+ "inspect:accounts": "node scripts/inspect-accounts.mjs",
42
+ "probe:accounts": "node scripts/probe-account-ids.mjs",
41
43
  "test": "vitest run",
42
44
  "test:watch": "vitest",
43
45
  "test:coverage": "vitest run --coverage"
@@ -51,7 +53,7 @@
51
53
  "@types/node": "^25.5.2",
52
54
  "@vitest/coverage-v8": "^4.1.2",
53
55
  "esbuild": "^0.28.0",
54
- "puppeteer-core": "^24.0.0",
56
+ "puppeteer-core": "^25.0.4",
55
57
  "puppeteer-extra": "^3.3.6",
56
58
  "puppeteer-extra-plugin-stealth": "^2.11.2",
57
59
  "typescript": "^6.0.2",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/creditkarma-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.0.6",
9
+ "version": "2.0.8",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "creditkarma-mcp",
14
- "version": "2.0.6",
14
+ "version": "2.0.8",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },