paisa-mcp 0.4.6 → 0.4.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/dist/index.js CHANGED
@@ -56470,13 +56470,19 @@ function netWorthTools(client) {
56470
56470
  investments: exports_external.string().optional(),
56471
56471
  emergencyFund: exports_external.string().optional(),
56472
56472
  liabilities: exports_external.string().optional(),
56473
- breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional()
56473
+ breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional(),
56474
+ overwrite: exports_external.boolean().optional().describe("mode=auto: replace this month's snapshot even if it was entered by hand")
56474
56475
  }),
56475
56476
  handler: async ({ mode, ...body }) => {
56476
56477
  let data;
56477
56478
  if (mode === "auto") {
56478
- data = await client.post("/api/net-worth/auto", { month: body.month, year: body.year });
56479
+ data = await client.post("/api/net-worth/auto", {
56480
+ month: body.month,
56481
+ year: body.year,
56482
+ overwrite: body.overwrite
56483
+ });
56479
56484
  } else {
56485
+ delete body.overwrite;
56480
56486
  const liquidCash = parseFloat(body.liquidCash ?? "0");
56481
56487
  const investments = parseFloat(body.investments ?? "0");
56482
56488
  const emergencyFund = parseFloat(body.emergencyFund ?? "0");
@@ -59266,6 +59272,84 @@ function parseAmount(raw) {
59266
59272
  const num = parseFloat(cleaned);
59267
59273
  return Number.isNaN(num) ? 0 : Math.abs(num);
59268
59274
  }
59275
+ // ../parsers/src/cdsl/cas.ts
59276
+ 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})$/;
59277
+ var ISIN_START = /^IN[EF][A-Z0-9]{9}\b/;
59278
+ var AS_ON = /HOLDING STATEMENT AS ON (\d{2})-(\d{2})-(\d{4})/;
59279
+ var PORTFOLIO_VALUE = /Portfolio Value\s*`?\s*([\d,]+\.\d{2})\s+as on/;
59280
+ var TOTAL_VALUE = /Total Portfolio Value\s+([\d,]+\.\d{2})/;
59281
+ var num = (s2) => Number(s2.replace(/,/g, ""));
59282
+ function parseCdslCasLines(lines) {
59283
+ let asOf = null;
59284
+ let totalValue = null;
59285
+ const portfolioValues = [];
59286
+ const holdings = [];
59287
+ lines.forEach((raw, i2) => {
59288
+ const line = raw.trim();
59289
+ const asOn = AS_ON.exec(line);
59290
+ if (asOn && !asOf)
59291
+ asOf = `${asOn[3]}-${asOn[2]}-${asOn[1]}`;
59292
+ const total = TOTAL_VALUE.exec(line);
59293
+ if (total && totalValue === null)
59294
+ totalValue = num(total[1]);
59295
+ const pv = PORTFOLIO_VALUE.exec(line);
59296
+ if (pv)
59297
+ portfolioValues.push(num(pv[1]));
59298
+ const m2 = HOLDING_ROW.exec(line);
59299
+ if (!m2)
59300
+ return;
59301
+ const prev = lines[i2 - 1]?.trim() ?? "";
59302
+ const lead = prev && !ISIN_START.test(prev) && !/\d\.\d{2}$/.test(prev) ? prev : "";
59303
+ holdings.push({
59304
+ isin: m2[1],
59305
+ name: `${lead} ${m2[2]}`.replace(/#/g, " ").replace(/\s+/g, " ").trim(),
59306
+ kind: m2[1].startsWith("INF") ? "mutual_fund" : "stock",
59307
+ units: num(m2[3]),
59308
+ price: num(m2[4]),
59309
+ value: num(m2[5])
59310
+ });
59311
+ });
59312
+ const dematValue = portfolioValues[0] ?? null;
59313
+ const npsValue = portfolioValues[1] ?? null;
59314
+ const sum2 = Math.round(holdings.reduce((s2, h2) => s2 + h2.value, 0) * 100) / 100;
59315
+ return {
59316
+ source: "cdsl_cas",
59317
+ asOf,
59318
+ holdings,
59319
+ dematValue,
59320
+ npsValue,
59321
+ totalValue,
59322
+ reconciled: dematValue !== null && Math.abs(sum2 - dematValue) < 1
59323
+ };
59324
+ }
59325
+ async function parseCdslCasPdf(data, password) {
59326
+ const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
59327
+ pdfjs.GlobalWorkerOptions.workerSrc = import.meta.resolve("pdfjs-dist/legacy/build/pdf.worker.mjs");
59328
+ const opts = {
59329
+ data: new Uint8Array(data),
59330
+ useWorkerFetch: false,
59331
+ useSystemFonts: true
59332
+ };
59333
+ if (password)
59334
+ opts.password = password;
59335
+ const pdf = await pdfjs.getDocument(opts).promise;
59336
+ const lines = [];
59337
+ for (let p2 = 1;p2 <= pdf.numPages; p2++) {
59338
+ const content = await (await pdf.getPage(p2)).getTextContent();
59339
+ const rows = new Map;
59340
+ for (const it2 of content.items) {
59341
+ if (!("str" in it2) || !it2.str.trim())
59342
+ continue;
59343
+ const y2 = Math.round(it2.transform[5]);
59344
+ rows.set(y2, (rows.get(y2) ?? []).concat([[it2.transform[4], it2.str]]));
59345
+ }
59346
+ for (const y2 of [...rows.keys()].sort((a2, b2) => b2 - a2)) {
59347
+ const row = rows.get(y2) ?? [];
59348
+ lines.push(row.sort((a2, b2) => a2[0] - b2[0]).map(([, s2]) => s2).join(" ").replace(/\s+/g, " ").trim());
59349
+ }
59350
+ }
59351
+ return parseCdslCasLines(lines);
59352
+ }
59269
59353
  // ../parsers/src/classify.ts
59270
59354
  var CORPORATE_TOKENS = [
59271
59355
  "PVT",
@@ -59306,8 +59390,8 @@ function parseAmount2(cell) {
59306
59390
  if (typeof cell === "number")
59307
59391
  return Math.abs(cell);
59308
59392
  const cleaned = String(cell).replace(/,/g, "").trim();
59309
- const num = parseFloat(cleaned);
59310
- return Number.isNaN(num) ? 0 : Math.abs(num);
59393
+ const num2 = parseFloat(cleaned);
59394
+ return Number.isNaN(num2) ? 0 : Math.abs(num2);
59311
59395
  }
59312
59396
 
59313
59397
  // ../parsers/src/hdfc/narration.ts
@@ -59955,8 +60039,8 @@ function parseHdfcDate(raw) {
59955
60039
  const month2 = match[2];
59956
60040
  let year = match[3];
59957
60041
  if (year.length === 2) {
59958
- const num = parseInt(year, 10);
59959
- year = num > 50 ? `19${year}` : `20${year}`;
60042
+ const num2 = parseInt(year, 10);
60043
+ year = num2 > 50 ? `19${year}` : `20${year}`;
59960
60044
  }
59961
60045
  return `${year}-${month2}-${day}`;
59962
60046
  }
@@ -59964,8 +60048,8 @@ function parseAmount3(raw) {
59964
60048
  if (!raw)
59965
60049
  return 0;
59966
60050
  const cleaned = raw.replace(/,/g, "").trim();
59967
- const num = parseFloat(cleaned);
59968
- return Number.isNaN(num) ? 0 : Math.abs(num);
60051
+ const num2 = parseFloat(cleaned);
60052
+ return Number.isNaN(num2) ? 0 : Math.abs(num2);
59969
60053
  }
59970
60054
  // ../parsers/src/sbi/savings-pdf.ts
59971
60055
  var _pdfjs4 = null;
@@ -60210,8 +60294,8 @@ function parseAmount4(raw) {
60210
60294
  if (!raw)
60211
60295
  return 0;
60212
60296
  const cleaned = raw.replace(/,/g, "").trim();
60213
- const num = parseFloat(cleaned);
60214
- return Number.isNaN(num) ? 0 : Math.abs(num);
60297
+ const num2 = parseFloat(cleaned);
60298
+ return Number.isNaN(num2) ? 0 : Math.abs(num2);
60215
60299
  }
60216
60300
  // src/lib/plan-hash.ts
60217
60301
  import { createHash } from "node:crypto";
@@ -61671,6 +61755,92 @@ function settingsTools(client) {
61671
61755
  // src/tools/statement-import.ts
61672
61756
  import { readFile as readFile2 } from "node:fs/promises";
61673
61757
 
61758
+ // src/lib/cas-import.ts
61759
+ var PLATFORM = "cdsl";
61760
+ async function runCasImport(deps, args) {
61761
+ const parse5 = deps.parse ?? parseCdslCasPdf;
61762
+ const cas2 = await parse5(await deps.readFile(args.filePath), args.password);
61763
+ if (!cas2.reconciled) {
61764
+ throw new Error(`This CAS doesn't add up: holdings don't match CDSL's demat total (${cas2.dematValue ?? "missing"}). Not importing.`);
61765
+ }
61766
+ const res = await deps.client.get("/api/investments");
61767
+ const byIsin = new Map;
61768
+ for (const h2 of res.holdings ?? []) {
61769
+ for (const key of [h2.isin, h2.symbol])
61770
+ if (key)
61771
+ byIsin.set(key.toUpperCase(), h2);
61772
+ }
61773
+ const tracked = [];
61774
+ const toAdd = [];
61775
+ for (const h2 of cas2.holdings) {
61776
+ const existing = byIsin.get(h2.isin);
61777
+ if (existing) {
61778
+ tracked.push({
61779
+ isin: h2.isin,
61780
+ name: existing.name,
61781
+ casValue: h2.value,
61782
+ paisaValue: existing.currentValue,
61783
+ diff: Math.round(h2.value - existing.currentValue)
61784
+ });
61785
+ } else
61786
+ toAdd.push(h2);
61787
+ }
61788
+ const hasNps = (res.holdings ?? []).some((h2) => h2.type === "nps");
61789
+ const addNps = cas2.npsValue !== null && cas2.npsValue > 0 && !hasNps;
61790
+ const plan = {
61791
+ asOf: cas2.asOf,
61792
+ casTotal: cas2.totalValue,
61793
+ dematValue: cas2.dematValue,
61794
+ npsValue: cas2.npsValue,
61795
+ alreadyTracked: { n: tracked.length, rows: tracked },
61796
+ toAdd: {
61797
+ n: toAdd.length + (addNps ? 1 : 0),
61798
+ rows: toAdd.map((h2) => ({
61799
+ isin: h2.isin,
61800
+ name: h2.name,
61801
+ kind: h2.kind,
61802
+ units: h2.units,
61803
+ value: h2.value
61804
+ })),
61805
+ nps: addNps ? cas2.npsValue : null
61806
+ },
61807
+ note: "CAS has no cost basis: new holdings use invested = current value until edited."
61808
+ };
61809
+ if (!args.confirm)
61810
+ return {
61811
+ ...plan,
61812
+ dryRun: true,
61813
+ next: "Call import_statement from='cas' with the same args to add the new holdings."
61814
+ };
61815
+ const owner = args.owner ?? "user_1";
61816
+ let added = 0;
61817
+ for (const h2 of toAdd) {
61818
+ await deps.client.post("/api/investments/holdings", {
61819
+ name: h2.name || h2.isin,
61820
+ holdingType: h2.kind,
61821
+ owner,
61822
+ platform: PLATFORM,
61823
+ isin: h2.isin,
61824
+ units: h2.units.toFixed(4),
61825
+ investedAmount: h2.value.toFixed(2),
61826
+ currentValue: h2.value.toFixed(2)
61827
+ });
61828
+ added++;
61829
+ }
61830
+ if (addNps && cas2.npsValue !== null) {
61831
+ await deps.client.post("/api/investments/holdings", {
61832
+ name: "NPS",
61833
+ holdingType: "nps",
61834
+ owner,
61835
+ platform: PLATFORM,
61836
+ investedAmount: cas2.npsValue.toFixed(2),
61837
+ currentValue: cas2.npsValue.toFixed(2)
61838
+ });
61839
+ added++;
61840
+ }
61841
+ return { ...plan, dryRun: false, added };
61842
+ }
61843
+
61674
61844
  // src/lib/statement-import.ts
