paisa-mcp 0.0.7 → 0.0.10

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/dist/index.js CHANGED
@@ -42360,7 +42360,39 @@ function registerAlertTools(server, client) {
42360
42360
  }
42361
42361
 
42362
42362
  // src/tools/analytics.ts
42363
- function registerAnalyticsTools(server, client) {
42363
+ async function dec(value, key) {
42364
+ if (typeof value !== "string" || !value)
42365
+ return value;
42366
+ try {
42367
+ return await decryptField(value, key);
42368
+ } catch {
42369
+ return value;
42370
+ }
42371
+ }
42372
+ async function decryptAnalytics(data, key) {
42373
+ if (!data || typeof data !== "object")
42374
+ return;
42375
+ const d = data;
42376
+ const topMerchants = d.topMerchants;
42377
+ if (Array.isArray(topMerchants)) {
42378
+ for (const m of topMerchants)
42379
+ m.name = await dec(m.name, key);
42380
+ }
42381
+ const recent = d.recentTransactions;
42382
+ if (Array.isArray(recent)) {
42383
+ for (const t of recent) {
42384
+ t.description = await dec(t.description, key);
42385
+ if ("merchantName" in t)
42386
+ t.merchantName = await dec(t.merchantName, key);
42387
+ }
42388
+ }
42389
+ const merchant = d.merchant;
42390
+ if (merchant && typeof merchant === "object") {
42391
+ merchant.cleanName = await dec(merchant.cleanName, key);
42392
+ merchant.rawId = await dec(merchant.rawId, key);
42393
+ }
42394
+ }
42395
+ function registerAnalyticsTools(server, client, crypto3) {
42364
42396
  server.registerTool("get_analytics", {
42365
42397
  description: "Analytics reports. Select one via `report`:\n" + "- monthly_summary: Income, expenses, and investments for a specific month, broken down by category type. Includes budget vs actual. Requires `month` and `year`; optional `owner`.\n" + "- spending_trends: Monthly spending trends by category type over the last N months. Optional `months` (default: 6) and `owner`.\n" + "- category_analytics: Detailed spending breakdown for a specific category over time. Requires `slug`; optional `months`.\n" + "- merchant_analytics: Transaction history and spend total for a specific merchant. Requires `id`; optional `months`.",
42366
42398
  inputSchema: {
@@ -42400,6 +42432,8 @@ function registerAnalyticsTools(server, client) {
42400
42432
  data = await client.get(`/api/analytics/merchants/${params.id}`, params.months ? { months: params.months } : undefined);
42401
42433
  break;
42402
42434
  }
42435
+ if (crypto3)
42436
+ await decryptAnalytics(data, crypto3.key);
42403
42437
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
42404
42438
  });
42405
42439
  }
@@ -42911,6 +42945,19 @@ function registerInvestmentTools(server, client) {
42911
42945
  }
42912
42946
 
42913
42947
  // src/tools/merchants.ts
42948
+ async function categorySlugToId(client) {
42949
+ const res = await client.get("/api/categories");
42950
+ const map2 = new Map;
42951
+ for (const root of res.data ?? []) {
42952
+ if (root.slug)
42953
+ map2.set(root.slug, root.id);
42954
+ for (const child of root.children ?? []) {
42955
+ if (child.slug)
42956
+ map2.set(child.slug, child.id);
42957
+ }
42958
+ }
42959
+ return map2;
42960
+ }
42914
42961
  function registerMerchantTools(server, client, crypto3) {
42915
42962
  server.registerTool("upsert_merchant", {
42916
42963
  description: "Create or update a merchant in the dictionary. Omit `id` to add a new merchant (future transactions from it are auto-categorised); pass `id` to update an existing merchant's name, category, or flags.",
@@ -42918,13 +42965,21 @@ function registerMerchantTools(server, client, crypto3) {
42918
42965
  id: exports_external.string().optional().describe("Merchant UUID — pass to update, omit to create"),
42919
42966
  rawId: exports_external.string().optional().describe("Raw identifier — UPI ID or normalised name (required when creating)"),
42920
42967
  cleanName: exports_external.string().optional().describe('Human-friendly name (e.g. "DMart")'),
42921
- categorySlug: exports_external.string().optional(),
42968
+ categorySlug: exports_external.string().optional().describe("Category slug, e.g. 'food_delivery'"),
42922
42969
  isPerson: exports_external.boolean().optional(),
42923
42970
  isRecurring: exports_external.boolean().optional(),
42924
42971
  notes: exports_external.string().optional()
42925
42972
  }
42926
- }, async ({ id, ...rest }) => {
42973
+ }, async ({ id, categorySlug, ...rest }) => {
42927
42974
  const payload = { ...rest };
42975
+ if (categorySlug) {
42976
+ const slugMap = await categorySlugToId(client);
42977
+ const categoryId = slugMap.get(categorySlug);
42978
+ if (!categoryId) {
42979
+ throw new Error(`Unknown categorySlug "${categorySlug}". Use list_categories to see valid slugs.`);
42980
+ }
42981
+ payload.categoryId = categoryId;
42982
+ }
42928
42983
  if (crypto3)
42929
42984
  await encryptMerchantFieldsOnObject(payload, crypto3.key);
42930
42985
  const data = id ? await client.patch(`/api/merchants/${id}`, payload) : await client.post("/api/merchants", payload);
@@ -42938,19 +42993,32 @@ function registerMerchantTools(server, client, crypto3) {
42938
42993
  }, async ({ rawId }) => {
42939
42994
  if (crypto3 && rawId) {
42940
42995
  const data2 = await client.get("/api/merchants");
42941
- if (data2.merchants)
42942
- await decryptMerchantFields(data2.merchants, crypto3.key);
42996
+ if (data2.data)
42997
+ await decryptMerchantFields(data2.data, crypto3.key);
42943
42998
  const needle = rawId.toLowerCase();
42944
- const merchants = (data2.merchants ?? []).filter((m) => String(m.rawId ?? "").toLowerCase() === needle || String(m.cleanName ?? "").toLowerCase().includes(needle));
42999
+ const merchants = (data2.data ?? []).filter((m) => String(m.rawId ?? "").toLowerCase() === needle || String(m.cleanName ?? "").toLowerCase().includes(needle));
42945
43000
  return {
42946
- content: [{ type: "text", text: JSON.stringify({ ...data2, merchants }) }]
43001
+ content: [{ type: "text", text: JSON.stringify({ ...data2, data: merchants }) }]
42947
43002
  };
42948
43003
  }
42949
43004
  const data = rawId ? await client.get("/api/merchants", {
42950
43005
  search: rawId
42951
43006
  }) : await client.get("/api/merchants");
42952
- if (crypto3 && data.merchants)
42953
- await decryptMerchantFields(data.merchants, crypto3.key);
43007
+ if (crypto3 && data.data)
43008
+ await decryptMerchantFields(data.data, crypto3.key);
43009
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
43010
+ });
43011
+ server.registerTool("delete_merchant", {
43012
+ description: "Delete merchant(s) from the dictionary. Provide `id` for a single merchant or `ids` for several. Transactions referencing a deleted merchant have their merchantId cleared. PERMANENT.",
43013
+ inputSchema: {
43014
+ id: exports_external.string().optional().describe("Single merchant UUID"),
43015
+ ids: exports_external.array(exports_external.string()).min(1).optional().describe("Multiple merchant UUIDs (bulk delete)")
43016
+ }
43017
+ }, async ({ id, ids }) => {
43018
+ if (!id && (!ids || ids.length === 0)) {
43019
+ throw new Error("Provide either id or ids");
43020
+ }
43021
+ const data = ids ? await client.post("/api/merchants/bulk-delete", { ids }) : await client.delete(`/api/merchants/${id}`);
42954
43022
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
42955
43023
  });
42956
43024
  }
@@ -43164,6 +43232,9 @@ function registerReportTools(server, client) {
43164
43232
  });
43165
43233
  }
43166
43234
 
43235
+ // src/tools/transactions.ts
43236
+ import { readFile } from "node:fs/promises";
43237
+
43167
43238
  // ../reconciliation/src/aliases.ts
43168
43239
  var MERCHANT_ALIASES = [
43169
43240
  ["Swiggy", ["swiggy", "bundl technologies", "swiggy bundl"]],
@@ -45241,7 +45312,27 @@ function settlementEnrichedData(description, type, source) {
45241
45312
  return out;
45242
45313
  }
45243
45314
  // src/tools/transactions.ts
45244
- async function runRescan(client) {
45315
+ async function importTransactions(client, crypto3, opts) {
45316
+ const rows = opts.txns.map((t2) => {
45317
+ const enrichedData = settlementEnrichedData(t2.description ?? "", t2.type, opts.source);
45318
+ return enrichedData ? { ...t2, enrichedData } : { ...t2 };
45319
+ });
45320
+ if (crypto3)
45321
+ await encryptTransactionFields(rows, crypto3.key);
45322
+ const CHUNK = 200;
45323
+ let imported = 0;
45324
+ for (let i2 = 0;i2 < rows.length; i2 += CHUNK) {
45325
+ const res = await client.post("/api/transactions/import-batch", {
45326
+ accountId: opts.accountId,
45327
+ source: opts.source,
45328
+ granularity: opts.granularity,
45329
+ transactions: rows.slice(i2, i2 + CHUNK)
45330
+ });
45331
+ imported += res.imported ?? 0;
45332
+ }
45333
+ return { imported };
45334
+ }
45335
+ async function runRescan(client, crypto3) {
45245
45336
  const EMPTY_BAG2 = {
45246
45337
  merchantAliases: [],
45247
45338
  personAliases: [],
@@ -45255,10 +45346,23 @@ async function runRescan(client) {
45255
45346
  } catch {}
45256
45347
  const merchantsRes = await client.get("/api/merchants");
45257
45348
  const allMerchants = merchantsRes.data ?? [];
45349
+ if (crypto3)
45350
+ await decryptMerchantFields(allMerchants, crypto3.key);
45258
45351
  const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45259
45352
  const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45353
+ const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
45260
45354
  const personsRes = await client.get("/api/persons");
45261
- const persons = (personsRes.data ?? []).filter((p2) => p2.name);
45355
+ const persons = [];
45356
+ for (const p2 of personsRes.data ?? []) {
45357
+ let name = p2.name ?? "";
45358
+ if (crypto3 && p2.cipher) {
45359
+ try {
45360
+ name = (await decryptPersonCipher(p2.cipher, crypto3.key)).name;
45361
+ } catch {}
45362
+ }
45363
+ if (name)
45364
+ persons.push({ id: p2.id, name });
45365
+ }
45262
45366
  const allRows = [];
45263
45367
  const limit = 200;
45264
45368
  let cursor;
@@ -45269,24 +45373,32 @@ async function runRescan(client) {
45269
45373
  params.cursor = cursor;
45270
45374
  const res = await client.get("/api/transactions", params);
45271
45375
  const rows = res.data ?? [];
45376
+ if (crypto3)
45377
+ await decryptTransactionFields(rows, crypto3.key);
45272
45378
  allRows.push(...rows);
45273
45379
  if (!res.hasMore || !res.nextCursor || rows.length === 0)
45274
45380
  break;
45275
45381
  cursor = res.nextCursor;
45276
45382
  }
45277
45383
  const plan = autoCategorizeFromDescriptions({ rows: allRows, bag, globalMerchants, localMerchants, persons });
45278
- const links = plan.suggestions.filter((s2) => s2.categoryId).flatMap((s2) => s2.transactionIds.map((txId) => ({
45279
- transactionId: txId,
45280
- ...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
45281
- categoryId: s2.categoryId
45282
- })));
45384
+ let categorized = 0;
45385
+ const links = plan.suggestions.flatMap((s2) => {
45386
+ const categoryId = s2.categoryId ?? (s2.entity.type === "merchant" ? merchantCategoryById.get(s2.entity.merchantId) ?? null : null);
45387
+ if (categoryId)
45388
+ categorized += s2.transactionIds.length;
45389
+ return s2.transactionIds.map((txId) => ({
45390
+ transactionId: txId,
45391
+ ...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
45392
+ ...categoryId ? { categoryId } : {}
45393
+ }));
45394
+ });
45283
45395
  if (links.length > 0) {
45284
45396
  const CHUNK = 500;
45285
45397
  for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
45286
45398
  await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
45287
45399
  }
45288
45400
  }
45289
- return { linked: links.length, unresolved: plan.unresolvedCount };
45401
+ return { linked: links.length, categorized, unresolved: plan.unresolvedCount };
45290
45402
  }
45291
45403
  function registerTransactionTools(server, client, crypto3) {
45292
45404
  server.registerTool("get_transactions", {
@@ -45383,6 +45495,8 @@ function registerTransactionTools(server, client, crypto3) {
45383
45495
  }
45384
45496
  }, async ({ accountId, startDate, endDate }) => {
45385
45497
  const data = await client.get("/api/transactions/reconciliation-context", { accountId, startDate, endDate });
45498
+ if (crypto3 && data.data)
45499
+ await decryptTransactionFields(data.data, crypto3.key);
45386
45500
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45387
45501
  });
45388
45502
  server.registerTool("import_statement_batch", {
@@ -45400,19 +45514,13 @@ function registerTransactionTools(server, client, crypto3) {
45400
45514
  })).describe("Transactions to insert — should not include duplicates")
45401
45515
  }
45402
45516
  }, async ({ accountId, source, granularity, transactions: txns }) => {
45403
- const rows = txns.map((t2) => {
45404
- const enrichedData = settlementEnrichedData(String(t2.description ?? ""), t2.type, source);
45405
- return enrichedData ? { ...t2, enrichedData } : { ...t2 };
45406
- });
45407
- if (crypto3)
45408
- await encryptTransactionFields(rows, crypto3.key);
45409
- const data = await client.post("/api/transactions/import-batch", {
45517
+ const data = await importTransactions(client, crypto3, {
45410
45518
  accountId,
45411
45519
  source,
45412
45520
  granularity,
45413
- transactions: rows
45521
+ txns
45414
45522
  });
45415
- const rescan = await runRescan(client);
45523
+ const rescan = await runRescan(client, crypto3);
45416
45524
  return {
45417
45525
  content: [
45418
45526
  {
@@ -45422,6 +45530,33 @@ function registerTransactionTools(server, client, crypto3) {
45422
45530
  ]
45423
45531
  };
45424
45532
  });
45533
+ server.registerTool("import_statement_file", {
45534
+ description: "Bulk-import a parsed statement from a local JSON file (avoids passing thousands of rows as args). " + "The file must contain a JSON array of {date (YYYY-MM-DD), amount (>0), type ('debit'|'credit'), description, referenceNumber?}. " + "Encrypts (private mode), inserts in chunks, then runs categorization ONCE at the end. Use this for large statements.",
45535
+ inputSchema: {
45536
+ accountId: exports_external.string().describe("Bank account UUID the statement belongs to"),
45537
+ source: exports_external.enum(["bank_statement", "cc_statement", "manual", "telegram", "wisprflow", "ai_import"]).describe("Origin of the statement"),
45538
+ granularity: exports_external.number().int().min(1).max(5).describe("Source trust level: 1=bank statement, 2=CC statement, 3=manual, 4=app export, 5=telegram/AI"),
45539
+ filePath: exports_external.string().describe("Absolute path to a JSON file containing the parsed transactions array")
45540
+ }
45541
+ }, async ({ accountId, source, granularity, filePath }) => {
45542
+ let txns;
45543
+ try {
45544
+ const raw = await readFile(filePath, "utf8");
45545
+ const parsed = JSON.parse(raw);
45546
+ if (!Array.isArray(parsed))
45547
+ throw new Error("File must contain a JSON array");
45548
+ txns = parsed;
45549
+ } catch (e2) {
45550
+ throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
45551
+ }
45552
+ const data = await importTransactions(client, crypto3, { accountId, source, granularity, txns });
45553
+ const rescan = await runRescan(client, crypto3);
45554
+ return {
45555
+ content: [
45556
+ { type: "text", text: JSON.stringify({ import: data, count: txns.length, rescan }) }
45557
+ ]
45558
+ };
45559
+ });
45425
45560
  server.registerTool("delete_transactions", {
45426
45561
  description: "Hard-delete transactions. This is PERMANENT and cannot be undone. " + "Provide exactly one of: `id` (single transaction), `ids` (a list of transactions), " + "or `filters` (deletes all matching transactions). Returns the number of deleted transactions " + "for the bulk paths.",
45427
45562
  inputSchema: {
@@ -45453,7 +45588,7 @@ function registerTransactionTools(server, client, crypto3) {
45453
45588
  description: "Apply the household's saved learning rules (merchant aliases + category rules) to all uncategorized transactions. " + "Equivalent to the browser's auto-categorize flow. Run this after importing statements via MCP to populate merchants and categories.",
45454
45589
  inputSchema: {}
45455
45590
  }, async () => {
45456
- const result = await runRescan(client);
45591
+ const result = await runRescan(client, crypto3);
45457
45592
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
45458
45593
  });
45459
45594
  server.registerTool("get_settlement_candidates", {
@@ -45463,6 +45598,8 @@ function registerTransactionTools(server, client, crypto3) {
45463
45598
  }
45464
45599
  }, async ({ ccAccountId }) => {
45465
45600
  const data = await client.get("/api/transactions/settlement-candidates", { ccAccountId });
45601
+ if (crypto3 && data.data)
45602
+ await decryptTransactionFields(data.data, crypto3.key);
45466
45603
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45467
45604
  });
45468
45605
  }
@@ -45529,7 +45666,7 @@ async function main() {
45529
45666
  registerMerchantTools(server, client, crypto3);
45530
45667
  registerPersonTools(server, client, crypto3);
45531
45668
  registerCategoryTools(server, client);
45532
- registerAnalyticsTools(server, client);
45669
+ registerAnalyticsTools(server, client, crypto3);
45533
45670
  registerAccountTools(server, client);
45534
45671
  registerGoalTools(server, client);
45535
45672
  registerBudgetTools(server, client);
@@ -45559,5 +45696,5 @@ main().catch((err) => {
45559
45696
  process.exit(1);
45560
45697
  });
45561
45698
 
45562
- //# debugId=D8F2B04A5FF304BF64756E2164756E21
45699
+ //# debugId=FB29DDFDDAE15AFC64756E2164756E21
45563
45700
  //# sourceMappingURL=index.js.map