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 +192 -15
- package/dist/index.js.map +7 -5
- package/package.json +1 -1
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", {
|
|
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
|
|
59310
|
-
return Number.isNaN(
|
|
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
|
|
59959
|
-
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
|
|
59968
|
-
return Number.isNaN(
|
|
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
|
|
60214
|
-
return Number.isNaN(
|
|
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.
|
|
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=
|
|
62390
|
+
//# debugId=C59CD090C0030F9764756E2164756E21
|
|
62214
62391
|
//# sourceMappingURL=index.js.map
|