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 +203 -13
- package/dist/index.js.map +8 -5
- package/package.json +1 -1
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
|
|
59316
|
-
return Number.isNaN(
|
|
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
|
|
59965
|
-
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
|
|
59974
|
-
return Number.isNaN(
|
|
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
|
|
60220
|
-
return Number.isNaN(
|
|
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.
|
|
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=
|
|
62409
|
+
//# debugId=42F8B288CDC176C264756E2164756E21
|
|
62220
62410
|
//# sourceMappingURL=index.js.map
|