paisa-mcp 0.4.7 → 0.4.9

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
@@ -58659,6 +58659,11 @@ function entityRule(e2, bag) {
58659
58659
  return bag.merchantRules[e2.merchantId] ?? null;
58660
58660
  return bag.personRules[e2.personId] ?? null;
58661
58661
  }
58662
+ // ../reconciliation/src/income.ts
58663
+ var SALARY = /\bSALARY\b|\bPAYROLL\b|\bSAL\s?CR\b/i;
58664
+ function salaryCreditIds(rows) {
58665
+ return rows.filter((r2) => r2.type === "credit" && !r2.categoryId && !r2.isTransfer && !r2.isIgnored && SALARY.test(r2.description)).map((r2) => r2.id);
58666
+ }
58662
58667
  // ../reconciliation/src/match.ts
58663
58668
  var DEFAULT_DATE_TOLERANCE = 2;
58664
58669
  var DEFAULT_AMOUNT_TOLERANCE = 0.02;
@@ -59272,6 +59277,84 @@ function parseAmount(raw) {
59272
59277
  const num = parseFloat(cleaned);
59273
59278
  return Number.isNaN(num) ? 0 : Math.abs(num);
59274
59279
  }
59280
+ // ../parsers/src/cdsl/cas.ts
59281
+ var HOLDING_ROW = /^(IN[EF][A-Z0-9]{9})\s*(.*?)\s*([\d,]+\.\d{3})\s+\S+\s+\S+\s+\S+\s+\S+\s+([\d,]+\.\d{2,4})\s+([\d,]+\.\d{2})$/;
59282
+ var ISIN_START = /^IN[EF][A-Z0-9]{9}\b/;
59283
+ var AS_ON = /HOLDING STATEMENT AS ON (\d{2})-(\d{2})-(\d{4})/;
59284
+ var PORTFOLIO_VALUE = /Portfolio Value\s*`?\s*([\d,]+\.\d{2})\s+as on/;
59285
+ var TOTAL_VALUE = /Total Portfolio Value\s+([\d,]+\.\d{2})/;
59286
+ var num = (s2) => Number(s2.replace(/,/g, ""));
59287
+ function parseCdslCasLines(lines) {
59288
+ let asOf = null;
59289
+ let totalValue = null;
59290
+ const portfolioValues = [];
59291
+ const holdings = [];
59292
+ lines.forEach((raw, i2) => {
59293
+ const line = raw.trim();
59294
+ const asOn = AS_ON.exec(line);
59295
+ if (asOn && !asOf)
59296
+ asOf = `${asOn[3]}-${asOn[2]}-${asOn[1]}`;
59297
+ const total = TOTAL_VALUE.exec(line);
59298
+ if (total && totalValue === null)
59299
+ totalValue = num(total[1]);
59300
+ const pv = PORTFOLIO_VALUE.exec(line);
59301
+ if (pv)
59302
+ portfolioValues.push(num(pv[1]));
59303
+ const m2 = HOLDING_ROW.exec(line);
59304
+ if (!m2)
59305
+ return;
59306
+ const prev = lines[i2 - 1]?.trim() ?? "";
59307
+ const lead = prev && !ISIN_START.test(prev) && !/\d\.\d{2}$/.test(prev) ? prev : "";
59308
+ holdings.push({
59309
+ isin: m2[1],
59310
+ name: `${lead} ${m2[2]}`.replace(/#/g, " ").replace(/\s+/g, " ").trim(),
59311
+ kind: m2[1].startsWith("INF") ? "mutual_fund" : "stock",
59312
+ units: num(m2[3]),
59313
+ price: num(m2[4]),
59314
+ value: num(m2[5])
59315
+ });
59316
+ });
59317
+ const dematValue = portfolioValues[0] ?? null;
59318
+ const npsValue = portfolioValues[1] ?? null;
59319
+ const sum2 = Math.round(holdings.reduce((s2, h2) => s2 + h2.value, 0) * 100) / 100;
59320
+ return {
59321
+ source: "cdsl_cas",
59322
+ asOf,
59323
+ holdings,
59324
+ dematValue,
59325
+ npsValue,
59326
+ totalValue,
59327
+ reconciled: dematValue !== null && Math.abs(sum2 - dematValue) < 1
59328
+ };
59329
+ }
59330
+ async function parseCdslCasPdf(data, password) {
59331
+ const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
59332
+ pdfjs.GlobalWorkerOptions.workerSrc = import.meta.resolve("pdfjs-dist/legacy/build/pdf.worker.mjs");
59333
+ const opts = {
59334
+ data: new Uint8Array(data),
59335
+ useWorkerFetch: false,
59336
+ useSystemFonts: true
59337
+ };
59338
+ if (password)
59339
+ opts.password = password;
59340
+ const pdf = await pdfjs.getDocument(opts).promise;
59341
+ const lines = [];
59342
+ for (let p2 = 1;p2 <= pdf.numPages; p2++) {
59343
+ const content = await (await pdf.getPage(p2)).getTextContent();
59344
+ const rows = new Map;
59345
+ for (const it2 of content.items) {
59346
+ if (!("str" in it2) || !it2.str.trim())
59347
+ continue;
59348
+ const y2 = Math.round(it2.transform[5]);
59349
+ rows.set(y2, (rows.get(y2) ?? []).concat([[it2.transform[4], it2.str]]));
59350
+ }
59351
+ for (const y2 of [...rows.keys()].sort((a2, b2) => b2 - a2)) {
59352
+ const row = rows.get(y2) ?? [];
59353
+ lines.push(row.sort((a2, b2) => a2[0] - b2[0]).map(([, s2]) => s2).join(" ").replace(/\s+/g, " ").trim());
59354
+ }
59355
+ }
59356
+ return parseCdslCasLines(lines);
59357
+ }
59275
59358
  // ../parsers/src/classify.ts
59276
59359
  var CORPORATE_TOKENS = [
59277
59360
  "PVT",
@@ -59312,8 +59395,8 @@ function parseAmount2(cell) {
59312
59395
  if (typeof cell === "number")
59313
59396
  return Math.abs(cell);
59314
59397
  const cleaned = String(cell).replace(/,/g, "").trim();
59315
- const num = parseFloat(cleaned);
59316
- return Number.isNaN(num) ? 0 : Math.abs(num);
59398
+ const num2 = parseFloat(cleaned);
59399
+ return Number.isNaN(num2) ? 0 : Math.abs(num2);
59317
59400
  }
59318
59401
 
59319
59402
  // ../parsers/src/hdfc/narration.ts
@@ -59961,8 +60044,8 @@ function parseHdfcDate(raw) {
59961
60044
  const month2 = match[2];
59962
60045
  let year = match[3];
59963
60046
  if (year.length === 2) {
59964
- const num = parseInt(year, 10);
59965
- year = num > 50 ? `19${year}` : `20${year}`;
60047
+ const num2 = parseInt(year, 10);
60048
+ year = num2 > 50 ? `19${year}` : `20${year}`;
59966
60049
  }
59967
60050
  return `${year}-${month2}-${day}`;
59968
60051
  }
@@ -59970,8 +60053,8 @@ function parseAmount3(raw) {
59970
60053
  if (!raw)
59971
60054
  return 0;
59972
60055
  const cleaned = raw.replace(/,/g, "").trim();
59973
- const num = parseFloat(cleaned);
59974
- return Number.isNaN(num) ? 0 : Math.abs(num);
60056
+ const num2 = parseFloat(cleaned);
60057
+ return Number.isNaN(num2) ? 0 : Math.abs(num2);
59975
60058
  }
59976
60059
  // ../parsers/src/sbi/savings-pdf.ts
59977
60060
  var _pdfjs4 = null;
@@ -60216,8 +60299,8 @@ function parseAmount4(raw) {
60216
60299
  if (!raw)
60217
60300
  return 0;
60218
60301
  const cleaned = raw.replace(/,/g, "").trim();
60219
- const num = parseFloat(cleaned);
60220
- return Number.isNaN(num) ? 0 : Math.abs(num);
60302
+ const num2 = parseFloat(cleaned);
60303
+ return Number.isNaN(num2) ? 0 : Math.abs(num2);
60221
60304
  }
60222
60305
  // src/lib/plan-hash.ts
60223
60306
  import { createHash } from "node:crypto";
@@ -60853,6 +60936,20 @@ async function runRescan(client, crypto4) {
60853
60936
  });
60854
60937
  });
60855
60938
  await postLinks(client, links);
60939
+ const salaryIds = salaryCreditIds(allRows);
60940
+ if (salaryIds.length > 0) {
60941
+ const salary = (await loadCategoryIndex(client)).bySlug.get("salary");
60942
+ if (salary) {
60943
+ for (let i2 = 0;i2 < salaryIds.length; i2 += 200) {
60944
+ await client.patch("/api/transactions/bulk", {
60945
+ ids: salaryIds.slice(i2, i2 + 200),
60946
+ categoryId: salary.id,
60947
+ categoryType: salary.type
60948
+ });
60949
+ }
60950
+ categorized += salaryIds.length;
60951
+ }
60952
+ }
60856
60953
  return { linked: links.length, categorized, unresolved: plan.unresolvedCount };
60857
60954
  }
60858
60955
  var TRUNCATED_WARNING = "TRUNCATED: the API capped this result, so it is INCOMPLETE. Do not import/reconcile/resolve against it — narrow the query (smaller date range) and re-run.";
@@ -61677,6 +61774,92 @@ function settingsTools(client) {
61677
61774
  // src/tools/statement-import.ts
61678
61775
  import { readFile as readFile2 } from "node:fs/promises";
61679
61776
 
61777
+ // src/lib/cas-import.ts
61778
+ var PLATFORM = "cdsl";
61779
+ async function runCasImport(deps, args) {
61780
+ const parse5 = deps.parse ?? parseCdslCasPdf;
61781
+ const cas2 = await parse5(await deps.readFile(args.filePath), args.password);
61782
+ if (!cas2.reconciled) {
61783
+ throw new Error(`This CAS doesn't add up: holdings don't match CDSL's demat total (${cas2.dematValue ?? "missing"}). Not importing.`);
61784
+ }
61785
+ const res = await deps.client.get("/api/investments");
61786
+ const byIsin = new Map;
61787
+ for (const h2 of res.holdings ?? []) {
61788
+ for (const key of [h2.isin, h2.symbol])
61789
+ if (key)
61790
+ byIsin.set(key.toUpperCase(), h2);
61791
+ }
61792
+ const tracked = [];
61793
+ const toAdd = [];
61794
+ for (const h2 of cas2.holdings) {
61795
+ const existing = byIsin.get(h2.isin);
61796
+ if (existing) {
61797
+ tracked.push({
61798
+ isin: h2.isin,
61799
+ name: existing.name,
61800
+ casValue: h2.value,
61801
+ paisaValue: existing.currentValue,
61802
+ diff: Math.round(h2.value - existing.currentValue)
61803
+ });
61804
+ } else
61805
+ toAdd.push(h2);
61806
+ }
61807
+ const hasNps = (res.holdings ?? []).some((h2) => h2.type === "nps");
61808
+ const addNps = cas2.npsValue !== null && cas2.npsValue > 0 && !hasNps;
61809
+ const plan = {
61810
+ asOf: cas2.asOf,
61811
+ casTotal: cas2.totalValue,
61812
+ dematValue: cas2.dematValue,
61813
+ npsValue: cas2.npsValue,
61814
+ alreadyTracked: { n: tracked.length, rows: tracked },
61815
+ toAdd: {
61816
+ n: toAdd.length + (addNps ? 1 : 0),
61817
+ rows: toAdd.map((h2) => ({
61818
+ isin: h2.isin,
61819
+ name: h2.name,
61820
+ kind: h2.kind,
61821
+ units: h2.units,
61822
+ value: h2.value
61823
+ })),
61824
+ nps: addNps ? cas2.npsValue : null
61825
+ },
61826
+ note: "CAS has no cost basis: new holdings use invested = current value until edited."
61827
+ };
61828
+ if (!args.confirm)
61829
+ return {
61830
+ ...plan,
61831
+ dryRun: true,
61832
+ next: "Call import_statement from='cas' with the same args to add the new holdings."
61833
+ };
61834
+ const owner = args.owner ?? "user_1";
61835
+ let added = 0;
61836
+ for (const h2 of toAdd) {
61837
+ await deps.client.post("/api/investments/holdings", {
61838
+ name: h2.name || h2.isin,
61839
+ holdingType: h2.kind,
61840
+ owner,
61841
+ platform: PLATFORM,
61842
+ isin: h2.isin,
61843
+ units: h2.units.toFixed(4),
61844
+ investedAmount: h2.value.toFixed(2),
61845
+ currentValue: h2.value.toFixed(2)
61846
+ });
61847
+ added++;
61848
+ }
61849
+ if (addNps && cas2.npsValue !== null) {
61850
+ await deps.client.post("/api/investments/holdings", {
61851
+ name: "NPS",
61852
+ holdingType: "nps",
61853
+ owner,
61854
+ platform: PLATFORM,
61855
+ investedAmount: cas2.npsValue.toFixed(2),
61856
+ currentValue: cas2.npsValue.toFixed(2)
61857
+ });
61858
+ added++;
61859
+ }
61860
+ return { ...plan, dryRun: false, added };
61861
+ }
61862
+
61680
61863
  // src/lib/statement-import.ts
