paisa-mcp 0.0.6 → 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 +175 -34
- 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,34 +45346,59 @@ 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
|
-
let offset = 0;
|
|
45264
45367
|
const limit = 200;
|
|
45265
|
-
|
|
45266
|
-
|
|
45368
|
+
let cursor;
|
|
45369
|
+
const MAX_PAGES = 100;
|
|
45370
|
+
for (let page = 0;page < MAX_PAGES; page++) {
|
|
45371
|
+
const params = { limit };
|
|
45372
|
+
if (cursor)
|
|
45373
|
+
params.cursor = cursor;
|
|
45374
|
+
const res = await client.get("/api/transactions", params);
|
|
45267
45375
|
const rows = res.data ?? [];
|
|
45376
|
+
if (crypto3)
|
|
45377
|
+
await decryptTransactionFields(rows, crypto3.key);
|
|
45268
45378
|
allRows.push(...rows);
|
|
45269
|
-
if (!res.hasMore || rows.length === 0)
|
|
45379
|
+
if (!res.hasMore || !res.nextCursor || rows.length === 0)
|
|
45270
45380
|
break;
|
|
45271
|
-
|
|
45381
|
+
cursor = res.nextCursor;
|
|
45272
45382
|
}
|
|
45273
45383
|
const plan = autoCategorizeFromDescriptions({ rows: allRows, bag, globalMerchants, localMerchants, persons });
|
|
45274
|
-
|
|
45275
|
-
|
|
45276
|
-
|
|
45277
|
-
|
|
45278
|
-
|
|
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
|
+
});
|
|
45279
45395
|
if (links.length > 0) {
|
|
45280
45396
|
const CHUNK = 500;
|
|
45281
45397
|
for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
|
|
45282
45398
|
await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
|
|
45283
45399
|
}
|
|
45284
45400
|
}
|
|
45285
|
-
return { linked: links.length, unresolved: plan.unresolvedCount };
|
|
45401
|
+
return { linked: links.length, categorized, unresolved: plan.unresolvedCount };
|
|
45286
45402
|
}
|
|
45287
45403
|
function registerTransactionTools(server, client, crypto3) {
|
|
45288
45404
|
server.registerTool("get_transactions", {
|
|
@@ -45379,6 +45495,8 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45379
45495
|
}
|
|
45380
45496
|
}, async ({ accountId, startDate, endDate }) => {
|
|
45381
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);
|
|
45382
45500
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45383
45501
|
});
|
|
45384
45502
|
server.registerTool("import_statement_batch", {
|
|
@@ -45396,19 +45514,13 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45396
45514
|
})).describe("Transactions to insert — should not include duplicates")
|
|
45397
45515
|
}
|
|
45398
45516
|
}, async ({ accountId, source, granularity, transactions: txns }) => {
|
|
45399
|
-
const
|
|
45400
|
-
const enrichedData = settlementEnrichedData(String(t2.description ?? ""), t2.type, source);
|
|
45401
|
-
return enrichedData ? { ...t2, enrichedData } : { ...t2 };
|
|
45402
|
-
});
|
|
45403
|
-
if (crypto3)
|
|
45404
|
-
await encryptTransactionFields(rows, crypto3.key);
|
|
45405
|
-
const data = await client.post("/api/transactions/import-batch", {
|
|
45517
|
+
const data = await importTransactions(client, crypto3, {
|
|
45406
45518
|
accountId,
|
|
45407
45519
|
source,
|
|
45408
45520
|
granularity,
|
|
45409
|
-
|
|
45521
|
+
txns
|
|
45410
45522
|
});
|
|
45411
|
-
const rescan = await runRescan(client);
|
|
45523
|
+
const rescan = await runRescan(client, crypto3);
|
|
45412
45524
|
return {
|
|
45413
45525
|
content: [
|
|
45414
45526
|
{
|
|
@@ -45418,6 +45530,33 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45418
45530
|
]
|
|
45419
45531
|
};
|
|
45420
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
|
+
});
|
|
45421
45560
|
server.registerTool("delete_transactions", {
|
|
45422
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.",
|
|
45423
45562
|
inputSchema: {
|
|
@@ -45449,7 +45588,7 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45449
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.",
|
|
45450
45589
|
inputSchema: {}
|
|
45451
45590
|
}, async () => {
|
|
45452
|
-
const result = await runRescan(client);
|
|
45591
|
+
const result = await runRescan(client, crypto3);
|
|
45453
45592
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
45454
45593
|
});
|
|
45455
45594
|
server.registerTool("get_settlement_candidates", {
|
|
@@ -45459,6 +45598,8 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45459
45598
|
}
|
|
45460
45599
|
}, async ({ ccAccountId }) => {
|
|
45461
45600
|
const data = await client.get("/api/transactions/settlement-candidates", { ccAccountId });
|
|
45601
|
+
if (crypto3 && data.data)
|
|
45602
|
+
await decryptTransactionFields(data.data, crypto3.key);
|
|
45462
45603
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45463
45604
|
});
|
|
45464
45605
|
}
|
|
@@ -45525,7 +45666,7 @@ async function main() {
|
|
|
45525
45666
|
registerMerchantTools(server, client, crypto3);
|
|
45526
45667
|
registerPersonTools(server, client, crypto3);
|
|
45527
45668
|
registerCategoryTools(server, client);
|
|
45528
|
-
registerAnalyticsTools(server, client);
|
|
45669
|
+
registerAnalyticsTools(server, client, crypto3);
|
|
45529
45670
|
registerAccountTools(server, client);
|
|
45530
45671
|
registerGoalTools(server, client);
|
|
45531
45672
|
registerBudgetTools(server, client);
|
|
@@ -45555,5 +45696,5 @@ main().catch((err) => {
|
|
|
45555
45696
|
process.exit(1);
|
|
45556
45697
|
});
|
|
45557
45698
|
|
|
45558
|
-
//# debugId=
|
|
45699
|
+
//# debugId=FB29DDFDDAE15AFC64756E2164756E21
|
|
45559
45700
|
//# sourceMappingURL=index.js.map
|