paisa-mcp 0.4.7 → 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 +184 -13
- package/dist/index.js.map +6 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59272,6 +59272,84 @@ function parseAmount(raw) {
|
|
|
59272
59272
|
const num = parseFloat(cleaned);
|
|
59273
59273
|
return Number.isNaN(num) ? 0 : Math.abs(num);
|
|
59274
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
|
+
}
|
|
59275
59353
|
// ../parsers/src/classify.ts
|
|
59276
59354
|
var CORPORATE_TOKENS = [
|
|
59277
59355
|
"PVT",
|
|
@@ -59312,8 +59390,8 @@ function parseAmount2(cell) {
|
|
|
59312
59390
|
if (typeof cell === "number")
|
|
59313
59391
|
return Math.abs(cell);
|
|
59314
59392
|
const cleaned = String(cell).replace(/,/g, "").trim();
|
|
59315
|
-
const
|
|
59316
|
-
return Number.isNaN(
|
|
59393
|
+
const num2 = parseFloat(cleaned);
|
|
59394
|
+
return Number.isNaN(num2) ? 0 : Math.abs(num2);
|
|
59317
59395
|
}
|
|
59318
59396
|
|
|
59319
59397
|
// ../parsers/src/hdfc/narration.ts
|
|
@@ -59961,8 +60039,8 @@ function parseHdfcDate(raw) {
|
|
|
59961
60039
|
const month2 = match[2];
|
|
59962
60040
|
let year = match[3];
|
|
59963
60041
|
if (year.length === 2) {
|
|
59964
|
-
const
|
|
59965
|
-
year =
|
|
60042
|
+
const num2 = parseInt(year, 10);
|
|
60043
|
+
year = num2 > 50 ? `19${year}` : `20${year}`;
|
|
59966
60044
|
}
|
|
59967
60045
|
return `${year}-${month2}-${day}`;
|
|
59968
60046
|
}
|
|
@@ -59970,8 +60048,8 @@ function parseAmount3(raw) {
|
|
|
59970
60048
|
if (!raw)
|
|
59971
60049
|
return 0;
|
|
59972
60050
|
const cleaned = raw.replace(/,/g, "").trim();
|
|
59973
|
-
const
|
|
59974
|
-
return Number.isNaN(
|
|
60051
|
+
const num2 = parseFloat(cleaned);
|
|
60052
|
+
return Number.isNaN(num2) ? 0 : Math.abs(num2);
|
|
59975
60053
|
}
|
|
59976
60054
|
// ../parsers/src/sbi/savings-pdf.ts
|
|
59977
60055
|
var _pdfjs4 = null;
|
|
@@ -60216,8 +60294,8 @@ function parseAmount4(raw) {
|
|
|
60216
60294
|
if (!raw)
|
|
60217
60295
|
return 0;
|
|
60218
60296
|
const cleaned = raw.replace(/,/g, "").trim();
|
|
60219
|
-
const
|
|
60220
|
-
return Number.isNaN(
|
|
60297
|
+
const num2 = parseFloat(cleaned);
|
|
60298
|
+
return Number.isNaN(num2) ? 0 : Math.abs(num2);
|
|
60221
60299
|
}
|
|
60222
60300
|
// src/lib/plan-hash.ts
|
|
60223
60301
|
import { createHash } from "node:crypto";
|
|
@@ -61677,6 +61755,92 @@ function settingsTools(client) {
|
|
|
61677
61755
|
// src/tools/statement-import.ts
|
|
61678
61756
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
61679
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
|
+
|
|
61680
61844
|
// src/lib/statement-import.ts
|
|
61681
61845
|
import { readFile } from "node:fs/promises";
|
|
61682
61846
|
import { basename } from "node:path";
|
|
@@ -61861,6 +62025,12 @@ function statementImportTools(client, crypto4) {
|
|
|
61861
62025
|
parsers: realParsers,
|
|
61862
62026
|
onProgress: progressFor(ctx)
|
|
61863
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 }));
|
|
61864
62034
|
const csvParse = variant(exports_external.object({ filePath: exports_external.string() }), async ({ filePath }) => {
|
|
61865
62035
|
if (crypto4)
|
|
61866
62036
|
return CSV_PRIVATE_MODE_ERROR;
|
|
@@ -61872,16 +62042,17 @@ function statementImportTools(client, crypto4) {
|
|
|
61872
62042
|
});
|
|
61873
62043
|
const preview = unionTool({
|
|
61874
62044
|
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.",
|
|
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.",
|
|
61876
62046
|
key: "from",
|
|
61877
|
-
variants: { pdf: pdf(false), csv: csvParse }
|
|
62047
|
+
variants: { pdf: pdf(false), csv: csvParse, cas: cas2(false) }
|
|
61878
62048
|
});
|
|
61879
62049
|
const commit = unionTool({
|
|
61880
62050
|
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.",
|
|
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.",
|
|
61882
62052
|
key: "from",
|
|
61883
62053
|
describe: { filePath: "Absolute path" },
|
|
61884
62054
|
variants: {
|
|
62055
|
+
cas: cas2(true),
|
|
61885
62056
|
pdf: pdf(true),
|
|
61886
62057
|
csv: variant(exports_external.object({
|
|
61887
62058
|
filePath: exports_external.string(),
|
|
@@ -62117,7 +62288,7 @@ function registerResources2(server2, tools) {
|
|
|
62117
62288
|
// package.json
|
|
62118
62289
|
var package_default = {
|
|
62119
62290
|
name: "paisa-mcp",
|
|
62120
|
-
version: "0.4.
|
|
62291
|
+
version: "0.4.8",
|
|
62121
62292
|
repository: {
|
|
62122
62293
|
type: "git",
|
|
62123
62294
|
url: "git+https://github.com/nimit9/paisa.git",
|
|
@@ -62216,5 +62387,5 @@ main().catch((err) => {
|
|
|
62216
62387
|
process.exit(1);
|
|
62217
62388
|
});
|
|
62218
62389
|
|
|
62219
|
-
//# debugId=
|
|
62390
|
+
//# debugId=C59CD090C0030F9764756E2164756E21
|
|
62220
62391
|
//# sourceMappingURL=index.js.map
|