paisa-mcp 0.0.12 → 0.0.14

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,368 @@ 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 links = [];
45680
+ let created = 0;
45681
+ let reused = 0;
45682
+ for (const [norm, g2] of groups) {
45683
+ const upiHandles = [...g2.upiHandles];
45684
+ let personId = personIdByNorm.get(norm);
45685
+ const action = personId ? "reuse" : "create";
45686
+ plan.push({
45687
+ name: g2.displayName,
45688
+ normalized: norm,
45689
+ txnCount: g2.txnIds.length,
45690
+ upiHandles,
45691
+ action
45692
+ });
45693
+ if (!opts.dryRun) {
45694
+ if (!personId) {
45695
+ const fields = {
45696
+ name: g2.displayName,
45697
+ upiHandles,
45698
+ phoneNumbers: [],
45699
+ relationship: null,
45700
+ notes: null
45701
+ };
45702
+ const body = crypto3 ? await encryptPersonCipher(fields, crypto3.key) : { name: fields.name, upiHandles, phoneNumbers: [], relationship: null, notes: null };
45703
+ const res = await client.post("/api/persons", body);
45704
+ personId = res.data.id;
45705
+ personIdByNorm.set(norm, personId);
45706
+ created++;
45707
+ } else {
45708
+ reused++;
45709
+ }
45710
+ for (const txnId of g2.txnIds)
45711
+ links.push({ transactionId: txnId, personId });
45712
+ } else if (personId) {
45713
+ reused++;
45714
+ } else {
45715
+ created++;
45716
+ }
45717
+ }
45718
+ if (!opts.dryRun && links.length > 0) {
45719
+ const CHUNK = 500;
45720
+ for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
45721
+ await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
45722
+ }
45723
+ }
45724
+ return { candidatesScanned: candidates.length, created, reused, linked: links.length, plan };
45725
+ }
45726
+
45350
45727
  // src/tools/transactions.ts
45351
45728
  async function importTransactions(client, crypto3, opts) {
45352
45729
  const rows = opts.txns.map((t2) => {
@@ -45387,18 +45764,7 @@ async function runRescan(client, crypto3) {
45387
45764
  const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45388
45765
  const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45389
45766
  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
- }
45767
+ const persons = await loadDecryptedPersons(client, crypto3);
45402
45768
  const allRows = [];
45403
45769
  const limit = 200;
45404
45770
  let cursor;
@@ -45444,9 +45810,11 @@ async function runRescan(client, crypto3) {
45444
45810
  }
45445
45811
  function registerTransactionTools(server, client, crypto3) {
45446
45812
  server.registerTool("get_transactions", {
45447
- description: "Search and filter transactions. Returns paginated results with merchant and category details.",
45813
+ 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
45814
  inputSchema: {
45449
- search: exports_external.string().optional().describe("Search merchant name or description"),
45815
+ search: exports_external.string().optional().describe("Search merchant name (NOT encrypted descriptions)"),
45816
+ personId: exports_external.string().optional().describe("Filter to one person (exact, indexed)"),
45817
+ personName: exports_external.string().optional().describe("Resolve a counterparty name to a person and filter by it"),
45450
45818
  startDate: exports_external.string().optional().describe("Filter from date (YYYY-MM-DD)"),
45451
45819
  endDate: exports_external.string().optional().describe("Filter to date (YYYY-MM-DD)"),
45452
45820
  owner: exports_external.string().optional(),
@@ -45460,11 +45828,51 @@ function registerTransactionTools(server, client, crypto3) {
45460
45828
  offset: exports_external.number().optional()
45461
45829
  }
45462
45830
  }, 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)])));
45831
+ const { personName, ...rest } = params;
45832
+ const apiParams = { ...rest };
45833
+ if (personName && !apiParams.personId) {
45834
+ const persons = await loadDecryptedPersons(client, crypto3);
45835
+ const match = resolvePersonByName(persons, personName);
45836
+ if (!match) {
45837
+ return {
45838
+ content: [
45839
+ {
45840
+ type: "text",
45841
+ text: JSON.stringify({
45842
+ error: `No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`
45843
+ })
45844
+ }
45845
+ ]
45846
+ };
45847
+ }
45848
+ if ("candidates" in match) {
45849
+ return {
45850
+ content: [
45851
+ {
45852
+ type: "text",
45853
+ text: JSON.stringify({
45854
+ error: `Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`
45855
+ })
45856
+ }
45857
+ ]
45858
+ };
45859
+ }
45860
+ apiParams.personId = match.personId;
45861
+ }
45862
+ const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
45464
45863
  if (crypto3 && data.data)
45465
45864
  await decryptTransactionFields(data.data, crypto3.key);
45466
45865
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45467
45866
  });