61675
61845
  import { readFile } from "node:fs/promises";
61676
61846
  import { basename } from "node:path";
@@ -61855,6 +62025,12 @@ function statementImportTools(client, crypto4) {
61855
62025
  parsers: realParsers,
61856
62026
  onProgress: progressFor(ctx)
61857
62027
  }, confirm2 ? { ...args, confirm: true } : args));
62028
+ const casInput = exports_external.object({
62029
+ filePath: exports_external.string().describe("Absolute path"),
62030
+ password: exports_external.string().optional().describe("cas: PAN"),
62031
+ owner: exports_external.string().optional()
62032
+ });
62033
+ const cas2 = (confirm2) => variant(casInput, (args) => runCasImport({ client, readFile: readFileAsArrayBuffer }, { ...args, confirm: confirm2 }));
61858
62034
  const csvParse = variant(exports_external.object({ filePath: exports_external.string() }), async ({ filePath }) => {
61859
62035
  if (crypto4)
61860
62036
  return CSV_PRIVATE_MODE_ERROR;
@@ -61866,16 +62042,17 @@ function statementImportTools(client, crypto4) {
61866
62042
  });
61867
62043
  const preview = unionTool({
61868
62044
  name: "preview_import",
61869
- 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.",
62045
+ 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.",
61870
62046
  key: "from",
61871
- variants: { pdf: pdf(false), csv: csvParse }
62047
+ variants: { pdf: pdf(false), csv: csvParse, cas: cas2(false) }
61872
62048
  });
61873
62049
  const commit = unionTool({
61874
62050
  name: "import_statement",
61875
- 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.",
62051
+ 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.",
61876
62052
  key: "from",
61877
62053
  describe: { filePath: "Absolute path" },
61878
62054
  variants: {
62055
+ cas: cas2(true),
61879
62056
  pdf: pdf(true),
61880
62057
  csv: variant(exports_external.object({
61881
62058
  filePath: exports_external.string(),
@@ -62111,7 +62288,7 @@ function registerResources2(server2, tools) {
62111
62288
  // package.json
62112
62289
  var package_default = {
62113
62290
  name: "paisa-mcp",
62114
- version: "0.4.6",
62291
+ version: "0.4.8",
62115
62292
  repository: {
62116
62293
  type: "git",
62117
62294
  url: "git+https://github.com/nimit9/paisa.git",
@@ -62210,5 +62387,5 @@ main().catch((err) => {
62210
62387
  process.exit(1);
62211
62388
  });
62212
62389
 
62213
- //# debugId=C090AF152E241F7864756E2164756E21
62390
+ //# debugId=C59CD090C0030F9764756E2164756E21
62214
62391
  //# sourceMappingURL=index.js.map