61681
61864
  import { readFile } from "node:fs/promises";
61682
61865
  import { basename } from "node:path";
@@ -61861,6 +62044,12 @@ function statementImportTools(client, crypto4) {
61861
62044
  parsers: realParsers,
61862
62045
  onProgress: progressFor(ctx)
61863
62046
  }, confirm2 ? { ...args, confirm: true } : args));
62047
+ const casInput = exports_external.object({
62048
+ filePath: exports_external.string().describe("Absolute path"),
62049
+ password: exports_external.string().optional().describe("cas: PAN"),
62050
+ owner: exports_external.string().optional()
62051
+ });
62052
+ const cas2 = (confirm2) => variant(casInput, (args) => runCasImport({ client, readFile: readFileAsArrayBuffer }, { ...args, confirm: confirm2 }));
61864
62053
  const csvParse = variant(exports_external.object({ filePath: exports_external.string() }), async ({ filePath }) => {
61865
62054
  if (crypto4)
61866
62055
  return CSV_PRIVATE_MODE_ERROR;
@@ -61872,16 +62061,17 @@ function statementImportTools(client, crypto4) {
61872
62061
  });
61873
62062
  const preview = unionTool({
61874
62063
  name: "preview_import",
61875
- description: "Preview a local statement; writes nothing. pdf (HDFC savings/cards, Amex, SBI savings): filePath, account (missing? upsert_entity entity='bank_account' first) — returns already-in-Paisa / conflict / new counts and totals; show the user, then import_statement with the same args. csv: filePath — returns headers, sample rows and the detected mapping.",
62064
+ description: "Preview a local statement; writes nothing. pdf (HDFC savings/cards, Amex, SBI savings): filePath, account (missing? upsert_entity entity='bank_account' first) — returns already-in-Paisa / conflict / new counts and totals; show the user, then import_statement with the same args. csv: filePath — returns headers, sample rows and the detected mapping. cas (CDSL CAS): filePath, password.",
61876
62065
  key: "from",
61877
- variants: { pdf: pdf(false), csv: csvParse }
62066
+ variants: { pdf: pdf(false), csv: csvParse, cas: cas2(false) }
61878
62067
  });
61879
62068
  const commit = unionTool({
61880
62069
  name: "import_statement",
61881
- description: "Import transactions, then auto-categorize. pdf: filePath, account — only rows preview_import reported new. " + "csv: filePath, mapping (from preview_import); duplicates skipped; date-range overlap → 409 unless force; not in private mode. " + "rows: accountId, source, granularity, transactions (reconciled, no duplicates). " + "json_file: same but filePath of a JSON array of those rows — for large statements.",
62070
+ description: "Import transactions, then auto-categorize. pdf: filePath, account — only rows preview_import reported new. " + "csv: filePath, mapping (from preview_import); duplicates skipped; date-range overlap → 409 unless force; not in private mode. " + "rows: accountId, source, granularity, transactions (reconciled, no duplicates). " + "json_file: same but filePath of a JSON array of those rows — for large statements. " + "cas: filePath, password — adds untracked holdings.",
61882
62071
  key: "from",
61883
62072
  describe: { filePath: "Absolute path" },
61884
62073
  variants: {
62074
+ cas: cas2(true),
61885
62075
  pdf: pdf(true),
61886
62076
  csv: variant(exports_external.object({
61887
62077
  filePath: exports_external.string(),
@@ -62117,7 +62307,7 @@ function registerResources2(server2, tools) {
62117
62307
  // package.json
62118
62308
  var package_default = {
62119
62309
  name: "paisa-mcp",
62120
- version: "0.4.7",
62310
+ version: "0.4.9",
62121
62311
  repository: {
62122
62312
  type: "git",
62123
62313
  url: "git+https://github.com/nimit9/paisa.git",
@@ -62216,5 +62406,5 @@ main().catch((err) => {
62216
62406
  process.exit(1);
62217
62407
  });
62218
62408
 
62219
- //# debugId=55A6DF6E25797A6764756E2164756E21
62409
+ //# debugId=42F8B288CDC176C264756E2164756E21
62220
62410
  //# sourceMappingURL=index.js.map