paisa-mcp 0.0.12 → 0.0.15

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
@@ -38162,6 +38162,7 @@ class PaisaApiClient {
38162
38162
  constructor(baseUrl, token) {
38163
38163
  this.http = axios_default.create({
38164
38164
  baseURL: baseUrl.replace(/\/$/, ""),
38165
+ timeout: 30000,
38165
38166
  headers: {
38166
38167
  Authorization: `Bearer ${token}`,
38167
38168
  "Content-Type": "application/json"
@@ -43081,7 +43082,21 @@ function registerNetWorthTools(server, client) {
43081
43082
  breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional()
43082
43083
  }
43083
43084
  }, async ({ mode, ...body }) => {
43084
- const data = mode === "auto" ? await client.post("/api/net-worth/auto", { month: body.month, year: body.year }) : await client.post("/api/net-worth", body);
43085
+ let data;
43086
+ if (mode === "auto") {
43087
+ data = await client.post("/api/net-worth/auto", { month: body.month, year: body.year });
43088
+ } else {
43089
+ const liquidCash = parseFloat(body.liquidCash ?? "0");
43090
+ const investments = parseFloat(body.investments ?? "0");
43091
+ const emergencyFund = parseFloat(body.emergencyFund ?? "0");
43092
+ const liabilities = parseFloat(body.liabilities ?? "0");
43093
+ const totalNetWorth = (liquidCash + investments + emergencyFund - liabilities).toFixed(2);
43094
+ data = await client.post("/api/net-worth", {
43095
+ ...body,
43096
+ emergencyFund: body.emergencyFund ?? "0",
43097
+ totalNetWorth
43098
+ });
43099
+ }
43085
43100
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
43086
43101
  });
43087
43102
  }
@@ -45347,6 +45362,375 @@ function settlementEnrichedData(description, type, source) {
45347
45362
  out.settlementCardLast4 = last4;
45348
45363
  return out;
45349
45364
  }
45365
+ // ../parsers/src/classify.ts
45366
+ var CORPORATE_TOKENS = [
45367
+ "PVT",
45368
+ "PRIVATE",
45369
+ "LIMITED",
45370
+ "LTD",
45371
+ "LLP",
45372
+ "INC",
45373
+ "CORP",
45374
+ "ENTERPRISES",
45375
+ "TECHNOLOGIES",
45376
+ "SOLUTIONS",
45377
+ "SERVICES"
45378
+ ];
45379
+ var CORPORATE_REGEX = new RegExp(`\\b(${CORPORATE_TOKENS.join("|")})\\b`);
45380
+ function classifyEntity({ paymentMethod, extractedName }) {
45381
+ if (paymentMethod === "atm")
45382
+ return "merchant";
45383
+ if (paymentMethod === "pos")
45384
+ return "merchant";
45385
+ const upper = (extractedName ?? "").toUpperCase();
45386
+ if (upper && CORPORATE_REGEX.test(upper))
45387
+ return "merchant";
45388
+ if (paymentMethod === "imps")
45389
+ return "person";
45390
+ if (paymentMethod === "upi")
45391
+ return "person";
45392
+ if (paymentMethod === "neft" || paymentMethod === "rtgs")
45393
+ return "merchant";
45394
+ if (paymentMethod === "ach")
45395
+ return "merchant";
45396
+ return "unknown";
45397
+ }
45398
+ // ../parsers/src/hdfc/narration.ts
45399
+ function parseHdfcNarration(narration) {
45400
+ const trimmed = narration.trim();
45401
+ if (trimmed.startsWith("UPI-")) {
45402
+ return parseUpi(trimmed);
45403
+ }
45404
+ if (trimmed.startsWith("NEFT CR-") || trimmed.startsWith("NEFT DR-")) {
45405
+ return parseNeft(trimmed);
45406
+ }
45407
+ if (trimmed.startsWith("ACH D-") || trimmed.startsWith("ACH C-")) {
45408
+ return parseAch(trimmed);
45409
+ }
45410
+ if (trimmed.startsWith("FT-") || trimmed.startsWith("FT -")) {
45411
+ return parseFt(trimmed);
45412
+ }
45413
+ if (trimmed.startsWith("IMPS-") || trimmed.startsWith("IMPS/")) {
45414
+ return parseImps(trimmed);
45415
+ }
45416
+ if (trimmed.startsWith("ATW-") || trimmed.startsWith("ATM-") || trimmed.startsWith("ATM/")) {
45417
+ return {
45418
+ paymentMethod: "atm",
45419
+ name: "ATM Withdrawal",
45420
+ upiId: null,
45421
+ referenceNumber: null,
45422
+ description: trimmed
45423
+ };
45424
+ }
45425
+ if (trimmed.startsWith("POS ")) {
45426
+ return parsePos(trimmed);
45427
+ }
45428
+ return parseFallback(trimmed);
45429
+ }
45430
+ function parseUpi(narration) {
45431
+ const rest = narration.slice(4);
45432
+ const parts = rest.split("-");
45433
+ if (parts.length < 4) {
45434
+ return {
45435
+ paymentMethod: "upi",
45436
+ name: rest,
45437
+ upiId: null,
45438
+ referenceNumber: null,
45439
+ description: null
45440
+ };
45441
+ }
45442
+ const name = parts[0].trim();
45443
+ const upiId = parts[1]?.trim() || null;
45444
+ const referenceNumber = parts[3]?.trim() || null;
45445
+ const description = parts.length > 4 ? parts.slice(4).join("-").trim() : null;
45446
+ return {
45447
+ paymentMethod: "upi",
45448
+ name: cleanName(name),
45449
+ upiId,
45450
+ referenceNumber,
45451
+ description: description || null
45452
+ };
45453
+ }
45454
+ function parseNeft(narration) {
45455
+ const isCredit = narration.startsWith("NEFT CR-");
45456
+ const rest = narration.slice(isCredit ? 8 : 8);
45457
+ const parts = rest.split("-");
45458
+ if (parts.length < 2) {
45459
+ return {
45460
+ paymentMethod: "neft",
45461
+ name: rest,
45462
+ upiId: null,
45463
+ referenceNumber: null,
45464
+ description: null
45465
+ };
45466
+ }
45467
+ const companyName = parts[1]?.trim() || parts[0]?.trim() || rest;
45468
+ const referenceNumber = parts[parts.length - 1]?.trim() || null;
45469
+ return {
45470
+ paymentMethod: "neft",
45471
+ name: cleanName(companyName),
45472
+ upiId: null,
45473
+ referenceNumber,
45474
+ description: null
45475
+ };
45476
+ }
45477
+ function parseAch(narration) {
45478
+ const rest = narration.slice(7).trim();
45479
+ const parts = rest.split("-");
45480
+ const name = parts[0]?.trim() || rest;
45481
+ const referenceNumber = parts.length > 1 ? parts[parts.length - 1]?.trim() : null;
45482
+ return {
45483
+ paymentMethod: "ach",
45484
+ name: cleanName(name),
45485
+ upiId: null,
45486
+ referenceNumber,
45487
+ description: null
45488
+ };
45489
+ }
45490
+ function parseFt(narration) {
45491
+ const rest = narration.replace(/^FT[\s-]+/, "").trim();
45492
+ const spaceDashParts = rest.split(" - ");
45493
+ if (spaceDashParts.length >= 2) {
45494
+ const namePart = spaceDashParts.find((p2, i2) => i2 > 0 && p2.trim().length > 0);
45495
+ return {
45496
+ paymentMethod: "fund_transfer",
45497
+ name: cleanName(namePart?.trim() || rest),
45498
+ upiId: null,
45499
+ referenceNumber: spaceDashParts[0]?.split("-")[0]?.trim() || null,
45500
+ description: null
45501
+ };
45502
+ }
45503
+ return {
45504
+ paymentMethod: "fund_transfer",
45505
+ name: cleanName(rest),
45506
+ upiId: null,
45507
+ referenceNumber: null,
45508
+ description: null
45509
+ };
45510
+ }
45511
+ function parseImps(narration) {
45512
+ const rest = narration.replace(/^IMPS[-/]/, "").trim();
45513
+ const parts = rest.split("-");
45514
+ const referenceNumber = parts[0]?.trim() || null;
45515
+ const name = parts.length > 1 ? parts[1]?.trim() : rest;
45516
+ return {
45517
+ paymentMethod: "imps",
45518
+ name: cleanName(name || rest),
45519
+ upiId: null,
45520
+ referenceNumber,
45521
+ description: null
45522
+ };
45523
+ }
45524
+ function parsePos(narration) {
45525
+ const rest = narration.slice(4).trim();
45526
+ const cleaned = rest.replace(/^[0-9X]{4,}\s*/, "").trim();
45527
+ return {
45528
+ paymentMethod: "pos",
45529
+ name: cleanName(cleaned || rest),
45530
+ upiId: null,
45531
+ referenceNumber: null,
45532
+ description: null
45533
+ };
45534
+ }
45535
+ function parseFallback(narration) {
45536
+ return {
45537
+ paymentMethod: "other",
45538
+ name: cleanName(narration),
45539
+ upiId: null,
45540
+ referenceNumber: null,
45541
+ description: null
45542
+ };
45543
+ }
45544
+ function cleanName(raw) {
45545
+ return raw.replace(/\s+(PRIVATE|PVT\.?)\s*\w*/gi, "").replace(/\s+(LIMITED|LTD\.?)(\s|$)/gi, " ").replace(/[\s.-]+$/, "").replace(/\s{2,}/g, " ").trim();
45546
+ }
45547
+
45548
+ // ../parsers/src/sbi/savings.ts
45549
+ function parseSbiNarration(narration) {
45550
+ const upper = narration.toUpperCase();
45551
+ const upiMatch = narration.match(/UPI\/(?:DR|CR)\/(\d+)\/([^/]+)\/([A-Z]{2,6})\/([^/\s]+)/i);
45552
+ if (upiMatch) {
45553
+ const name = upiMatch[2].trim();
45554
+ const upiId = upiMatch[4].trim();
45555
+ return {
45556
+ paymentMethod: "upi",
45557
+ name: name || null,
45558
+ upiId: upiId || null
45559
+ };
45560
+ }
45561
+ if (upper.includes("NEFT")) {
45562
+ const neftMatch = narration.match(/NEFT[-/][\w]+[-/]([\w\s]+?)(?:[-/]|$)/i);
45563
+ return {
45564
+ paymentMethod: "neft",
45565
+ name: neftMatch ? neftMatch[1].trim() : null,
45566
+ upiId: null
45567
+ };
45568
+ }
45569
+ if (upper.includes("RTGS")) {
45570
+ return { paymentMethod: "rtgs", name: null, upiId: null };
45571
+ }
45572
+ if (upper.includes("IMPS")) {
45573
+ return { paymentMethod: "imps", name: null, upiId: null };
45574
+ }
45575
+ if (upper.includes("ATM") || upper.includes("WDL ATM")) {
45576
+ return { paymentMethod: "atm", name: null, upiId: null };
45577
+ }
45578
+ if (upper.includes("POS") || upper.includes("PURCHASE")) {
45579
+ return { paymentMethod: "pos", name: null, upiId: null };
45580
+ }
45581
+ if (upper.includes("INT.COLL") || upper.includes("INTEREST")) {
45582
+ return { paymentMethod: "other", name: "Interest", upiId: null };
45583
+ }
45584
+ return { paymentMethod: null, name: null, upiId: null };
45585
+ }
45586
+ // src/lib/person-backfill.ts
45587
+ function normalizePersonName(name) {
45588
+ return name.toLowerCase().trim().replace(/\s+/g, " ");
45589
+ }
45590
+ async function loadDecryptedPersons(client, crypto3) {
45591
+ const res = await client.get("/api/persons");
45592
+ const persons = [];
45593
+ for (const p2 of res.data ?? []) {
45594
+ let name = p2.name ?? "";
45595
+ if (crypto3 && p2.cipher) {
45596
+ try {
45597
+ name = (await decryptPersonCipher(p2.cipher, crypto3.key)).name;
45598
+ } catch {}
45599
+ }
45600
+ if (name)
45601
+ persons.push({ id: p2.id, name });
45602
+ }
45603
+ return persons;
45604
+ }
45605
+ function resolvePersonByName(persons, query) {
45606
+ const q2 = normalizePersonName(query);
45607
+ if (!q2)
45608
+ return null;
45609
+ const exact = persons.filter((p2) => normalizePersonName(p2.name) === q2);
45610
+ if (exact.length === 1)
45611
+ return { personId: exact[0].id };
45612
+ if (exact.length > 1)
45613
+ return { candidates: exact.map((p2) => p2.name) };
45614
+ const partial2 = persons.filter((p2) => normalizePersonName(p2.name).includes(q2));
45615
+ if (partial2.length === 1)
45616
+ return { personId: partial2[0].id };
45617
+ if (partial2.length > 1)
45618
+ return { candidates: partial2.map((p2) => p2.name) };
45619
+ return null;
45620
+ }
45621
+ function parseNarration(description) {
45622
+ const hdfc = parseHdfcNarration(description);
45623
+ if (hdfc.name && hdfc.paymentMethod)
45624
+ return { name: hdfc.name, upiId: hdfc.upiId, paymentMethod: hdfc.paymentMethod };
45625
+ const sbi = parseSbiNarration(description);
45626
+ if (sbi.name && sbi.paymentMethod)
45627
+ return { name: sbi.name, upiId: sbi.upiId, paymentMethod: sbi.paymentMethod };
45628
+ return {
45629
+ name: hdfc.name || sbi.name,
45630
+ upiId: hdfc.upiId ?? sbi.upiId,
45631
+ paymentMethod: hdfc.paymentMethod ?? sbi.paymentMethod
45632
+ };
45633
+ }
45634
+ async function backfillPersons(client, crypto3, opts = {}) {
45635
+ const existing = await loadDecryptedPersons(client, crypto3);
45636
+ const personIdByNorm = new Map;
45637
+ for (const p2 of existing)
45638
+ personIdByNorm.set(normalizePersonName(p2.name), p2.id);
45639
+ const candidates = [];
45640
+ let cursor;
45641
+ const MAX_PAGES = 100;
45642
+ for (let page = 0;page < MAX_PAGES; page++) {
45643
+ const params = { limit: 200 };
45644
+ if (cursor)
45645
+ params.cursor = cursor;
45646
+ const res = await client.get("/api/transactions", params);
45647
+ const rows = res.data ?? [];
45648
+ if (crypto3)
45649
+ await decryptTransactionFields(rows, crypto3.key);
45650
+ for (const r2 of rows) {
45651
+ if (!r2.merchantId && !r2.personId && !r2.isTransfer && !r2.isIgnored)
45652
+ candidates.push(r2);
45653
+ }
45654
+ if (!res.hasMore || !res.nextCursor || rows.length === 0)
45655
+ break;
45656
+ cursor = res.nextCursor;
45657
+ }
45658
+ const groups = new Map;
45659
+ for (const t2 of candidates) {
45660
+ const { name, upiId, paymentMethod } = parseNarration(t2.description);
45661
+ if (!name)
45662
+ continue;
45663
+ if (classifyEntity({ paymentMethod, extractedName: name }) !== "person")
45664
+ continue;
45665
+ const norm = normalizePersonName(name);
45666
+ if (!norm)
45667
+ continue;
45668
+ const g2 = groups.get(norm) ?? {
45669
+ displayName: name.trim(),
45670
+ upiHandles: new Set,
45671
+ txnIds: []
45672
+ };
45673
+ if (upiId)
45674
+ g2.upiHandles.add(upiId);
45675
+ g2.txnIds.push(t2.id);
45676
+ groups.set(norm, g2);
45677
+ }
45678
+ const plan = [];
45679
+ const newGroups = [];
45680
+ let reused = 0;
45681
+ for (const [norm, g2] of groups) {
45682
+ const upiHandles = [...g2.upiHandles];
45683
+ const isReuse = personIdByNorm.has(norm);
45684
+ plan.push({
45685
+ name: g2.displayName,
45686
+ normalized: norm,
45687
+ txnCount: g2.txnIds.length,
45688
+ upiHandles,
45689
+ action: isReuse ? "reuse" : "create"
45690
+ });
45691
+ if (isReuse)
45692
+ reused++;
45693
+ else
45694
+ newGroups.push({ norm, displayName: g2.displayName, upiHandles });
45695
+ }
45696
+ if (!opts.dryRun && newGroups.length > 0) {
45697
+ const bodies = await Promise.all(newGroups.map(async ({ displayName, upiHandles }) => {
45698
+ const fields = {
45699
+ name: displayName,
45700
+ upiHandles,
45701
+ phoneNumbers: [],
45702
+ relationship: null,
45703
+ notes: null
45704
+ };
45705
+ return crypto3 ? await encryptPersonCipher(fields, crypto3.key) : { name: displayName, upiHandles, phoneNumbers: [], relationship: null, notes: null };
45706
+ }));
45707
+ const res = await client.post("/api/persons/bulk", {
45708
+ persons: bodies
45709
+ });
45710
+ res.data.forEach((p2, i2) => {
45711
+ personIdByNorm.set(newGroups[i2].norm, p2.id);
45712
+ });
45713
+ }
45714
+ const created = newGroups.length;
45715
+ const links = [];
45716
+ if (!opts.dryRun) {
45717
+ for (const [norm, g2] of groups) {
45718
+ const personId = personIdByNorm.get(norm);
45719
+ if (!personId)
45720
+ continue;
45721
+ for (const txnId of g2.txnIds)
45722
+ links.push({ transactionId: txnId, personId });
45723
+ }
45724
+ }
45725
+ if (!opts.dryRun && links.length > 0) {
45726
+ const CHUNK = 500;
45727
+ for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
45728
+ await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
45729
+ }
45730
+ }
45731
+ return { candidatesScanned: candidates.length, created, reused, linked: links.length, plan };
45732
+ }
45733
+
45350
45734
  // src/tools/transactions.ts
45351
45735
  async function importTransactions(client, crypto3, opts) {
45352
45736
  const rows = opts.txns.map((t2) => {
@@ -45387,18 +45771,7 @@ async function runRescan(client, crypto3) {
45387
45771
  const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45388
45772
  const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45389
45773
  const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
45390
- const personsRes = await client.get("/api/persons");
45391
- const persons = [];
45392
- for (const p2 of personsRes.data ?? []) {
45393
- let name = p2.name ?? "";
45394
- if (crypto3 && p2.cipher) {
45395
- try {
45396
- name = (await decryptPersonCipher(p2.cipher, crypto3.key)).name;
45397
- } catch {}
45398
- }
45399
- if (name)
45400
- persons.push({ id: p2.id, name });
45401
- }
45774
+ const persons = await loadDecryptedPersons(client, crypto3);
45402
45775
  const allRows = [];
45403
45776
  const limit = 200;
45404
45777
  let cursor;
@@ -45444,9 +45817,11 @@ async function runRescan(client, crypto3) {
45444
45817
  }
45445
45818
  function registerTransactionTools(server, client, crypto3) {
45446
45819
  server.registerTool("get_transactions", {
45447
- description: "Search and filter transactions. Returns paginated results with merchant and category details.",
45820
+ description: "Search and filter transactions. Returns paginated results with merchant and category details. To find everything paid to a specific counterparty, pass `personName` (resolved to a person via the household's persons list) or `personId` directly — this is an indexed lookup, not a description scan. Note: free-text `search` only matches plaintext merchant names, not encrypted descriptions.",
45448
45821
  inputSchema: {
45449
- search: exports_external.string().optional().describe("Search merchant name or description"),
45822
+ search: exports_external.string().optional().describe("Search merchant name (NOT encrypted descriptions)"),
45823
+ personId: exports_external.string().optional().describe("Filter to one person (exact, indexed)"),
45824
+ personName: exports_external.string().optional().describe("Resolve a counterparty name to a person and filter by it"),
45450
45825
  startDate: exports_external.string().optional().describe("Filter from date (YYYY-MM-DD)"),
45451
45826
  endDate: exports_external.string().optional().describe("Filter to date (YYYY-MM-DD)"),
45452
45827
  owner: exports_external.string().optional(),
@@ -45460,11 +45835,51 @@ function registerTransactionTools(server, client, crypto3) {
45460
45835
  offset: exports_external.number().optional()
45461
45836
  }
45462
45837
  }, async (params) => {
45463
- const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(params).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
45838
+ const { personName, ...rest } = params;
45839
+ const apiParams = { ...rest };
45840
+ if (personName && !apiParams.personId) {
45841
+ const persons = await loadDecryptedPersons(client, crypto3);
45842
+ const match = resolvePersonByName(persons, personName);
45843
+ if (!match) {
45844
+ return {
45845
+ content: [
45846
+ {
45847
+ type: "text",
45848
+ text: JSON.stringify({
45849
+ error: `No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`
45850
+ })
45851
+ }
45852
+ ]
45853
+ };
45854
+ }
45855
+ if ("candidates" in match) {
45856
+ return {
45857
+ content: [
45858
+ {
45859
+ type: "text",
45860
+ text: JSON.stringify({
45861
+ error: `Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`
45862
+ })
45863
+ }
45864
+ ]
45865
+ };
45866
+ }
45867
+ apiParams.personId = match.personId;
45868
+ }
45869
+ const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
45464
45870
  if (crypto3 && data.data)
45465
45871
  await decryptTransactionFields(data.data, crypto3.key);
45466
45872
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45467
45873
  });
45874
+ server.registerTool("backfill_persons", {
45875
+ description: "Extract UPI/IMPS counterparties from unlinked transactions, create person records for them (deduped by name), and link transactions via personId — so you can later filter all payments to a person. Idempotent: re-running reuses existing persons and skips already-linked rows. Pass dryRun to preview without writing.",
45876
+ inputSchema: {
45877
+ dryRun: exports_external.boolean().optional().describe("Preview the persons that would be created/reused without writing")
45878
+ }
45879
+ }, async ({ dryRun }) => {
45880
+ const result = await backfillPersons(client, crypto3, { dryRun });
45881
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
45882
+ });
45468
45883
  server.registerTool("update_transaction", {
45469
45884
  description: "Update a transaction's category, merchant, owner, notes, or flags.",
45470
45885
  inputSchema: {
@@ -45482,6 +45897,16 @@ function registerTransactionTools(server, client, crypto3) {
45482
45897
  const body = { ...updates };
45483
45898
  if (crypto3 && body.notes)
45484
45899
  body.notes = await encryptField(body.notes, crypto3.key);
45900
+ if (body.categorySlug) {
45901
+ const cats = await client.get("/api/categories");
45902
+ const allCats = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
45903
+ const cat = allCats.find((c2) => c2.slug === body.categorySlug);
45904
+ if (cat) {
45905
+ body.categoryId = cat.id;
45906
+ body.categoryType = cat.type;
45907
+ }
45908
+ delete body.categorySlug;
45909
+ }
45485
45910
  const data = await client.patch(`/api/transactions/${id}`, body);
45486
45911
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45487
45912
  });
@@ -45607,6 +46032,49 @@ function registerTransactionTools(server, client, crypto3) {
45607
46032
  ]
45608
46033
  };
45609
46034
  });
46035
+ server.registerTool("bulk_categorize_transactions", {
46036
+ description: "Set categories on multiple transactions at once without needing to link them to a merchant or person. " + "Pass an array of {transactionId, categoryId} pairs. Runs PATCH /api/transactions/{id} for each link in parallel batches. " + "Use this when you have category UUIDs and want to bulk-categorize uncategorized transactions directly.",
46037
+ inputSchema: {
46038
+ links: exports_external.array(exports_external.object({
46039
+ transactionId: exports_external.string().describe("Transaction UUID"),
46040
+ categoryId: exports_external.string().describe("Category UUID")
46041
+ })).min(1).max(500)
46042
+ }
46043
+ }, async ({ links }) => {
46044
+ const catsRes = await client.get("/api/categories");
46045
+ const allCats = catsRes.data.flatMap((c2) => [c2, ...c2.children ?? []]);
46046
+ const typeById = new Map(allCats.map((c2) => [c2.id, c2.type]));
46047
+ const groups = new Map;
46048
+ for (const { transactionId, categoryId } of links) {
46049
+ const arr = groups.get(categoryId) ?? [];
46050
+ arr.push(transactionId);
46051
+ groups.set(categoryId, arr);
46052
+ }
46053
+ const CHUNK = 200;
46054
+ let succeeded = 0;
46055
+ const failed = [];
46056
+ await Promise.allSettled([...groups.entries()].flatMap(([categoryId, ids]) => {
46057
+ const categoryType = typeById.get(categoryId);
46058
+ const chunks = [];
46059
+ for (let i2 = 0;i2 < ids.length; i2 += CHUNK)
46060
+ chunks.push(ids.slice(i2, i2 + CHUNK));
46061
+ return chunks.map((chunk) => client.patch("/api/transactions/bulk", {
46062
+ ids: chunk,
46063
+ categoryId,
46064
+ ...categoryType ? { categoryType } : {}
46065
+ }).then((r2) => {
46066
+ succeeded += r2.updated ?? chunk.length;
46067
+ }).catch((e2) => failed.push({ categoryId, error: String(e2) })));
46068
+ }));
46069
+ return {
46070
+ content: [
46071
+ {
46072
+ type: "text",
46073
+ text: JSON.stringify({ succeeded, failedCount: failed.length, failed })
46074
+ }
46075
+ ]
46076
+ };
46077
+ });
45610
46078
  server.registerTool("delete_transactions", {
45611
46079
  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.",
45612
46080
  inputSchema: {
@@ -45696,7 +46164,7 @@ async function main() {
45696
46164
  console.error("Error: PAISA_USER_ID is required when PAISA_PIN is set.");
45697
46165
  process.exit(1);
45698
46166
  }
45699
- const cryptoMeta = await client.get("/auth-crypto").catch(() => null);
46167
+ const cryptoMeta = await client.get("/api/auth-crypto").catch(() => null);
45700
46168
  if (cryptoMeta && (cryptoMeta.kdfVersion ?? 1) >= 2 && cryptoMeta.kdfSalt && cryptoMeta.wrappedDek) {
45701
46169
  const kek = await deriveKEK(pin, cryptoMeta.kdfSalt);
45702
46170
  const dek = await unwrapDEK(cryptoMeta.wrappedDek, kek);
@@ -45746,5 +46214,5 @@ main().catch((err) => {
45746
46214
  process.exit(1);
45747
46215
  });
45748
46216
 
45749
- //# debugId=2A5FB457B4850D1E64756E2164756E21
46217
+ //# debugId=D3F9CA60DACD6F8864756E2164756E21
45750
46218
  //# sourceMappingURL=index.js.map