paisa-mcp 0.0.7 → 0.0.10
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 +166 -29
- package/dist/index.js.map +7 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42360,7 +42360,39 @@ function registerAlertTools(server, client) {
|
|
|
42360
42360
|
}
|
|
42361
42361
|
|
|
42362
42362
|
// src/tools/analytics.ts
|
|
42363
|
-
function
|
|
42363
|
+
async function dec(value, key) {
|
|
42364
|
+
if (typeof value !== "string" || !value)
|
|
42365
|
+
return value;
|
|
42366
|
+
try {
|
|
42367
|
+
return await decryptField(value, key);
|
|
42368
|
+
} catch {
|
|
42369
|
+
return value;
|
|
42370
|
+
}
|
|
42371
|
+
}
|
|
42372
|
+
async function decryptAnalytics(data, key) {
|
|
42373
|
+
if (!data || typeof data !== "object")
|
|
42374
|
+
return;
|
|
42375
|
+
const d = data;
|
|
42376
|
+
const topMerchants = d.topMerchants;
|
|
42377
|
+
if (Array.isArray(topMerchants)) {
|
|
42378
|
+
for (const m of topMerchants)
|
|
42379
|
+
m.name = await dec(m.name, key);
|
|
42380
|
+
}
|
|
42381
|
+
const recent = d.recentTransactions;
|
|
42382
|
+
if (Array.isArray(recent)) {
|
|
42383
|
+
for (const t of recent) {
|
|
42384
|
+
t.description = await dec(t.description, key);
|
|
42385
|
+
if ("merchantName" in t)
|
|
42386
|
+
t.merchantName = await dec(t.merchantName, key);
|
|
42387
|
+
}
|
|
42388
|
+
}
|
|
42389
|
+
const merchant = d.merchant;
|
|
42390
|
+
if (merchant && typeof merchant === "object") {
|
|
42391
|
+
merchant.cleanName = await dec(merchant.cleanName, key);
|
|
42392
|
+
merchant.rawId = await dec(merchant.rawId, key);
|
|
42393
|
+
}
|
|
42394
|
+
}
|
|
42395
|
+
function registerAnalyticsTools(server, client, crypto3) {
|
|
42364
42396
|
server.registerTool("get_analytics", {
|
|
42365
42397
|
description: "Analytics reports. Select one via `report`:\n" + "- monthly_summary: Income, expenses, and investments for a specific month, broken down by category type. Includes budget vs actual. Requires `month` and `year`; optional `owner`.\n" + "- spending_trends: Monthly spending trends by category type over the last N months. Optional `months` (default: 6) and `owner`.\n" + "- category_analytics: Detailed spending breakdown for a specific category over time. Requires `slug`; optional `months`.\n" + "- merchant_analytics: Transaction history and spend total for a specific merchant. Requires `id`; optional `months`.",
|
|
42366
42398
|
inputSchema: {
|
|
@@ -42400,6 +42432,8 @@ function registerAnalyticsTools(server, client) {
|
|
|
42400
42432
|
data = await client.get(`/api/analytics/merchants/${params.id}`, params.months ? { months: params.months } : undefined);
|
|
42401
42433
|
break;
|
|
42402
42434
|
}
|
|
42435
|
+
if (crypto3)
|
|
42436
|
+
await decryptAnalytics(data, crypto3.key);
|
|
42403
42437
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
42404
42438
|
});
|
|
42405
42439
|
}
|
|
@@ -42911,6 +42945,19 @@ function registerInvestmentTools(server, client) {
|
|
|
42911
42945
|
}
|
|
42912
42946
|
|
|
42913
42947
|
// src/tools/merchants.ts
|
|
42948
|
+
async function categorySlugToId(client) {
|
|
42949
|
+
const res = await client.get("/api/categories");
|
|
42950
|
+
const map2 = new Map;
|
|
42951
|
+
for (const root of res.data ?? []) {
|
|
42952
|
+
if (root.slug)
|
|
42953
|
+
map2.set(root.slug, root.id);
|
|
42954
|
+
for (const child of root.children ?? []) {
|
|
42955
|
+
if (child.slug)
|
|
42956
|
+
map2.set(child.slug, child.id);
|
|
42957
|
+
}
|
|
42958
|
+
}
|
|
42959
|
+
return map2;
|
|
42960
|
+
}
|
|
42914
42961
|
function registerMerchantTools(server, client, crypto3) {
|
|
42915
42962
|
server.registerTool("upsert_merchant", {
|
|
42916
42963
|
description: "Create or update a merchant in the dictionary. Omit `id` to add a new merchant (future transactions from it are auto-categorised); pass `id` to update an existing merchant's name, category, or flags.",
|
|
@@ -42918,13 +42965,21 @@ function registerMerchantTools(server, client, crypto3) {
|
|
|
42918
42965
|
id: exports_external.string().optional().describe("Merchant UUID — pass to update, omit to create"),
|
|
42919
42966
|
rawId: exports_external.string().optional().describe("Raw identifier — UPI ID or normalised name (required when creating)"),
|
|
42920
42967
|
cleanName: exports_external.string().optional().describe('Human-friendly name (e.g. "DMart")'),
|
|
42921
|
-
categorySlug: exports_external.string().optional(),
|
|
42968
|
+
categorySlug: exports_external.string().optional().describe("Category slug, e.g. 'food_delivery'"),
|
|
42922
42969
|
isPerson: exports_external.boolean().optional(),
|
|
42923
42970
|
isRecurring: exports_external.boolean().optional(),
|
|
42924
42971
|
notes: exports_external.string().optional()
|
|
42925
42972
|
}
|
|
42926
|
-
}, async ({ id, ...rest }) => {
|
|
42973
|
+
}, async ({ id, categorySlug, ...rest }) => {
|
|
42927
42974
|
const payload = { ...rest };
|
|
42975
|
+
if (categorySlug) {
|
|
42976
|
+
const slugMap = await categorySlugToId(client);
|
|
42977
|
+
const categoryId = slugMap.get(categorySlug);
|
|
42978
|
+
if (!categoryId) {
|
|
42979
|
+
throw new Error(`Unknown categorySlug "${categorySlug}". Use list_categories to see valid slugs.`);
|
|
42980
|
+
}
|
|
42981
|
+
payload.categoryId = categoryId;
|
|
42982
|
+
}
|
|
42928
42983
|
if (crypto3)
|
|
42929
42984
|
await encryptMerchantFieldsOnObject(payload, crypto3.key);
|
|
42930
42985
|
const data = id ? await client.patch(`/api/merchants/${id}`, payload) : await client.post("/api/merchants", payload);
|
|
@@ -42938,19 +42993,32 @@ function registerMerchantTools(server, client, crypto3) {
|
|
|
42938
42993
|
}, async ({ rawId }) => {
|
|
42939
42994
|
if (crypto3 && rawId) {
|
|
42940
42995
|
const data2 = await client.get("/api/merchants");
|
|
42941
|
-
if (data2.
|
|
42942
|
-
await decryptMerchantFields(data2.
|
|
42996
|
+
if (data2.data)
|
|
42997
|
+
await decryptMerchantFields(data2.data, crypto3.key);
|
|
42943
42998
|
const needle = rawId.toLowerCase();
|
|
42944
|
-
const merchants = (data2.
|
|
42999
|
+
const merchants = (data2.data ?? []).filter((m) => String(m.rawId ?? "").toLowerCase() === needle || String(m.cleanName ?? "").toLowerCase().includes(needle));
|
|
42945
43000
|
return {
|
|
42946
|
-
content: [{ type: "text", text: JSON.stringify({ ...data2, merchants }) }]
|
|
43001
|
+
content: [{ type: "text", text: JSON.stringify({ ...data2, data: merchants }) }]
|
|
42947
43002
|
};
|
|
42948
43003
|
}
|
|
42949
43004
|
const data = rawId ? await client.get("/api/merchants", {
|
|
42950
43005
|
search: rawId
|
|
42951
43006
|
}) : await client.get("/api/merchants");
|
|
42952
|
-
if (crypto3 && data.
|
|
42953
|
-
await decryptMerchantFields(data.
|
|
43007
|
+
if (crypto3 && data.data)
|
|
43008
|
+
await decryptMerchantFields(data.data, crypto3.key);
|
|
43009
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43010
|
+
});
|
|
43011
|
+
server.registerTool("delete_merchant", {
|
|
43012
|
+
description: "Delete merchant(s) from the dictionary. Provide `id` for a single merchant or `ids` for several. Transactions referencing a deleted merchant have their merchantId cleared. PERMANENT.",
|
|
43013
|
+
inputSchema: {
|
|
43014
|
+
id: exports_external.string().optional().describe("Single merchant UUID"),
|
|
43015
|
+
ids: exports_external.array(exports_external.string()).min(1).optional().describe("Multiple merchant UUIDs (bulk delete)")
|
|
43016
|
+
}
|
|
43017
|
+
}, async ({ id, ids }) => {
|
|
43018
|
+
if (!id && (!ids || ids.length === 0)) {
|
|
43019
|
+
throw new Error("Provide either id or ids");
|
|
43020
|
+
}
|
|
43021
|
+
const data = ids ? await client.post("/api/merchants/bulk-delete", { ids }) : await client.delete(`/api/merchants/${id}`);
|
|
42954
43022
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
42955
43023
|
});
|
|
42956
43024
|
}
|
|
@@ -43164,6 +43232,9 @@ function registerReportTools(server, client) {
|
|
|
43164
43232
|
});
|
|
43165
43233
|
}
|
|
43166
43234
|
|
|
43235
|
+
// src/tools/transactions.ts
|
|
43236
|
+
import { readFile } from "node:fs/promises";
|
|
43237
|
+
|
|
43167
43238
|
// ../reconciliation/src/aliases.ts
|
|
43168
43239
|
var MERCHANT_ALIASES = [
|
|
43169
43240
|
["Swiggy", ["swiggy", "bundl technologies", "swiggy bundl"]],
|
|
@@ -45241,7 +45312,27 @@ function settlementEnrichedData(description, type, source) {
|
|
|
45241
45312
|
return out;
|
|
45242
45313
|
}
|
|
45243
45314
|
// src/tools/transactions.ts
|
|
45244
|
-
async function
|
|
45315
|
+
async function importTransactions(client, crypto3, opts) {
|
|
45316
|
+
const rows = opts.txns.map((t2) => {
|
|
45317
|
+
const enrichedData = settlementEnrichedData(t2.description ?? "", t2.type, opts.source);
|
|
45318
|
+
return enrichedData ? { ...t2, enrichedData } : { ...t2 };
|
|
45319
|
+
});
|
|
45320
|
+
if (crypto3)
|
|
45321
|
+
await encryptTransactionFields(rows, crypto3.key);
|
|
45322
|
+
const CHUNK = 200;
|
|
45323
|
+
let imported = 0;
|
|
45324
|
+
for (let i2 = 0;i2 < rows.length; i2 += CHUNK) {
|
|
45325
|
+
const res = await client.post("/api/transactions/import-batch", {
|
|
45326
|
+
accountId: opts.accountId,
|
|
45327
|
+
source: opts.source,
|
|
45328
|
+
granularity: opts.granularity,
|
|
45329
|
+
transactions: rows.slice(i2, i2 + CHUNK)
|
|
45330
|
+
});
|
|
45331
|
+
imported += res.imported ?? 0;
|
|
45332
|
+
}
|
|
45333
|
+
return { imported };
|
|
45334
|
+
}
|
|
45335
|
+
async function runRescan(client, crypto3) {
|
|
45245
45336
|
const EMPTY_BAG2 = {
|
|
45246
45337
|
merchantAliases: [],
|
|
45247
45338
|
personAliases: [],
|
|
@@ -45255,10 +45346,23 @@ async function runRescan(client) {
|
|
|
45255
45346
|
} catch {}
|
|
45256
45347
|
const merchantsRes = await client.get("/api/merchants");
|
|
45257
45348
|
const allMerchants = merchantsRes.data ?? [];
|
|
45349
|
+
if (crypto3)
|
|
45350
|
+
await decryptMerchantFields(allMerchants, crypto3.key);
|
|
45258
45351
|
const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45259
45352
|
const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45353
|
+
const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
|
|
45260
45354
|
const personsRes = await client.get("/api/persons");
|
|
45261
|
-
const persons =
|
|
45355
|
+
const persons = [];
|
|
45356
|
+
for (const p2 of personsRes.data ?? []) {
|
|
45357
|
+
let name = p2.name ?? "";
|
|
45358
|
+
if (crypto3 && p2.cipher) {
|
|
45359
|
+
try {
|
|
45360
|
+
name = (await decryptPersonCipher(p2.cipher, crypto3.key)).name;
|
|
45361
|
+
} catch {}
|
|
45362
|
+
}
|
|
45363
|
+
if (name)
|
|
45364
|
+
persons.push({ id: p2.id, name });
|
|
45365
|
+
}
|
|
45262
45366
|
const allRows = [];
|
|
45263
45367
|
const limit = 200;
|
|
45264
45368
|
let cursor;
|
|
@@ -45269,24 +45373,32 @@ async function runRescan(client) {
|
|
|
45269
45373
|
params.cursor = cursor;
|
|
45270
45374
|
const res = await client.get("/api/transactions", params);
|
|
45271
45375
|
const rows = res.data ?? [];
|
|
45376
|
+
if (crypto3)
|
|
45377
|
+
await decryptTransactionFields(rows, crypto3.key);
|
|
45272
45378
|
allRows.push(...rows);
|
|
45273
45379
|
if (!res.hasMore || !res.nextCursor || rows.length === 0)
|
|
45274
45380
|
break;
|
|
45275
45381
|
cursor = res.nextCursor;
|
|
45276
45382
|
}
|
|
45277
45383
|
const plan = autoCategorizeFromDescriptions({ rows: allRows, bag, globalMerchants, localMerchants, persons });
|
|
45278
|
-
|
|
45279
|
-
|
|
45280
|
-
|
|
45281
|
-
|
|
45282
|
-
|
|
45384
|
+
let categorized = 0;
|
|
45385
|
+
const links = plan.suggestions.flatMap((s2) => {
|
|
45386
|
+
const categoryId = s2.categoryId ?? (s2.entity.type === "merchant" ? merchantCategoryById.get(s2.entity.merchantId) ?? null : null);
|
|
45387
|
+
if (categoryId)
|
|
45388
|
+
categorized += s2.transactionIds.length;
|
|
45389
|
+
return s2.transactionIds.map((txId) => ({
|
|
45390
|
+
transactionId: txId,
|
|
45391
|
+
...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
|
|
45392
|
+
...categoryId ? { categoryId } : {}
|
|
45393
|
+
}));
|
|
45394
|
+
});
|
|
45283
45395
|
if (links.length > 0) {
|
|
45284
45396
|
const CHUNK = 500;
|
|
45285
45397
|
for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
|
|
45286
45398
|
await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
|
|
45287
45399
|
}
|
|
45288
45400
|
}
|
|
45289
|
-
return { linked: links.length, unresolved: plan.unresolvedCount };
|
|
45401
|
+
return { linked: links.length, categorized, unresolved: plan.unresolvedCount };
|
|
45290
45402
|
}
|
|
45291
45403
|
function registerTransactionTools(server, client, crypto3) {
|
|
45292
45404
|
server.registerTool("get_transactions", {
|
|
@@ -45383,6 +45495,8 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45383
45495
|
}
|
|
45384
45496
|
}, async ({ accountId, startDate, endDate }) => {
|
|
45385
45497
|
const data = await client.get("/api/transactions/reconciliation-context", { accountId, startDate, endDate });
|
|
45498
|
+
if (crypto3 && data.data)
|
|
45499
|
+
await decryptTransactionFields(data.data, crypto3.key);
|
|
45386
45500
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45387
45501
|
});
|
|
45388
45502
|
server.registerTool("import_statement_batch", {
|
|
@@ -45400,19 +45514,13 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45400
45514
|
})).describe("Transactions to insert — should not include duplicates")
|
|
45401
45515
|
}
|
|
45402
45516
|
}, async ({ accountId, source, granularity, transactions: txns }) => {
|
|
45403
|
-
const
|
|
45404
|
-
const enrichedData = settlementEnrichedData(String(t2.description ?? ""), t2.type, source);
|
|
45405
|
-
return enrichedData ? { ...t2, enrichedData } : { ...t2 };
|
|
45406
|
-
});
|
|
45407
|
-
if (crypto3)
|
|
45408
|
-
await encryptTransactionFields(rows, crypto3.key);
|
|
45409
|
-
const data = await client.post("/api/transactions/import-batch", {
|
|
45517
|
+
const data = await importTransactions(client, crypto3, {
|
|
45410
45518
|
accountId,
|
|
45411
45519
|
source,
|
|
45412
45520
|
granularity,
|
|
45413
|
-
|
|
45521
|
+
txns
|
|
45414
45522
|
});
|
|
45415
|
-
const rescan = await runRescan(client);
|
|
45523
|
+
const rescan = await runRescan(client, crypto3);
|
|
45416
45524
|
return {
|
|
45417
45525
|
content: [
|
|
45418
45526
|
{
|
|
@@ -45422,6 +45530,33 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45422
45530
|
]
|
|
45423
45531
|
};
|
|
45424
45532
|
});
|
|
45533
|
+
server.registerTool("import_statement_file", {
|
|
45534
|
+
description: "Bulk-import a parsed statement from a local JSON file (avoids passing thousands of rows as args). " + "The file must contain a JSON array of {date (YYYY-MM-DD), amount (>0), type ('debit'|'credit'), description, referenceNumber?}. " + "Encrypts (private mode), inserts in chunks, then runs categorization ONCE at the end. Use this for large statements.",
|
|
45535
|
+
inputSchema: {
|
|
45536
|
+
accountId: exports_external.string().describe("Bank account UUID the statement belongs to"),
|
|
45537
|
+
source: exports_external.enum(["bank_statement", "cc_statement", "manual", "telegram", "wisprflow", "ai_import"]).describe("Origin of the statement"),
|
|
45538
|
+
granularity: exports_external.number().int().min(1).max(5).describe("Source trust level: 1=bank statement, 2=CC statement, 3=manual, 4=app export, 5=telegram/AI"),
|
|
45539
|
+
filePath: exports_external.string().describe("Absolute path to a JSON file containing the parsed transactions array")
|
|
45540
|
+
}
|
|
45541
|
+
}, async ({ accountId, source, granularity, filePath }) => {
|
|
45542
|
+
let txns;
|
|
45543
|
+
try {
|
|
45544
|
+
const raw = await readFile(filePath, "utf8");
|
|
45545
|
+
const parsed = JSON.parse(raw);
|
|
45546
|
+
if (!Array.isArray(parsed))
|
|
45547
|
+
throw new Error("File must contain a JSON array");
|
|
45548
|
+
txns = parsed;
|
|
45549
|
+
} catch (e2) {
|
|
45550
|
+
throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
45551
|
+
}
|
|
45552
|
+
const data = await importTransactions(client, crypto3, { accountId, source, granularity, txns });
|
|
45553
|
+
const rescan = await runRescan(client, crypto3);
|
|
45554
|
+
return {
|
|
45555
|
+
content: [
|
|
45556
|
+
{ type: "text", text: JSON.stringify({ import: data, count: txns.length, rescan }) }
|
|
45557
|
+
]
|
|
45558
|
+
};
|
|
45559
|
+
});
|
|
45425
45560
|
server.registerTool("delete_transactions", {
|
|
45426
45561
|
description: "Hard-delete transactions. This is PERMANENT and cannot be undone. " + "Provide exactly one of: `id` (single transaction), `ids` (a list of transactions), " + "or `filters` (deletes all matching transactions). Returns the number of deleted transactions " + "for the bulk paths.",
|
|
45427
45562
|
inputSchema: {
|
|
@@ -45453,7 +45588,7 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45453
45588
|
description: "Apply the household's saved learning rules (merchant aliases + category rules) to all uncategorized transactions. " + "Equivalent to the browser's auto-categorize flow. Run this after importing statements via MCP to populate merchants and categories.",
|
|
45454
45589
|
inputSchema: {}
|
|
45455
45590
|
}, async () => {
|
|
45456
|
-
const result = await runRescan(client);
|
|
45591
|
+
const result = await runRescan(client, crypto3);
|
|
45457
45592
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
45458
45593
|
});
|
|
45459
45594
|
server.registerTool("get_settlement_candidates", {
|
|
@@ -45463,6 +45598,8 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45463
45598
|
}
|
|
45464
45599
|
}, async ({ ccAccountId }) => {
|
|
45465
45600
|
const data = await client.get("/api/transactions/settlement-candidates", { ccAccountId });
|
|
45601
|
+
if (crypto3 && data.data)
|
|
45602
|
+
await decryptTransactionFields(data.data, crypto3.key);
|
|
45466
45603
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45467
45604
|
});
|
|
45468
45605
|
}
|
|
@@ -45529,7 +45666,7 @@ async function main() {
|
|
|
45529
45666
|
registerMerchantTools(server, client, crypto3);
|
|
45530
45667
|
registerPersonTools(server, client, crypto3);
|
|
45531
45668
|
registerCategoryTools(server, client);
|
|
45532
|
-
registerAnalyticsTools(server, client);
|
|
45669
|
+
registerAnalyticsTools(server, client, crypto3);
|
|
45533
45670
|
registerAccountTools(server, client);
|
|
45534
45671
|
registerGoalTools(server, client);
|
|
45535
45672
|
registerBudgetTools(server, client);
|
|
@@ -45559,5 +45696,5 @@ main().catch((err) => {
|
|
|
45559
45696
|
process.exit(1);
|
|
45560
45697
|
});
|
|
45561
45698
|
|
|
45562
|
-
//# debugId=
|
|
45699
|
+
//# debugId=FB29DDFDDAE15AFC64756E2164756E21
|
|
45563
45700
|
//# sourceMappingURL=index.js.map
|