45867
+ server.registerTool("backfill_persons", {
45868
+ 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.",
45869
+ inputSchema: {
45870
+ dryRun: exports_external.boolean().optional().describe("Preview the persons that would be created/reused without writing")
45871
+ }
45872
+ }, async ({ dryRun }) => {
45873
+ const result = await backfillPersons(client, crypto3, { dryRun });
45874
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
45875
+ });
45468
45876
  server.registerTool("update_transaction", {
45469
45877
  description: "Update a transaction's category, merchant, owner, notes, or flags.",
45470
45878
  inputSchema: {
@@ -45482,6 +45890,16 @@ function registerTransactionTools(server, client, crypto3) {
45482
45890
  const body = { ...updates };
45483
45891
  if (crypto3 && body.notes)
45484
45892
  body.notes = await encryptField(body.notes, crypto3.key);
45893
+ if (body.categorySlug) {
45894
+ const cats = await client.get("/api/categories");
45895
+ const allCats = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
45896
+ const cat = allCats.find((c2) => c2.slug === body.categorySlug);
45897
+ if (cat) {
45898
+ body.categoryId = cat.id;
45899
+ body.categoryType = cat.type;
45900
+ }
45901
+ delete body.categorySlug;
45902
+ }
45485
45903
  const data = await client.patch(`/api/transactions/${id}`, body);
45486
45904
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45487
45905
  });
@@ -45607,6 +46025,49 @@ function registerTransactionTools(server, client, crypto3) {
45607
46025
  ]
45608
46026
  };
45609
46027
  });
46028
+ server.registerTool("bulk_categorize_transactions", {
46029
+ 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.",
46030
+ inputSchema: {
46031
+ links: exports_external.array(exports_external.object({
46032
+ transactionId: exports_external.string().describe("Transaction UUID"),
46033
+ categoryId: exports_external.string().describe("Category UUID")
46034
+ })).min(1).max(500)
46035
+ }
46036
+ }, async ({ links }) => {
46037
+ const catsRes = await client.get("/api/categories");
46038
+ const allCats = catsRes.data.flatMap((c2) => [c2, ...c2.children ?? []]);
46039
+ const typeById = new Map(allCats.map((c2) => [c2.id, c2.type]));
46040
+ const groups = new Map;
46041
+ for (const { transactionId, categoryId } of links) {
46042
+ const arr = groups.get(categoryId) ?? [];
46043
+ arr.push(transactionId);
46044
+ groups.set(categoryId, arr);
46045
+ }
46046
+ const CHUNK = 200;
46047
+ let succeeded = 0;
46048
+ const failed = [];
46049
+ await Promise.allSettled([...groups.entries()].flatMap(([categoryId, ids]) => {
46050
+ const categoryType = typeById.get(categoryId);
46051
+ const chunks = [];
46052
+ for (let i2 = 0;i2 < ids.length; i2 += CHUNK)
46053
+ chunks.push(ids.slice(i2, i2 + CHUNK));
46054
+ return chunks.map((chunk) => client.patch("/api/transactions/bulk", {
46055
+ ids: chunk,
46056
+ categoryId,
46057
+ ...categoryType ? { categoryType } : {}
46058
+ }).then((r2) => {
46059
+ succeeded += r2.updated ?? chunk.length;
46060
+ }).catch((e2) => failed.push({ categoryId, error: String(e2) })));
46061
+ }));
46062
+ return {
46063
+ content: [
46064
+ {
46065
+ type: "text",
46066
+ text: JSON.stringify({ succeeded, failedCount: failed.length, failed })
46067
+ }
46068
+ ]
46069
+ };
46070
+ });
45610
46071
  server.registerTool("delete_transactions", {
45611
46072
  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
46073
  inputSchema: {
@@ -45696,7 +46157,7 @@ async function main() {
45696
46157
  console.error("Error: PAISA_USER_ID is required when PAISA_PIN is set.");
45697
46158
  process.exit(1);
45698
46159
  }
45699
- const cryptoMeta = await client.get("/auth-crypto").catch(() => null);
46160
+ const cryptoMeta = await client.get("/api/auth-crypto").catch(() => null);
45700
46161
  if (cryptoMeta && (cryptoMeta.kdfVersion ?? 1) >= 2 && cryptoMeta.kdfSalt && cryptoMeta.wrappedDek) {
45701
46162
  const kek = await deriveKEK(pin, cryptoMeta.kdfSalt);
45702
46163
  const dek = await unwrapDEK(cryptoMeta.wrappedDek, kek);
@@ -45746,5 +46207,5 @@ main().catch((err) => {
45746
46207
  process.exit(1);
45747
46208
  });
45748
46209
 
45749
- //# debugId=2A5FB457B4850D1E64756E2164756E21
46210
+ //# debugId=52C61062ED34DC2364756E2164756E21
45750
46211
  //# sourceMappingURL=index.js.map