paisa-mcp 0.4.1 → 0.4.3
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 +427 -261
- package/dist/index.js.map +17 -16
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -38033,17 +38033,6 @@ var inputRequired = Object.assign(buildInputRequired, {
|
|
|
38033
38033
|
return { method: "roots/list" };
|
|
38034
38034
|
}
|
|
38035
38035
|
});
|
|
38036
|
-
function acceptedContent(responses, key, schema) {
|
|
38037
|
-
const view = inputResponse(responses, key);
|
|
38038
|
-
if (view.kind !== "elicit" || view.action !== "accept" || view.content === undefined)
|
|
38039
|
-
return;
|
|
38040
|
-
if (schema === undefined)
|
|
38041
|
-
return view.content;
|
|
38042
|
-
const outcome = schema["~standard"].validate(view.content);
|
|
38043
|
-
if (outcome instanceof Promise)
|
|
38044
|
-
throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
|
|
38045
|
-
return outcome.issues === undefined ? outcome.value : undefined;
|
|
38046
|
-
}
|
|
38047
38036
|
function inputResponse(responses, key) {
|
|
38048
38037
|
if (responses === undefined || typeof responses !== "object" || responses === null)
|
|
38049
38038
|
return { kind: "missing" };
|
|
@@ -53267,25 +53256,8 @@ async function decryptFields(input2, paths, { decrypt, isCiphertext }) {
|
|
|
53267
53256
|
return { data, undecrypted };
|
|
53268
53257
|
}
|
|
53269
53258
|
|
|
53270
|
-
// src/
|
|
53259
|
+
// ../crypto/src/index.ts
|
|
53271
53260
|
var import_hash_wasm = __toESM(require_index_umd(), 1);
|
|
53272
|
-
var ENCRYPTED_TXN_FIELDS = ["description", "referenceNumber", "notes"];
|
|
53273
|
-
var ENCRYPTED_MERCHANT_FIELDS = ["cleanName", "rawId", "notes"];
|
|
53274
|
-
async function hmacWithKey(value, key) {
|
|
53275
|
-
const rawKey = await crypto.subtle.exportKey("raw", key);
|
|
53276
|
-
const hmacKey = await crypto.subtle.importKey("raw", rawKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
53277
|
-
const sig = await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(value));
|
|
53278
|
-
return btoa(String.fromCharCode(...new Uint8Array(sig)));
|
|
53279
|
-
}
|
|
53280
|
-
async function encryptPersonCipher(fields, key) {
|
|
53281
|
-
const cipher = await encryptField(JSON.stringify(fields), key);
|
|
53282
|
-
const searchHash = await hmacWithKey(fields.name.toLowerCase().trim().replace(/\s+/g, " "), key);
|
|
53283
|
-
return { cipher, searchHash };
|
|
53284
|
-
}
|
|
53285
|
-
async function decryptPersonCipher(cipher, key) {
|
|
53286
|
-
const json2 = await decryptField(cipher, key);
|
|
53287
|
-
return JSON.parse(json2);
|
|
53288
|
-
}
|
|
53289
53261
|
async function deriveKey(userId, pin, salt) {
|
|
53290
53262
|
const enc = new TextEncoder;
|
|
53291
53263
|
const baseKey = await crypto.subtle.importKey("raw", enc.encode(userId + pin), "PBKDF2", false, [
|
|
@@ -53329,6 +53301,34 @@ async function decryptField(encrypted, key) {
|
|
|
53329
53301
|
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
53330
53302
|
return new TextDecoder().decode(plaintext);
|
|
53331
53303
|
}
|
|
53304
|
+
var VERIFIER_PLAINTEXT = "paisa-verify-v1";
|
|
53305
|
+
async function checkVerifier(key, encryptedVerifier) {
|
|
53306
|
+
try {
|
|
53307
|
+
const decrypted = await decryptField(encryptedVerifier, key);
|
|
53308
|
+
return decrypted === VERIFIER_PLAINTEXT;
|
|
53309
|
+
} catch {
|
|
53310
|
+
return false;
|
|
53311
|
+
}
|
|
53312
|
+
}
|
|
53313
|
+
|
|
53314
|
+
// src/crypto.ts
|
|
53315
|
+
var ENCRYPTED_TXN_FIELDS = ["description", "referenceNumber", "notes"];
|
|
53316
|
+
var ENCRYPTED_MERCHANT_FIELDS = ["cleanName", "rawId", "notes"];
|
|
53317
|
+
async function hmacWithKey(value, key) {
|
|
53318
|
+
const rawKey = await crypto.subtle.exportKey("raw", key);
|
|
53319
|
+
const hmacKey = await crypto.subtle.importKey("raw", rawKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
53320
|
+
const sig = await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(value));
|
|
53321
|
+
return btoa(String.fromCharCode(...new Uint8Array(sig)));
|
|
53322
|
+
}
|
|
53323
|
+
async function encryptPersonCipher(fields, key) {
|
|
53324
|
+
const cipher = await encryptField(JSON.stringify(fields), key);
|
|
53325
|
+
const searchHash = await hmacWithKey(fields.name.toLowerCase().trim().replace(/\s+/g, " "), key);
|
|
53326
|
+
return { cipher, searchHash };
|
|
53327
|
+
}
|
|
53328
|
+
async function decryptPersonCipher(cipher, key) {
|
|
53329
|
+
const json2 = await decryptField(cipher, key);
|
|
53330
|
+
return JSON.parse(json2);
|
|
53331
|
+
}
|
|
53332
53332
|
async function encryptObjectFields(obj, fields, key) {
|
|
53333
53333
|
for (const field of fields) {
|
|
53334
53334
|
const val = obj[field];
|
|
@@ -53386,15 +53386,6 @@ async function decryptPersonCiphers(input2, key) {
|
|
|
53386
53386
|
merge3(data);
|
|
53387
53387
|
return { data, undecrypted };
|
|
53388
53388
|
}
|
|
53389
|
-
var VERIFIER_PLAINTEXT = "paisa-verify-v1";
|
|
53390
|
-
async function checkVerifier(key, encryptedVerifier) {
|
|
53391
|
-
try {
|
|
53392
|
-
const decrypted = await decryptField(encryptedVerifier, key);
|
|
53393
|
-
return decrypted === VERIFIER_PLAINTEXT;
|
|
53394
|
-
} catch {
|
|
53395
|
-
return false;
|
|
53396
|
-
}
|
|
53397
|
-
}
|
|
53398
53389
|
|
|
53399
53390
|
// src/lib/crypto-init.ts
|
|
53400
53391
|
async function initCrypto(client, pin, userId) {
|
|
@@ -53439,10 +53430,153 @@ async function initCrypto(client, pin, userId) {
|
|
|
53439
53430
|
return { key };
|
|
53440
53431
|
}
|
|
53441
53432
|
|
|
53433
|
+
// src/server.ts
|
|
53434
|
+
import { randomBytes } from "node:crypto";
|
|
53435
|
+
|
|
53442
53436
|
// ../../node_modules/.bun/@modelcontextprotocol+server@2.1.0/node_modules/@modelcontextprotocol/server/dist/index.mjs
|
|
53443
53437
|
var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
|
|
53438
|
+
var PREFIX = "v1.";
|
|
53439
|
+
function bytesToBase64Url(bytes) {
|
|
53440
|
+
let bin = "";
|
|
53441
|
+
for (const b of bytes)
|
|
53442
|
+
bin += String.fromCodePoint(b);
|
|
53443
|
+
return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
53444
|
+
}
|
|
53445
|
+
function constantTimeTagEqual(a, b) {
|
|
53446
|
+
if (a.length !== b.length)
|
|
53447
|
+
return false;
|
|
53448
|
+
let r = 0;
|
|
53449
|
+
for (let i = 0;i < a.length; i++)
|
|
53450
|
+
r |= a.codePointAt(i) ^ b.codePointAt(i);
|
|
53451
|
+
return r === 0;
|
|
53452
|
+
}
|
|
53453
|
+
function base64UrlToBytes(s) {
|
|
53454
|
+
const b64 = s.replaceAll("-", "+").replaceAll("_", "/");
|
|
53455
|
+
const bin = atob(b64);
|
|
53456
|
+
const bytes = new Uint8Array(bin.length);
|
|
53457
|
+
for (let i = 0;i < bin.length; i++)
|
|
53458
|
+
bytes[i] = bin.codePointAt(i);
|
|
53459
|
+
return bytes;
|
|
53460
|
+
}
|
|
53461
|
+
function createRequestStateCodec(options) {
|
|
53462
|
+
const subtle = globalThis.crypto?.subtle;
|
|
53463
|
+
if (subtle === undefined)
|
|
53464
|
+
throw new TypeError("createRequestStateCodec requires the Web Crypto API (globalThis.crypto.subtle); see https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting for the Node.js polyfill instructions");
|
|
53465
|
+
const keyBytes = typeof options.key === "string" ? new TextEncoder().encode(options.key) : Uint8Array.from(options.key);
|
|
53466
|
+
if (keyBytes.byteLength < 32)
|
|
53467
|
+
throw new RangeError(`createRequestStateCodec: key must be at least 32 bytes (got ${keyBytes.byteLength})`);
|
|
53468
|
+
const ttlSeconds = options.ttlSeconds ?? 600;
|
|
53469
|
+
if (!Number.isFinite(ttlSeconds))
|
|
53470
|
+
throw new RangeError("createRequestStateCodec: ttlSeconds must be a finite number");
|
|
53471
|
+
const bind2 = options.bind;
|
|
53472
|
+
let cryptoKey;
|
|
53473
|
+
const importedKey = () => cryptoKey ??= subtle.importKey("raw", keyBytes, {
|
|
53474
|
+
name: "HMAC",
|
|
53475
|
+
hash: "SHA-256"
|
|
53476
|
+
}, false, ["sign", "verify"]);
|
|
53477
|
+
const utf8 = new TextEncoder;
|
|
53478
|
+
const BIND_LABEL = "mcp.requestState.bind:";
|
|
53479
|
+
const bindTag = async (value) => {
|
|
53480
|
+
return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(BIND_LABEL + value))).slice(0, 16));
|
|
53481
|
+
};
|
|
53482
|
+
return {
|
|
53483
|
+
async mint(payload, ctx) {
|
|
53484
|
+
const envelope = {
|
|
53485
|
+
p: payload,
|
|
53486
|
+
exp: Math.floor(Date.now() / 1000) + ttlSeconds
|
|
53487
|
+
};
|
|
53488
|
+
if (bind2 !== undefined) {
|
|
53489
|
+
if (ctx === undefined)
|
|
53490
|
+
throw new TypeError("createRequestStateCodec: mint() requires ctx when a bind callback is configured");
|
|
53491
|
+
envelope.b = await bindTag(bind2(ctx));
|
|
53492
|
+
}
|
|
53493
|
+
const body = bytesToBase64Url(utf8.encode(JSON.stringify(envelope)));
|
|
53494
|
+
return `${PREFIX}${body}.${bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(PREFIX + body))))}`;
|
|
53495
|
+
},
|
|
53496
|
+
async verify(state, ctx) {
|
|
53497
|
+
const dot = state.lastIndexOf(".");
|
|
53498
|
+
if (!state.startsWith(PREFIX) || dot <= 3)
|
|
53499
|
+
throw new Error("malformed");
|
|
53500
|
+
const body = state.slice(3, dot);
|
|
53501
|
+
let macBytes;
|
|
53502
|
+
try {
|
|
53503
|
+
macBytes = base64UrlToBytes(state.slice(dot + 1));
|
|
53504
|
+
} catch {
|
|
53505
|
+
throw new Error("malformed");
|
|
53506
|
+
}
|
|
53507
|
+
if (!await subtle.verify("HMAC", await importedKey(), macBytes, utf8.encode(PREFIX + body)))
|
|
53508
|
+
throw new Error("mac");
|
|
53509
|
+
let envelope;
|
|
53510
|
+
try {
|
|
53511
|
+
envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlToBytes(body)));
|
|
53512
|
+
} catch {
|
|
53513
|
+
throw new Error("malformed");
|
|
53514
|
+
}
|
|
53515
|
+
if (typeof envelope.exp !== "number" || envelope.exp < Math.floor(Date.now() / 1000))
|
|
53516
|
+
throw new Error("expired");
|
|
53517
|
+
if (bind2 !== undefined) {
|
|
53518
|
+
const expected = await bindTag(bind2(ctx));
|
|
53519
|
+
if (envelope.b === undefined || !constantTimeTagEqual(envelope.b, expected))
|
|
53520
|
+
throw new Error("bind");
|
|
53521
|
+
} else if (envelope.b !== undefined)
|
|
53522
|
+
throw new Error("bind");
|
|
53523
|
+
return envelope.p;
|
|
53524
|
+
}
|
|
53525
|
+
};
|
|
53526
|
+
}
|
|
53444
53527
|
|
|
53445
|
-
// ../../node_modules/.bun/@nimit9+signet-server@0.2.
|
|
53528
|
+
// ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/interaction.js
|
|
53529
|
+
var CONFIRM_KEY = "signet_confirm";
|
|
53530
|
+
function canonicalize(value) {
|
|
53531
|
+
if (Array.isArray(value))
|
|
53532
|
+
return value.map(canonicalize);
|
|
53533
|
+
if (value !== null && typeof value === "object") {
|
|
53534
|
+
const obj = value;
|
|
53535
|
+
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, canonicalize(obj[k])]));
|
|
53536
|
+
}
|
|
53537
|
+
return value;
|
|
53538
|
+
}
|
|
53539
|
+
async function fingerprint(args) {
|
|
53540
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(canonicalize(args ?? null))));
|
|
53541
|
+
return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
53542
|
+
}
|
|
53543
|
+
async function confirm(ctx, { message, detail, args, key }) {
|
|
53544
|
+
const id = key ?? `${CONFIRM_KEY}:${ctx.tool}`;
|
|
53545
|
+
const view = inputResponse(ctx.mcp.mcpReq.inputResponses, id);
|
|
53546
|
+
if (view.kind === "missing") {
|
|
53547
|
+
if (!ctx.confirmSupport.getClientCapabilities()?.elicitation)
|
|
53548
|
+
return "unsupported";
|
|
53549
|
+
const payload = { tool: ctx.tool, id, fp: await fingerprint(args) };
|
|
53550
|
+
const requestState = await ctx.confirmSupport.mint?.(payload);
|
|
53551
|
+
return inputRequired({
|
|
53552
|
+
inputRequests: {
|
|
53553
|
+
[id]: inputRequired.elicit({
|
|
53554
|
+
message: detail ? `${message}
|
|
53555
|
+
|
|
53556
|
+
${detail}` : message,
|
|
53557
|
+
requestedSchema: {
|
|
53558
|
+
type: "object",
|
|
53559
|
+
properties: { confirm: { type: "boolean", title: "Confirm" } },
|
|
53560
|
+
required: ["confirm"]
|
|
53561
|
+
}
|
|
53562
|
+
})
|
|
53563
|
+
},
|
|
53564
|
+
...requestState ? { requestState } : {}
|
|
53565
|
+
});
|
|
53566
|
+
}
|
|
53567
|
+
if (view.kind !== "elicit" || view.action !== "accept" || view.content?.confirm !== true) {
|
|
53568
|
+
return "declined";
|
|
53569
|
+
}
|
|
53570
|
+
if (ctx.confirmSupport.mint) {
|
|
53571
|
+
const state = ctx.mcp.mcpReq.requestState();
|
|
53572
|
+
if (!state || state.tool !== ctx.tool || state.id !== id || state.fp !== await fingerprint(args))
|
|
53573
|
+
return "declined";
|
|
53574
|
+
}
|
|
53575
|
+
return "confirmed";
|
|
53576
|
+
}
|
|
53577
|
+
function isConfirmPending(value) {
|
|
53578
|
+
return isInputRequiredResult(value);
|
|
53579
|
+
}
|
|
53446
53580
|
async function progress(ctx, done, total, message) {
|
|
53447
53581
|
const token = ctx.mcp.mcpReq._meta?.progressToken;
|
|
53448
53582
|
if (token === undefined)
|
|
@@ -53459,7 +53593,7 @@ async function progress(ctx, done, total, message) {
|
|
|
53459
53593
|
});
|
|
53460
53594
|
} catch {}
|
|
53461
53595
|
}
|
|
53462
|
-
// ../../node_modules/.bun/@nimit9+signet-server@0.2.
|
|
53596
|
+
// ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
|
|
53463
53597
|
var STATUS_CODES = {
|
|
53464
53598
|
400: "BAD_REQUEST",
|
|
53465
53599
|
401: "UNAUTHORIZED",
|
|
@@ -53473,7 +53607,7 @@ var STATUS_CODES = {
|
|
|
53473
53607
|
429: "RATE_LIMITED"
|
|
53474
53608
|
};
|
|
53475
53609
|
|
|
53476
|
-
// ../../node_modules/.bun/@nimit9+signet-server@0.2.
|
|
53610
|
+
// ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/errors.js
|
|
53477
53611
|
function isZodError(err) {
|
|
53478
53612
|
return err instanceof Error && err.name === "ZodError" && Array.isArray(err.issues);
|
|
53479
53613
|
}
|
|
@@ -53537,7 +53671,7 @@ function report(opts, err, meta3, c) {
|
|
|
53537
53671
|
} catch {}
|
|
53538
53672
|
}
|
|
53539
53673
|
|
|
53540
|
-
// ../../node_modules/.bun/@nimit9+signet-server@0.2.
|
|
53674
|
+
// ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/primitives.js
|
|
53541
53675
|
var RAW = Symbol.for("signet.mcp.rawResult");
|
|
53542
53676
|
function protocolError(err, deps, meta3) {
|
|
53543
53677
|
if (ProtocolError.isInstance(err))
|
|
@@ -53570,8 +53704,14 @@ function checkScopes(scopes, token) {
|
|
|
53570
53704
|
});
|
|
53571
53705
|
}
|
|
53572
53706
|
function baseContext(mcp, deps) {
|
|
53573
|
-
const { c, token } = deps.context;
|
|
53574
|
-
return {
|
|
53707
|
+
const { c, token, confirmSupport } = deps.context;
|
|
53708
|
+
return {
|
|
53709
|
+
signal: mcp.mcpReq.signal,
|
|
53710
|
+
mcp,
|
|
53711
|
+
confirmSupport,
|
|
53712
|
+
...c ? { c } : {},
|
|
53713
|
+
...token ? { token } : {}
|
|
53714
|
+
};
|
|
53575
53715
|
}
|
|
53576
53716
|
function toContents(uri, value, mimeType) {
|
|
53577
53717
|
if (value !== null && typeof value === "object" && RAW in value)
|
|
@@ -53681,7 +53821,7 @@ function registerPrompts(server, prompts, deps) {
|
|
|
53681
53821
|
}
|
|
53682
53822
|
}
|
|
53683
53823
|
|
|
53684
|
-
// ../../node_modules/.bun/@nimit9+signet-server@0.2.
|
|
53824
|
+
// ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
|
|
53685
53825
|
var RAW2 = Symbol.for("signet.mcp.rawResult");
|
|
53686
53826
|
function toolResult(result) {
|
|
53687
53827
|
return Object.defineProperty({ ...result }, RAW2, { value: true, enumerable: false });
|
|
@@ -53709,9 +53849,14 @@ function createMcpServer(options) {
|
|
|
53709
53849
|
...options.resources?.length ? { resources: { listChanged: false } } : {},
|
|
53710
53850
|
...options.prompts?.length ? { prompts: { listChanged: false } } : {}
|
|
53711
53851
|
};
|
|
53852
|
+
const confirmCodec = options.confirm ? createRequestStateCodec({
|
|
53853
|
+
key: options.confirm.key,
|
|
53854
|
+
...options.confirm.ttlSeconds !== undefined ? { ttlSeconds: options.confirm.ttlSeconds } : {}
|
|
53855
|
+
}) : undefined;
|
|
53712
53856
|
const server = new McpServer({ name: options.name, version: options.version }, {
|
|
53713
53857
|
...unlisted.size ? {} : { capabilities: toolsCapability },
|
|
53714
|
-
...options.instructions ? { instructions: options.instructions } : {}
|
|
53858
|
+
...options.instructions ? { instructions: options.instructions } : {},
|
|
53859
|
+
...confirmCodec ? { requestState: { verify: confirmCodec.verify } } : {}
|
|
53715
53860
|
});
|
|
53716
53861
|
let restore;
|
|
53717
53862
|
if (unlisted.size) {
|
|
@@ -53726,6 +53871,10 @@ function createMcpServer(options) {
|
|
|
53726
53871
|
tool: tool.name,
|
|
53727
53872
|
signal: mcp.mcpReq.signal,
|
|
53728
53873
|
mcp,
|
|
53874
|
+
confirmSupport: {
|
|
53875
|
+
getClientCapabilities: () => server.server.getClientCapabilities(),
|
|
53876
|
+
...confirmCodec ? { mint: (payload) => confirmCodec.mint(payload, mcp) } : {}
|
|
53877
|
+
},
|
|
53729
53878
|
...c ? { c } : {},
|
|
53730
53879
|
...token ? { token } : {}
|
|
53731
53880
|
};
|
|
@@ -53764,7 +53913,14 @@ function createMcpServer(options) {
|
|
|
53764
53913
|
restore?.();
|
|
53765
53914
|
restore = undefined;
|
|
53766
53915
|
}
|
|
53767
|
-
const
|
|
53916
|
+
const confirmSupport = {
|
|
53917
|
+
getClientCapabilities: () => server.server.getClientCapabilities(),
|
|
53918
|
+
...confirmCodec ? { mint: (payload) => confirmCodec.mint(payload) } : {}
|
|
53919
|
+
};
|
|
53920
|
+
const deps = {
|
|
53921
|
+
options,
|
|
53922
|
+
context: { confirmSupport, ...c ? { c } : {}, ...token ? { token } : {} }
|
|
53923
|
+
};
|
|
53768
53924
|
if (options.resources?.length)
|
|
53769
53925
|
registerResources(server, options.resources, deps);
|
|
53770
53926
|
if (options.prompts?.length)
|
|
@@ -53867,84 +54023,34 @@ function applyAnnotations(tools) {
|
|
|
53867
54023
|
}
|
|
53868
54024
|
|
|
53869
54025
|
// src/lib/confirm.ts
|
|
53870
|
-
|
|
53871
|
-
var
|
|
53872
|
-
|
|
53873
|
-
|
|
53874
|
-
|
|
53875
|
-
|
|
53876
|
-
}
|
|
53877
|
-
|
|
53878
|
-
const canonical = (v) => Array.isArray(v) ? v.map(canonical) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([k, x]) => [k, canonical(x)])) : v;
|
|
53879
|
-
return createHash("sha256").update(JSON.stringify(canonical(plan))).digest("hex");
|
|
53880
|
-
}
|
|
53881
|
-
var STATE_KEY = randomBytes(32);
|
|
53882
|
-
function sign(nonce, fingerprint) {
|
|
53883
|
-
return createHmac("sha256", STATE_KEY).update(`${nonce}.${fingerprint}`).digest("hex");
|
|
53884
|
-
}
|
|
53885
|
-
function mintState(fingerprint) {
|
|
53886
|
-
const nonce = randomBytes(16).toString("hex");
|
|
53887
|
-
return `${nonce}.${sign(nonce, fingerprint)}`;
|
|
53888
|
-
}
|
|
53889
|
-
function verifyState(state, fingerprint) {
|
|
53890
|
-
if (typeof state !== "string")
|
|
53891
|
-
return false;
|
|
53892
|
-
const [nonce, mac3, ...rest] = state.split(".");
|
|
53893
|
-
if (!nonce || !mac3 || rest.length)
|
|
53894
|
-
return false;
|
|
53895
|
-
const expected = Buffer.from(sign(nonce, fingerprint), "hex");
|
|
53896
|
-
const given = Buffer.from(mac3, "hex");
|
|
53897
|
-
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
54026
|
+
var CANCELLED = { cancelled: true, message: "Cancelled by the user. Nothing was changed." };
|
|
54027
|
+
var PLAN_CHANGED = {
|
|
54028
|
+
planChanged: true,
|
|
54029
|
+
message: "The request changed after you confirmed, so nothing was changed. Run it again to see the new plan."
|
|
54030
|
+
};
|
|
54031
|
+
function acceptedDifferentPlan(ctx) {
|
|
54032
|
+
const view = inputResponse(ctx.mcp.mcpReq.inputResponses, `signet_confirm:${ctx.tool}`);
|
|
54033
|
+
return view.kind === "elicit" && view.action === "accept" && view.content?.confirm === true;
|
|
53898
54034
|
}
|
|
53899
|
-
function
|
|
53900
|
-
|
|
53901
|
-
|
|
53902
|
-
|
|
53903
|
-
|
|
53904
|
-
|
|
53905
|
-
|
|
53906
|
-
|
|
53907
|
-
|
|
53908
|
-
if (answered && verifyState(echoed, fingerprint)) {
|
|
53909
|
-
const content = acceptedContent(responses, ANSWER_KEY);
|
|
53910
|
-
if (content?.confirm === true)
|
|
53911
|
-
return { kind: "confirmed" };
|
|
53912
|
-
return {
|
|
53913
|
-
kind: "declined",
|
|
53914
|
-
result: { cancelled: true, message: "Cancelled by the user. Nothing was changed." }
|
|
53915
|
-
};
|
|
53916
|
-
}
|
|
53917
|
-
return {
|
|
53918
|
-
kind: "ask",
|
|
53919
|
-
result: inputRequired({
|
|
53920
|
-
inputRequests: {
|
|
53921
|
-
[ANSWER_KEY]: inputRequired.elicit({
|
|
53922
|
-
message,
|
|
53923
|
-
requestedSchema: {
|
|
53924
|
-
type: "object",
|
|
53925
|
-
properties: {
|
|
53926
|
-
confirm: { type: "boolean", title: "Confirm", description: "This can't be undone." }
|
|
53927
|
-
},
|
|
53928
|
-
required: ["confirm"]
|
|
53929
|
-
}
|
|
53930
|
-
})
|
|
53931
|
-
},
|
|
53932
|
-
requestState: mintState(fingerprint)
|
|
53933
|
-
})
|
|
53934
|
-
};
|
|
54035
|
+
async function askThenRun(ctx, message, plan, run, fallback = run) {
|
|
54036
|
+
const answer = await confirm(ctx, { message, args: plan });
|
|
54037
|
+
if (isConfirmPending(answer))
|
|
54038
|
+
return answer;
|
|
54039
|
+
if (answer === "confirmed")
|
|
54040
|
+
return run();
|
|
54041
|
+
if (answer === "unsupported")
|
|
54042
|
+
return fallback();
|
|
54043
|
+
return acceptedDifferentPlan(ctx) ? PLAN_CHANGED : CANCELLED;
|
|
53935
54044
|
}
|
|
53936
|
-
function withConfirmation(tool, describe3
|
|
54045
|
+
function withConfirmation(tool, describe3) {
|
|
53937
54046
|
const run = tool.handler;
|
|
53938
54047
|
return {
|
|
53939
54048
|
...tool,
|
|
53940
|
-
handler: (args, ctx) => {
|
|
54049
|
+
handler: async (args, ctx) => {
|
|
53941
54050
|
const message = describe3(args);
|
|
53942
54051
|
if (message === null || !ctx?.mcp)
|
|
53943
54052
|
return run(args, ctx);
|
|
53944
|
-
|
|
53945
|
-
if (outcome.kind === "confirmed" || outcome.kind === "unsupported")
|
|
53946
|
-
return run(args, ctx);
|
|
53947
|
-
return outcome.result;
|
|
54053
|
+
return askThenRun(ctx, message, { tool: tool.name, args }, () => run(args, ctx));
|
|
53948
54054
|
}
|
|
53949
54055
|
};
|
|
53950
54056
|
}
|
|
@@ -54021,11 +54127,7 @@ var BULK_CONFIRMED_TOOLS = {
|
|
|
54021
54127
|
}),
|
|
54022
54128
|
bulk_link_entity: async (args) => ({ n: Array.isArray(args.links) ? args.links.length : 0 })
|
|
54023
54129
|
};
|
|
54024
|
-
|
|
54025
|
-
planChanged: true,
|
|
54026
|
-
message: "The transactions changed after they were counted, so nothing was written. Run it again to see the new count."
|
|
54027
|
-
};
|
|
54028
|
-
function withBulkConfirmation(tool, count, initializeCapabilities) {
|
|
54130
|
+
function withBulkConfirmation(tool, count) {
|
|
54029
54131
|
const run = tool.handler;
|
|
54030
54132
|
const runPlanned = async (args, ctx, hash2) => {
|
|
54031
54133
|
const result = await run(hash2 === undefined ? args : { ...args, expectedPlanHash: hash2 }, ctx);
|
|
@@ -54034,18 +54136,13 @@ function withBulkConfirmation(tool, count, initializeCapabilities) {
|
|
|
54034
54136
|
return {
|
|
54035
54137
|
...tool,
|
|
54036
54138
|
handler: async (args, ctx) => {
|
|
54037
|
-
|
|
54139
|
+
const canAsk = Boolean(ctx?.confirmSupport?.getClientCapabilities()?.elicitation);
|
|
54140
|
+
if (args.dryRun === true || !ctx?.mcp || !canAsk)
|
|
54038
54141
|
return run(args, ctx);
|
|
54039
54142
|
const { n, planHash: hash2 } = await count(args, async (a) => run(a, ctx));
|
|
54040
54143
|
if (n <= BULK_CONFIRM_THRESHOLD)
|
|
54041
54144
|
return runPlanned(args, ctx, hash2);
|
|
54042
|
-
|
|
54043
|
-
const outcome = confirmPlan(ctx.mcp, message, { tool: tool.name, args, n, planHash: hash2 }, initializeCapabilities);
|
|
54044
|
-
if (outcome.kind === "confirmed")
|
|
54045
|
-
return runPlanned(args, ctx, hash2);
|
|
54046
|
-
if (outcome.kind === "unsupported")
|
|
54047
|
-
return run(args, ctx);
|
|
54048
|
-
return outcome.result;
|
|
54145
|
+
return askThenRun(ctx, `Update ${plural2(n, "transaction")} in one go?`, { tool: tool.name, args, n, planHash: hash2 }, () => runPlanned(args, ctx, hash2), () => run(args, ctx));
|
|
54049
54146
|
}
|
|
54050
54147
|
};
|
|
54051
54148
|
}
|
|
@@ -54478,7 +54575,7 @@ async function decryptAnalytics(data, key) {
|
|
|
54478
54575
|
], key);
|
|
54479
54576
|
return undecrypted.length ? { ...decrypted, undecrypted } : decrypted;
|
|
54480
54577
|
}
|
|
54481
|
-
function analyticsTools(client,
|
|
54578
|
+
function analyticsTools(client, crypto4) {
|
|
54482
54579
|
return [
|
|
54483
54580
|
defineTool({
|
|
54484
54581
|
name: "get_analytics",
|
|
@@ -54503,8 +54600,8 @@ function analyticsTools(client, crypto3) {
|
|
|
54503
54600
|
data = await client.get(`/api/analytics/merchants/${params.id}`, params.months ? { months: params.months } : undefined);
|
|
54504
54601
|
break;
|
|
54505
54602
|
}
|
|
54506
|
-
if (
|
|
54507
|
-
data = await decryptAnalytics(data,
|
|
54603
|
+
if (crypto4)
|
|
54604
|
+
data = await decryptAnalytics(data, crypto4.key);
|
|
54508
54605
|
return data;
|
|
54509
54606
|
}
|
|
54510
54607
|
})
|
|
@@ -54562,7 +54659,7 @@ function budgetTools(client) {
|
|
|
54562
54659
|
}
|
|
54563
54660
|
|
|
54564
54661
|
// src/tools/dashboard.ts
|
|
54565
|
-
function dashboardTools(client,
|
|
54662
|
+
function dashboardTools(client, crypto4) {
|
|
54566
54663
|
return [
|
|
54567
54664
|
defineTool({
|
|
54568
54665
|
name: "get_dashboard",
|
|
@@ -54571,8 +54668,8 @@ function dashboardTools(client, crypto3) {
|
|
|
54571
54668
|
output: dashboardOutputSchema,
|
|
54572
54669
|
handler: async (params) => {
|
|
54573
54670
|
const data = await client.get("/api/dashboard", params.months ? { months: params.months } : undefined);
|
|
54574
|
-
if (
|
|
54575
|
-
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.recentTransactions,
|
|
54671
|
+
if (crypto4 && Array.isArray(data?.recentTransactions)) {
|
|
54672
|
+
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.recentTransactions, crypto4.key);
|
|
54576
54673
|
data.recentTransactions = decrypted;
|
|
54577
54674
|
if (undecrypted.length)
|
|
54578
54675
|
return { ...data, undecrypted };
|
|
@@ -54718,7 +54815,7 @@ function goalTools(client) {
|
|
|
54718
54815
|
return [
|
|
54719
54816
|
defineTool({
|
|
54720
54817
|
name: "upsert_savings",
|
|
54721
|
-
description: "Create (omit id) or update (pass id; all fields optional) a savings goal, EMI or sinking fund. Amounts are ₹ decimal strings, dates YYYY-MM-DD. Required on create — " + "goal: goalType, name, targetAmount. emi: name, totalAmount, emiAmount (monthly), startDate, endDate (last payment), totalMonths, owner. " + "sinking_fund: name, targetAmount, targetDate (due), monthlyContribution.",
|
|
54818
|
+
description: "Create (omit id) or update (pass id; all fields optional) a savings goal, EMI or sinking fund. Amounts are ₹ decimal strings, dates YYYY-MM-DD. Required on create — " + "goal: goalType, name, targetAmount. emi: name, totalAmount, emiAmount (monthly), startDate, endDate (last payment), totalMonths, owner. " + "sinking_fund: name, targetAmount, targetDate (due), monthlyContribution. " + "status — goal: active|paused|completed|cancelled; emi: active|completed|foreclosed; sinking funds have none.",
|
|
54722
54819
|
input: exports_external.object({
|
|
54723
54820
|
type: exports_external.enum(["goal", "emi", "sinking_fund"]),
|
|
54724
54821
|
id: exports_external.string().optional(),
|
|
@@ -54739,7 +54836,8 @@ function goalTools(client) {
|
|
|
54739
54836
|
startDate: exports_external.string().optional(),
|
|
54740
54837
|
endDate: exports_external.string().optional(),
|
|
54741
54838
|
totalMonths: exports_external.number().optional(),
|
|
54742
|
-
paidMonths: exports_external.number().optional()
|
|
54839
|
+
paidMonths: exports_external.number().optional(),
|
|
54840
|
+
status: exports_external.enum(["active", "paused", "completed", "cancelled", "foreclosed"]).optional().describe("goal: active|paused|completed|cancelled; emi: active|completed|foreclosed")
|
|
54743
54841
|
}),
|
|
54744
54842
|
handler: async ({ type, ...fields }) => {
|
|
54745
54843
|
const require2 = (keys) => {
|
|
@@ -54748,6 +54846,14 @@ function goalTools(client) {
|
|
|
54748
54846
|
throw new Error(`upsert_savings type='${type}' requires: ${missing.join(", ")}`);
|
|
54749
54847
|
}
|
|
54750
54848
|
};
|
|
54849
|
+
const allowedStatus = {
|
|
54850
|
+
goal: ["active", "paused", "completed", "cancelled"],
|
|
54851
|
+
emi: ["active", "completed", "foreclosed"],
|
|
54852
|
+
sinking_fund: []
|
|
54853
|
+
};
|
|
54854
|
+
if (fields.status !== undefined && !allowedStatus[type].includes(fields.status)) {
|
|
54855
|
+
throw new Error(type === "sinking_fund" ? "upsert_savings type='sinking_fund' has no status" : `upsert_savings type='${type}' status must be one of: ${allowedStatus[type].join(", ")}`);
|
|
54856
|
+
}
|
|
54751
54857
|
if (type === "goal") {
|
|
54752
54858
|
const {
|
|
54753
54859
|
id: id2,
|
|
@@ -54760,7 +54866,8 @@ function goalTools(client) {
|
|
|
54760
54866
|
owner,
|
|
54761
54867
|
icon,
|
|
54762
54868
|
color,
|
|
54763
|
-
notes
|
|
54869
|
+
notes,
|
|
54870
|
+
status
|
|
54764
54871
|
} = fields;
|
|
54765
54872
|
const body2 = {
|
|
54766
54873
|
type: goalType,
|
|
@@ -54772,7 +54879,8 @@ function goalTools(client) {
|
|
|
54772
54879
|
owner,
|
|
54773
54880
|
icon,
|
|
54774
54881
|
color,
|
|
54775
|
-
notes
|
|
54882
|
+
notes,
|
|
54883
|
+
status
|
|
54776
54884
|
};
|
|
54777
54885
|
if (!id2)
|
|
54778
54886
|
require2(["name", "targetAmount", "goalType"]);
|
|
@@ -54790,7 +54898,8 @@ function goalTools(client) {
|
|
|
54790
54898
|
totalMonths,
|
|
54791
54899
|
paidMonths,
|
|
54792
54900
|
interestRate,
|
|
54793
|
-
outstandingPrincipal
|
|
54901
|
+
outstandingPrincipal,
|
|
54902
|
+
status
|
|
54794
54903
|
} = fields;
|
|
54795
54904
|
const { owner } = fields;
|
|
54796
54905
|
if (!id2) {
|
|
@@ -54814,7 +54923,8 @@ function goalTools(client) {
|
|
|
54814
54923
|
paidMonths,
|
|
54815
54924
|
interestRate,
|
|
54816
54925
|
outstandingPrincipal,
|
|
54817
|
-
owner
|
|
54926
|
+
owner,
|
|
54927
|
+
status
|
|
54818
54928
|
};
|
|
54819
54929
|
const data2 = id2 ? await client.patch(`/api/goals/emis/${id2}`, body2) : await client.post("/api/goals/emis", body2);
|
|
54820
54930
|
return data2;
|
|
@@ -55123,13 +55233,13 @@ async function categorySlugToId(client) {
|
|
|
55123
55233
|
}
|
|
55124
55234
|
return map2;
|
|
55125
55235
|
}
|
|
55126
|
-
function merchantVariants(client,
|
|
55236
|
+
function merchantVariants(client, crypto4) {
|
|
55127
55237
|
return {
|
|
55128
55238
|
list: variant(exports_external.object({ rawId: exports_external.string().optional().describe("merchants: find one by UPI ID or name") }), async ({ rawId }) => {
|
|
55129
55239
|
const data = await client.get("/api/merchants");
|
|
55130
55240
|
let undecrypted = [];
|
|
55131
|
-
if (
|
|
55132
|
-
const dec = await decryptMerchantFields(data.data,
|
|
55241
|
+
if (crypto4 && data.data) {
|
|
55242
|
+
const dec = await decryptMerchantFields(data.data, crypto4.key);
|
|
55133
55243
|
data.data = dec.data;
|
|
55134
55244
|
undecrypted = dec.undecrypted;
|
|
55135
55245
|
}
|
|
@@ -55187,7 +55297,7 @@ function merchantTools(client) {
|
|
|
55187
55297
|
}
|
|
55188
55298
|
|
|
55189
55299
|
// src/tools/persons.ts
|
|
55190
|
-
function personVariants(client,
|
|
55300
|
+
function personVariants(client, crypto4) {
|
|
55191
55301
|
return {
|
|
55192
55302
|
upsert: variant(exports_external.object({
|
|
55193
55303
|
id: exports_external.string().optional().describe("Person UUID — pass to update, omit to create"),
|
|
@@ -55197,13 +55307,13 @@ function personVariants(client, crypto3) {
|
|
|
55197
55307
|
relationship: exports_external.string().optional(),
|
|
55198
55308
|
notes: exports_external.string().optional()
|
|
55199
55309
|
}), async ({ id, ...updates }) => {
|
|
55200
|
-
if (
|
|
55310
|
+
if (crypto4) {
|
|
55201
55311
|
if (id) {
|
|
55202
55312
|
const existing = await client.get(`/api/persons/${id}`);
|
|
55203
55313
|
const personData = existing.data;
|
|
55204
55314
|
let current;
|
|
55205
55315
|
if (personData.cipher) {
|
|
55206
|
-
current = await decryptPersonCipher(personData.cipher,
|
|
55316
|
+
current = await decryptPersonCipher(personData.cipher, crypto4.key);
|
|
55207
55317
|
} else {
|
|
55208
55318
|
current = {
|
|
55209
55319
|
name: personData.name,
|
|
@@ -55220,7 +55330,7 @@ function personVariants(client, crypto3) {
|
|
|
55220
55330
|
relationship: updates.relationship ?? current.relationship ?? null,
|
|
55221
55331
|
notes: updates.notes ?? current.notes ?? null
|
|
55222
55332
|
};
|
|
55223
|
-
const payload2 = await encryptPersonCipher(merged,
|
|
55333
|
+
const payload2 = await encryptPersonCipher(merged, crypto4.key);
|
|
55224
55334
|
const data3 = await client.patch(`/api/persons/${id}`, payload2);
|
|
55225
55335
|
return data3;
|
|
55226
55336
|
}
|
|
@@ -55233,7 +55343,7 @@ function personVariants(client, crypto3) {
|
|
|
55233
55343
|
relationship: updates.relationship ?? null,
|
|
55234
55344
|
notes: updates.notes ?? null
|
|
55235
55345
|
};
|
|
55236
|
-
const payload = await encryptPersonCipher(fields,
|
|
55346
|
+
const payload = await encryptPersonCipher(fields, crypto4.key);
|
|
55237
55347
|
const data2 = await client.post("/api/persons", payload);
|
|
55238
55348
|
return data2;
|
|
55239
55349
|
}
|
|
@@ -55245,8 +55355,8 @@ function personVariants(client, crypto3) {
|
|
|
55245
55355
|
}), async ({ id }) => {
|
|
55246
55356
|
if (id) {
|
|
55247
55357
|
const data2 = await client.get(`/api/persons/${id}`);
|
|
55248
|
-
if (
|
|
55249
|
-
const { data: decrypted, undecrypted } = await decryptPersonCiphers(data2.data,
|
|
55358
|
+
if (crypto4 && data2.data.cipher) {
|
|
55359
|
+
const { data: decrypted, undecrypted } = await decryptPersonCiphers(data2.data, crypto4.key);
|
|
55250
55360
|
data2.data = decrypted;
|
|
55251
55361
|
if (undecrypted.length)
|
|
55252
55362
|
return { ...data2, undecrypted };
|
|
@@ -55254,8 +55364,8 @@ function personVariants(client, crypto3) {
|
|
|
55254
55364
|
return data2;
|
|
55255
55365
|
}
|
|
55256
55366
|
const data = await client.get("/api/persons");
|
|
55257
|
-
if (
|
|
55258
|
-
const { data: decrypted, undecrypted } = await decryptPersonCiphers(data.data,
|
|
55367
|
+
if (crypto4 && data.data) {
|
|
55368
|
+
const { data: decrypted, undecrypted } = await decryptPersonCiphers(data.data, crypto4.key);
|
|
55259
55369
|
data.data = decrypted;
|
|
55260
55370
|
if (undecrypted.length)
|
|
55261
55371
|
return { ...data, undecrypted };
|
|
@@ -55393,11 +55503,11 @@ function reportTools(client) {
|
|
|
55393
55503
|
}
|
|
55394
55504
|
|
|
55395
55505
|
// src/tools/entities.ts
|
|
55396
|
-
function entityTools(client,
|
|
55506
|
+
function entityTools(client, crypto4) {
|
|
55397
55507
|
const category = categoryVariants(client);
|
|
55398
55508
|
const account = accountVariants(client);
|
|
55399
|
-
const person = personVariants(client,
|
|
55400
|
-
const merchant = merchantVariants(client,
|
|
55509
|
+
const person = personVariants(client, crypto4);
|
|
55510
|
+
const merchant = merchantVariants(client, crypto4);
|
|
55401
55511
|
const debt = debtVariants(client);
|
|
55402
55512
|
const household = householdVariants(client);
|
|
55403
55513
|
const goal = goalVariants(client);
|
|
@@ -59025,9 +59135,9 @@ function parseAmount4(raw) {
|
|
|
59025
59135
|
return Number.isNaN(num) ? 0 : Math.abs(num);
|
|
59026
59136
|
}
|
|
59027
59137
|
// src/lib/plan-hash.ts
|
|
59028
|
-
import { createHash
|
|
59138
|
+
import { createHash } from "node:crypto";
|
|
59029
59139
|
function planHash(ids) {
|
|
59030
|
-
return
|
|
59140
|
+
return createHash("sha256").update([...ids].sort().join(",")).digest("hex");
|
|
59031
59141
|
}
|
|
59032
59142
|
|
|
59033
59143
|
// src/lib/txn-rows.ts
|
|
@@ -59046,10 +59156,10 @@ function toTxnRow(r2) {
|
|
|
59046
59156
|
isDuplicate: !!r2.isDuplicate
|
|
59047
59157
|
};
|
|
59048
59158
|
}
|
|
59049
|
-
async function fetchAllRows(client,
|
|
59050
|
-
return (await fetchAllRowsPaged(client,
|
|
59159
|
+
async function fetchAllRows(client, crypto4, params = {}, maxPages = 100) {
|
|
59160
|
+
return (await fetchAllRowsPaged(client, crypto4, params, maxPages)).rows;
|
|
59051
59161
|
}
|
|
59052
|
-
async function fetchAllRowsPaged(client,
|
|
59162
|
+
async function fetchAllRowsPaged(client, crypto4, params = {}, maxPages = 100) {
|
|
59053
59163
|
const out = [];
|
|
59054
59164
|
let cursor;
|
|
59055
59165
|
let truncated = false;
|
|
@@ -59059,7 +59169,7 @@ async function fetchAllRowsPaged(client, crypto3, params = {}, maxPages = 100) {
|
|
|
59059
59169
|
q2.cursor = cursor;
|
|
59060
59170
|
const res = await client.get("/api/transactions", q2);
|
|
59061
59171
|
const batch0 = res.data ?? [];
|
|
59062
|
-
const batch =
|
|
59172
|
+
const batch = crypto4 ? (await decryptTransactionFields(batch0, crypto4.key)).data : batch0;
|
|
59063
59173
|
out.push(...batch.map(toTxnRow));
|
|
59064
59174
|
if (!res.hasMore || !res.nextCursor || batch.length === 0 || res.nextCursor === cursor)
|
|
59065
59175
|
break;
|
|
@@ -59105,18 +59215,21 @@ function toLeanRow(r2, personNames) {
|
|
|
59105
59215
|
function normalizePersonName(name) {
|
|
59106
59216
|
return name.toLowerCase().trim().replace(/\s+/g, " ");
|
|
59107
59217
|
}
|
|
59108
|
-
|
|
59218
|
+
function normalizeUpiHandle(handle) {
|
|
59219
|
+
return handle.toLowerCase().trim().split("@")[0];
|
|
59220
|
+
}
|
|
59221
|
+
async function loadDecryptedPersons(client, crypto4) {
|
|
59109
59222
|
const res = await client.get("/api/persons");
|
|
59110
59223
|
const persons = [];
|
|
59111
59224
|
for (const p2 of res.data ?? []) {
|
|
59112
59225
|
let name = p2.name ?? "";
|
|
59113
|
-
if (
|
|
59226
|
+
if (crypto4 && p2.cipher) {
|
|
59114
59227
|
try {
|
|
59115
|
-
name = (await decryptPersonCipher(p2.cipher,
|
|
59228
|
+
name = (await decryptPersonCipher(p2.cipher, crypto4.key)).name;
|
|
59116
59229
|
} catch {}
|
|
59117
59230
|
}
|
|
59118
59231
|
if (name)
|
|
59119
|
-
persons.push({ id: p2.id, name });
|
|
59232
|
+
persons.push({ id: p2.id, name, upiHandles: p2.upiHandles ?? [] });
|
|
59120
59233
|
}
|
|
59121
59234
|
return persons;
|
|
59122
59235
|
}
|
|
@@ -59153,13 +59266,18 @@ function isPersonLikeNarration(description) {
|
|
|
59153
59266
|
const { name, paymentMethod } = parseNarration(description);
|
|
59154
59267
|
return classifyEntity({ paymentMethod, extractedName: name }) !== "merchant";
|
|
59155
59268
|
}
|
|
59156
|
-
async function backfillPersons(client,
|
|
59157
|
-
const existing = await loadDecryptedPersons(client,
|
|
59269
|
+
async function backfillPersons(client, crypto4, opts = {}) {
|
|
59270
|
+
const existing = await loadDecryptedPersons(client, crypto4);
|
|
59158
59271
|
const personIdByNorm = new Map;
|
|
59159
|
-
|
|
59272
|
+
const personIdByHandle = new Map;
|
|
59273
|
+
for (const p2 of existing) {
|
|
59160
59274
|
personIdByNorm.set(normalizePersonName(p2.name), p2.id);
|
|
59161
|
-
|
|
59275
|
+
for (const h2 of p2.upiHandles)
|
|
59276
|
+
personIdByHandle.set(normalizeUpiHandle(h2), p2.id);
|
|
59277
|
+
}
|
|
59278
|
+
const { rows, truncated } = await fetchAllRowsPaged(client, crypto4, { unresolved: "true" });
|
|
59162
59279
|
const candidates = rows.filter((r2) => !r2.merchantId && !r2.personId && !r2.isTransfer && !r2.isIgnored);
|
|
59280
|
+
const directLinks = [];
|
|
59163
59281
|
const groups = new Map;
|
|
59164
59282
|
for (const t2 of candidates) {
|
|
59165
59283
|
const { name, upiId, paymentMethod } = parseNarration(t2.description);
|
|
@@ -59167,6 +59285,11 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
59167
59285
|
continue;
|
|
59168
59286
|
if (classifyEntity({ paymentMethod, extractedName: name }) !== "person")
|
|
59169
59287
|
continue;
|
|
59288
|
+
const handlePersonId = upiId ? personIdByHandle.get(normalizeUpiHandle(upiId)) : undefined;
|
|
59289
|
+
if (handlePersonId) {
|
|
59290
|
+
directLinks.push({ transactionId: t2.id, personId: handlePersonId, upiHandle: upiId });
|
|
59291
|
+
continue;
|
|
59292
|
+
}
|
|
59170
59293
|
const norm = normalizePersonName(name);
|
|
59171
59294
|
if (!norm)
|
|
59172
59295
|
continue;
|
|
@@ -59182,7 +59305,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
59182
59305
|
}
|
|
59183
59306
|
const plan = [];
|
|
59184
59307
|
const newGroups = [];
|
|
59185
|
-
|
|
59308
|
+
const reusedPersonIds = new Set;
|
|
59186
59309
|
for (const [norm, g2] of groups) {
|
|
59187
59310
|
const upiHandles = [...g2.upiHandles];
|
|
59188
59311
|
const isReuse = personIdByNorm.has(norm);
|
|
@@ -59194,11 +59317,34 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
59194
59317
|
action: isReuse ? "reuse" : "create"
|
|
59195
59318
|
});
|
|
59196
59319
|
if (isReuse)
|
|
59197
|
-
|
|
59320
|
+
reusedPersonIds.add(personIdByNorm.get(norm));
|
|
59198
59321
|
else
|
|
59199
59322
|
newGroups.push({ norm, displayName: g2.displayName, upiHandles });
|
|
59200
59323
|
}
|
|
59201
|
-
const
|
|
59324
|
+
const directByPerson = new Map;
|
|
59325
|
+
for (const d2 of directLinks) {
|
|
59326
|
+
const e2 = directByPerson.get(d2.personId) ?? { txnIds: [], handles: new Set };
|
|
59327
|
+
e2.txnIds.push(d2.transactionId);
|
|
59328
|
+
if (d2.upiHandle)
|
|
59329
|
+
e2.handles.add(d2.upiHandle);
|
|
59330
|
+
directByPerson.set(d2.personId, e2);
|
|
59331
|
+
}
|
|
59332
|
+
for (const [personId, e2] of directByPerson) {
|
|
59333
|
+
const person = existing.find((p2) => p2.id === personId);
|
|
59334
|
+
plan.push({
|
|
59335
|
+
name: person?.name ?? personId,
|
|
59336
|
+
normalized: person ? normalizePersonName(person.name) : personId,
|
|
59337
|
+
txnCount: e2.txnIds.length,
|
|
59338
|
+
upiHandles: [...e2.handles],
|
|
59339
|
+
action: "reuse"
|
|
59340
|
+
});
|
|
59341
|
+
reusedPersonIds.add(personId);
|
|
59342
|
+
}
|
|
59343
|
+
const reused = reusedPersonIds.size;
|
|
59344
|
+
const hash2 = planHash([
|
|
59345
|
+
...[...groups.values()].flatMap((g2) => g2.txnIds),
|
|
59346
|
+
...directLinks.map((d2) => d2.transactionId)
|
|
59347
|
+
]);
|
|
59202
59348
|
if (!opts.dryRun && opts.expectedPlanHash !== undefined && opts.expectedPlanHash !== hash2) {
|
|
59203
59349
|
return {
|
|
59204
59350
|
dryRun: false,
|
|
@@ -59221,7 +59367,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
59221
59367
|
relationship: null,
|
|
59222
59368
|
notes: null
|
|
59223
59369
|
};
|
|
59224
|
-
return
|
|
59370
|
+
return crypto4 ? await encryptPersonCipher(fields, crypto4.key) : { name: displayName, upiHandles, phoneNumbers: [], relationship: null, notes: null };
|
|
59225
59371
|
}));
|
|
59226
59372
|
const res = await client.post("/api/persons/bulk", {
|
|
59227
59373
|
persons: bodies
|
|
@@ -59240,6 +59386,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
59240
59386
|
for (const txnId of g2.txnIds)
|
|
59241
59387
|
links.push({ transactionId: txnId, personId });
|
|
59242
59388
|
}
|
|
59389
|
+
links.push(...directLinks);
|
|
59243
59390
|
}
|
|
59244
59391
|
if (!opts.dryRun && links.length > 0) {
|
|
59245
59392
|
const CHUNK = 25;
|
|
@@ -59355,20 +59502,20 @@ function groupRows(rows) {
|
|
|
59355
59502
|
function isCategorizable(r2) {
|
|
59356
59503
|
return !r2.categoryId && !r2.isTransfer && !r2.isIgnored && !r2.isDuplicate;
|
|
59357
59504
|
}
|
|
59358
|
-
async function fetchUncategorized(client,
|
|
59505
|
+
async function fetchUncategorized(client, crypto4, range = {}) {
|
|
59359
59506
|
const params = { uncategorized: "true" };
|
|
59360
59507
|
if (range.startDate)
|
|
59361
59508
|
params.startDate = range.startDate;
|
|
59362
59509
|
if (range.endDate)
|
|
59363
59510
|
params.endDate = range.endDate;
|
|
59364
|
-
return (await fetchAllRows(client,
|
|
59511
|
+
return (await fetchAllRows(client, crypto4, params)).filter(isCategorizable);
|
|
59365
59512
|
}
|
|
59366
59513
|
var MERCHANT_CREATE_CONCURRENCY = 5;
|
|
59367
59514
|
var titleCase = (s2) => s2.replace(/\b[a-z]/g, (c2) => c2.toUpperCase());
|
|
59368
|
-
async function applyGroupAssignments(client,
|
|
59515
|
+
async function applyGroupAssignments(client, crypto4, assignments, opts = {}) {
|
|
59369
59516
|
const catIndex = await loadCategoryIndex(client);
|
|
59370
59517
|
const resolved = assignments.map((a2) => ({ a: a2, cat: requireCategory(catIndex, a2.categorySlug) }));
|
|
59371
|
-
const groups = groupRows(await fetchUncategorized(client,
|
|
59518
|
+
const groups = groupRows(await fetchUncategorized(client, crypto4, opts));
|
|
59372
59519
|
const result = { applied: [], notFound: [], rulesSaved: 0, unlearnable: [] };
|
|
59373
59520
|
const plan = [];
|
|
59374
59521
|
const claimed = new Set;
|
|
@@ -59380,7 +59527,13 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
|
|
|
59380
59527
|
}
|
|
59381
59528
|
for (const g2 of hits) {
|
|
59382
59529
|
claimed.add(g2);
|
|
59383
|
-
plan.push({
|
|
59530
|
+
plan.push({
|
|
59531
|
+
group: g2,
|
|
59532
|
+
categoryId: cat.id,
|
|
59533
|
+
slug: cat.slug,
|
|
59534
|
+
learn: a2.learn !== false,
|
|
59535
|
+
confirmMerchant: !!a2.confirmMerchant
|
|
59536
|
+
});
|
|
59384
59537
|
}
|
|
59385
59538
|
}
|
|
59386
59539
|
result.planHash = planHash(plan.flatMap((p2) => p2.group.rows.map((r2) => r2.id)));
|
|
@@ -59414,10 +59567,10 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
|
|
|
59414
59567
|
const learnPlans = [];
|
|
59415
59568
|
const directPlans = [];
|
|
59416
59569
|
for (const p2 of plan) {
|
|
59417
|
-
const { group, learn } = p2;
|
|
59570
|
+
const { group, learn, confirmMerchant } = p2;
|
|
59418
59571
|
if (!learn || group.pattern === UNPARSED) {
|
|
59419
59572
|
directPlans.push({ p: p2, reason: learn ? UNPARSED : undefined });
|
|
59420
|
-
} else if (
|
|
59573
|
+
} else if (crypto4 && !confirmMerchant && group.rows.some((r2) => isPersonLikeNarration(r2.description))) {
|
|
59421
59574
|
directPlans.push({ p: p2, reason: `${group.pattern} (private mode: person-like)` });
|
|
59422
59575
|
} else if (taught.has(group.pattern) && taught.get(group.pattern) !== p2.categoryId) {
|
|
59423
59576
|
directPlans.push({ p: p2, reason: `${group.pattern} (conflicting category for same pattern)` });
|
|
@@ -59515,12 +59668,12 @@ function flagBatchDuplicates(txns) {
|
|
|
59515
59668
|
return false;
|
|
59516
59669
|
});
|
|
59517
59670
|
}
|
|
59518
|
-
async function importTransactions(client,
|
|
59671
|
+
async function importTransactions(client, crypto4, opts) {
|
|
59519
59672
|
let txns = opts.txns;
|
|
59520
59673
|
let duplicatesSkipped = 0;
|
|
59521
59674
|
if (txns.length > 0) {
|
|
59522
59675
|
const dates = txns.map((t2) => t2.date).sort();
|
|
59523
|
-
const existing = await fetchAllRows(client,
|
|
59676
|
+
const existing = await fetchAllRows(client, crypto4, {
|
|
59524
59677
|
startDate: dates[0],
|
|
59525
59678
|
endDate: dates[dates.length - 1]
|
|
59526
59679
|
});
|
|
@@ -59536,8 +59689,8 @@ async function importTransactions(client, crypto3, opts) {
|
|
|
59536
59689
|
const base = { ...t2, isDuplicate: dupFlags[i2] };
|
|
59537
59690
|
return enrichedData ? { ...base, enrichedData } : base;
|
|
59538
59691
|
});
|
|
59539
|
-
if (
|
|
59540
|
-
await encryptTransactionFields(rows,
|
|
59692
|
+
if (crypto4)
|
|
59693
|
+
await encryptTransactionFields(rows, crypto4.key);
|
|
59541
59694
|
const CHUNK = 200;
|
|
59542
59695
|
let imported = 0;
|
|
59543
59696
|
for (let i2 = 0;i2 < rows.length; i2 += CHUNK) {
|
|
@@ -59552,19 +59705,19 @@ async function importTransactions(client, crypto3, opts) {
|
|
|
59552
59705
|
}
|
|
59553
59706
|
return { imported, duplicatesSkipped, duplicatesFlagged };
|
|
59554
59707
|
}
|
|
59555
|
-
async function runRescan(client,
|
|
59708
|
+
async function runRescan(client, crypto4) {
|
|
59556
59709
|
let bag = structuredClone(EMPTY_BAG);
|
|
59557
59710
|
try {
|
|
59558
59711
|
bag = (await loadBag(client)).bag;
|
|
59559
59712
|
} catch {}
|
|
59560
59713
|
const merchantsRes = await client.get("/api/merchants");
|
|
59561
59714
|
const allMerchants0 = merchantsRes.data ?? [];
|
|
59562
|
-
const allMerchants =
|
|
59715
|
+
const allMerchants = crypto4 ? (await decryptMerchantFields(allMerchants0, crypto4.key)).data : allMerchants0;
|
|
59563
59716
|
const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
59564
59717
|
const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
59565
59718
|
const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
|
|
59566
|
-
const persons = await loadDecryptedPersons(client,
|
|
59567
|
-
const allRows = await fetchAllRows(client,
|
|
59719
|
+
const persons = await loadDecryptedPersons(client, crypto4);
|
|
59720
|
+
const allRows = await fetchAllRows(client, crypto4, { unresolved: "true" });
|
|
59568
59721
|
const categoryByTxn = new Map(allRows.map((r2) => [r2.id, r2.categoryId]));
|
|
59569
59722
|
const plan = autoCategorizeFromDescriptions({
|
|
59570
59723
|
rows: allRows,
|
|
@@ -59573,8 +59726,17 @@ async function runRescan(client, crypto3) {
|
|
|
59573
59726
|
localMerchants,
|
|
59574
59727
|
persons
|
|
59575
59728
|
});
|
|
59729
|
+
const knownMerchants = new Set([
|
|
59730
|
+
...allMerchants.map((m2) => m2.id),
|
|
59731
|
+
...allRows.flatMap((r2) => r2.merchantId ? [r2.merchantId] : [])
|
|
59732
|
+
]);
|
|
59733
|
+
const knownPersons = new Set([
|
|
59734
|
+
...persons.map((p2) => p2.id),
|
|
59735
|
+
...allRows.flatMap((r2) => r2.personId ? [r2.personId] : [])
|
|
59736
|
+
]);
|
|
59737
|
+
const live = plan.suggestions.filter((s2) => s2.entity.type === "merchant" ? knownMerchants.has(s2.entity.merchantId) : knownPersons.has(s2.entity.personId));
|
|
59576
59738
|
let categorized = 0;
|
|
59577
|
-
const links =
|
|
59739
|
+
const links = live.flatMap((s2) => {
|
|
59578
59740
|
const categoryId = s2.categoryId ?? (s2.entity.type === "merchant" ? merchantCategoryById.get(s2.entity.merchantId) ?? null : null);
|
|
59579
59741
|
return s2.transactionIds.map((txId) => {
|
|
59580
59742
|
const applyCategory = categoryId && !categoryByTxn.get(txId);
|
|
@@ -59594,7 +59756,7 @@ var TRUNCATED_WARNING = "TRUNCATED: the API capped this result, so it is INCOMPL
|
|
|
59594
59756
|
function withTruncationWarning(data) {
|
|
59595
59757
|
return data.truncated ? { warning: TRUNCATED_WARNING, ...data } : data;
|
|
59596
59758
|
}
|
|
59597
|
-
function transactionTools(client,
|
|
59759
|
+
function transactionTools(client, crypto4) {
|
|
59598
59760
|
return [
|
|
59599
59761
|
defineTool({
|
|
59600
59762
|
name: "get_transactions",
|
|
@@ -59641,7 +59803,7 @@ function transactionTools(client, crypto3) {
|
|
|
59641
59803
|
apiParams.categoryId = requireCategory(await loadCategoryIndex(client), categorySlug).id;
|
|
59642
59804
|
}
|
|
59643
59805
|
if (personName && !apiParams.personId) {
|
|
59644
|
-
const persons = await loadDecryptedPersons(client,
|
|
59806
|
+
const persons = await loadDecryptedPersons(client, crypto4);
|
|
59645
59807
|
const match = resolvePersonByName(persons, personName);
|
|
59646
59808
|
const err = (error62) => ({ error: error62 });
|
|
59647
59809
|
if (!match) {
|
|
@@ -59658,14 +59820,14 @@ function transactionTools(client, crypto3) {
|
|
|
59658
59820
|
const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
|
|
59659
59821
|
let rows = data.data ?? [];
|
|
59660
59822
|
let undecrypted = [];
|
|
59661
|
-
if (
|
|
59662
|
-
const dec = await decryptTransactionFields(rows,
|
|
59823
|
+
if (crypto4) {
|
|
59824
|
+
const dec = await decryptTransactionFields(rows, crypto4.key);
|
|
59663
59825
|
rows = dec.data;
|
|
59664
59826
|
undecrypted = dec.undecrypted;
|
|
59665
59827
|
}
|
|
59666
59828
|
const personNames = new Map;
|
|
59667
59829
|
if (rows.some((r2) => r2.personId)) {
|
|
59668
|
-
for (const p2 of await loadDecryptedPersons(client,
|
|
59830
|
+
for (const p2 of await loadDecryptedPersons(client, crypto4))
|
|
59669
59831
|
personNames.set(p2.id, p2.name);
|
|
59670
59832
|
}
|
|
59671
59833
|
if (full) {
|
|
@@ -59696,7 +59858,7 @@ function transactionTools(client, crypto3) {
|
|
|
59696
59858
|
limit: exports_external.number().optional().describe("Max groups (default 40, max 100)")
|
|
59697
59859
|
}),
|
|
59698
59860
|
handler: async ({ startDate, endDate, minTotal, limit }) => {
|
|
59699
|
-
const rows = await fetchUncategorized(client,
|
|
59861
|
+
const rows = await fetchUncategorized(client, crypto4, { startDate, endDate });
|
|
59700
59862
|
const all2 = groupRows(rows);
|
|
59701
59863
|
const shown = all2.filter((g2) => g2.total >= (minTotal ?? 0)).slice(0, Math.min(Math.max(limit ?? 40, 1), 100));
|
|
59702
59864
|
const shownRows = shown.reduce((n2, g2) => n2 + g2.rows.length, 0);
|
|
@@ -59723,7 +59885,8 @@ function transactionTools(client, crypto3) {
|
|
|
59723
59885
|
pattern: exports_external.string().describe("Exactly as returned"),
|
|
59724
59886
|
type: exports_external.enum(["debit", "credit"]).optional(),
|
|
59725
59887
|
categorySlug: exports_external.string(),
|
|
59726
|
-
learn: exports_external.boolean().optional().describe("false = no rule (short/generic patterns)")
|
|
59888
|
+
learn: exports_external.boolean().optional().describe("false = no rule (short/generic patterns)"),
|
|
59889
|
+
confirmMerchant: exports_external.boolean().optional().describe("true = you've confirmed this pattern is a real business (brand lookup, web search, etc), not a person — skips the private-mode person-like block that would otherwise refuse to save a merchant rule")
|
|
59727
59890
|
})).min(1).max(100),
|
|
59728
59891
|
startDate: exports_external.string().optional().describe("Same range as the listing"),
|
|
59729
59892
|
endDate: exports_external.string().optional(),
|
|
@@ -59732,7 +59895,7 @@ function transactionTools(client, crypto3) {
|
|
|
59732
59895
|
handler: (args, ctx) => {
|
|
59733
59896
|
const { assignments, startDate, endDate, dryRun } = args;
|
|
59734
59897
|
const { expectedPlanHash } = args;
|
|
59735
|
-
return applyGroupAssignments(client,
|
|
59898
|
+
return applyGroupAssignments(client, crypto4, assignments, {
|
|
59736
59899
|
dryRun,
|
|
59737
59900
|
startDate,
|
|
59738
59901
|
endDate,
|
|
@@ -59745,7 +59908,7 @@ function transactionTools(client, crypto3) {
|
|
|
59745
59908
|
name: "backfill_persons",
|
|
59746
59909
|
description: "Create persons from UPI/IMPS counterparties in unlinked transactions (deduped by name) and link those transactions to them. Idempotent. dryRun previews.",
|
|
59747
59910
|
input: exports_external.object({ dryRun: exports_external.boolean().optional() }),
|
|
59748
|
-
handler: (args, ctx) => backfillPersons(client,
|
|
59911
|
+
handler: (args, ctx) => backfillPersons(client, crypto4, {
|
|
59749
59912
|
onProgress: progressFor(ctx),
|
|
59750
59913
|
dryRun: args.dryRun,
|
|
59751
59914
|
expectedPlanHash: args.expectedPlanHash
|
|
@@ -59767,8 +59930,8 @@ function transactionTools(client, crypto3) {
|
|
|
59767
59930
|
}),
|
|
59768
59931
|
handler: async ({ id, ...updates }) => {
|
|
59769
59932
|
const body = { ...updates };
|
|
59770
|
-
if (
|
|
59771
|
-
body.notes = await encryptField(body.notes,
|
|
59933
|
+
if (crypto4 && body.notes)
|
|
59934
|
+
body.notes = await encryptField(body.notes, crypto4.key);
|
|
59772
59935
|
if (body.categorySlug) {
|
|
59773
59936
|
const cat = requireCategory(await loadCategoryIndex(client), body.categorySlug);
|
|
59774
59937
|
body.categoryId = cat.id;
|
|
@@ -59817,8 +59980,8 @@ function transactionTools(client, crypto3) {
|
|
|
59817
59980
|
}
|
|
59818
59981
|
if (action === "list") {
|
|
59819
59982
|
const data2 = await client.get("/api/transactions/duplicates");
|
|
59820
|
-
if (
|
|
59821
|
-
const { data: decrypted, undecrypted } = await decryptTransactionFields(data2.data,
|
|
59983
|
+
if (crypto4 && data2.data) {
|
|
59984
|
+
const { data: decrypted, undecrypted } = await decryptTransactionFields(data2.data, crypto4.key);
|
|
59822
59985
|
data2.data = decrypted;
|
|
59823
59986
|
return withTruncationWarning(undecrypted.length ? { ...data2, undecrypted } : data2);
|
|
59824
59987
|
}
|
|
@@ -59841,8 +60004,8 @@ function transactionTools(client, crypto3) {
|
|
|
59841
60004
|
}),
|
|
59842
60005
|
handler: async ({ accountId, startDate, endDate }) => {
|
|
59843
60006
|
const data = await client.get("/api/transactions/reconciliation-context", { accountId, startDate, endDate });
|
|
59844
|
-
if (
|
|
59845
|
-
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.data,
|
|
60007
|
+
if (crypto4 && data.data) {
|
|
60008
|
+
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.data, crypto4.key);
|
|
59846
60009
|
data.data = decrypted;
|
|
59847
60010
|
return withTruncationWarning(undecrypted.length ? { ...data, undecrypted } : data);
|
|
59848
60011
|
}
|
|
@@ -59929,7 +60092,7 @@ function transactionTools(client, crypto3) {
|
|
|
59929
60092
|
name: "rescan_transactions",
|
|
59930
60093
|
description: "Apply saved learning rules (merchant aliases, category rules) to uncategorized transactions, like the web app's auto-categorize.",
|
|
59931
60094
|
input: exports_external.object({}),
|
|
59932
|
-
handler: () => runRescan(client,
|
|
60095
|
+
handler: () => runRescan(client, crypto4)
|
|
59933
60096
|
}),
|
|
59934
60097
|
defineTool({
|
|
59935
60098
|
name: "get_settlement_candidates",
|
|
@@ -59937,8 +60100,8 @@ function transactionTools(client, crypto3) {
|
|
|
59937
60100
|
input: exports_external.object({ ccAccountId: exports_external.string().uuid() }),
|
|
59938
60101
|
handler: async ({ ccAccountId }) => {
|
|
59939
60102
|
const data = await client.get("/api/transactions/settlement-candidates", { ccAccountId });
|
|
59940
|
-
if (
|
|
59941
|
-
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.data,
|
|
60103
|
+
if (crypto4 && data.data) {
|
|
60104
|
+
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.data, crypto4.key);
|
|
59942
60105
|
data.data = decrypted;
|
|
59943
60106
|
return withTruncationWarning(undecrypted.length ? { ...data, undecrypted } : data);
|
|
59944
60107
|
}
|
|
@@ -59955,8 +60118,8 @@ function transactionTools(client, crypto3) {
|
|
|
59955
60118
|
if (!row)
|
|
59956
60119
|
return res;
|
|
59957
60120
|
let undecrypted = [];
|
|
59958
|
-
if (
|
|
59959
|
-
const dec = await decryptTransactionFields([row],
|
|
60121
|
+
if (crypto4) {
|
|
60122
|
+
const dec = await decryptTransactionFields([row], crypto4.key);
|
|
59960
60123
|
row = dec.data[0];
|
|
59961
60124
|
undecrypted = dec.undecrypted;
|
|
59962
60125
|
}
|
|
@@ -59982,7 +60145,7 @@ function transactionTools(client, crypto3) {
|
|
|
59982
60145
|
}
|
|
59983
60146
|
|
|
59984
60147
|
// src/tools/export.ts
|
|
59985
|
-
function exportTools(client,
|
|
60148
|
+
function exportTools(client, crypto4) {
|
|
59986
60149
|
return [
|
|
59987
60150
|
defineTool({
|
|
59988
60151
|
name: "export_transactions",
|
|
@@ -59999,8 +60162,8 @@ function exportTools(client, crypto3) {
|
|
|
59999
60162
|
...Object.fromEntries(Object.entries(params).filter(([, v2]) => v2 !== undefined)),
|
|
60000
60163
|
format: "json"
|
|
60001
60164
|
});
|
|
60002
|
-
if (
|
|
60003
|
-
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.transactions,
|
|
60165
|
+
if (crypto4 && Array.isArray(data?.transactions)) {
|
|
60166
|
+
const { data: decrypted, undecrypted } = await decryptTransactionFields(data.transactions, crypto4.key);
|
|
60004
60167
|
data.transactions = decrypted;
|
|
60005
60168
|
return withTruncationWarning(undecrypted.length ? { ...data, undecrypted } : data);
|
|
60006
60169
|
}
|
|
@@ -60242,28 +60405,28 @@ var DEFAULT_MIN_CONFIDENCE = 0.95;
|
|
|
60242
60405
|
var TIMEOUT_MS = 8000;
|
|
60243
60406
|
var CONCURRENCY = 8;
|
|
60244
60407
|
var MIN_LEARN_PATTERN_LEN = 5;
|
|
60245
|
-
function assertPrivateModeAllowed(
|
|
60246
|
-
if (
|
|
60408
|
+
function assertPrivateModeAllowed(crypto4, env2 = process.env) {
|
|
60409
|
+
if (crypto4 && env2.JEV_ALLOW_PRIVATE_MODE !== "1") {
|
|
60247
60410
|
throw new Error("Jev categorization is disabled in private mode: it would send transaction text to a third party. " + "To opt in for your own account, set JEV_ALLOW_PRIVATE_MODE=1 in the MCP's environment. " + "Otherwise use get_uncategorized_groups + categorize_groups.");
|
|
60248
60411
|
}
|
|
60249
60412
|
}
|
|
60250
|
-
function assertJevAllowed(
|
|
60251
|
-
assertPrivateModeAllowed(
|
|
60413
|
+
function assertJevAllowed(crypto4, env2 = process.env) {
|
|
60414
|
+
assertPrivateModeAllowed(crypto4, env2);
|
|
60252
60415
|
const key = env2.JEV_API_KEY;
|
|
60253
60416
|
if (!key)
|
|
60254
60417
|
throw new Error("JEV_API_KEY is not set — Jev categorization is off.");
|
|
60255
60418
|
return key;
|
|
60256
60419
|
}
|
|
60257
|
-
function jevAllowSensitive(
|
|
60258
|
-
return
|
|
60420
|
+
function jevAllowSensitive(crypto4, env2) {
|
|
60421
|
+
return crypto4 === undefined || env2.JEV_ALLOW_PRIVATE_MODE === "1";
|
|
60259
60422
|
}
|
|
60260
|
-
function makeJev(apiKey,
|
|
60423
|
+
function makeJev(apiKey, crypto4, env2 = process.env, fetchImpl) {
|
|
60261
60424
|
return createJev({
|
|
60262
60425
|
apiKey,
|
|
60263
60426
|
model: JEV_MODEL,
|
|
60264
60427
|
minConfidence: DEFAULT_MIN_CONFIDENCE,
|
|
60265
60428
|
timeoutMs: TIMEOUT_MS,
|
|
60266
|
-
allowSensitive: jevAllowSensitive(
|
|
60429
|
+
allowSensitive: jevAllowSensitive(crypto4, env2),
|
|
60267
60430
|
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
60268
60431
|
});
|
|
60269
60432
|
}
|
|
@@ -60283,10 +60446,10 @@ function groupState(type, sample) {
|
|
|
60283
60446
|
const dir = type === "credit" ? "Money received" : "Money paid out";
|
|
60284
60447
|
return `${dir}. Bank narration: "${sample.slice(0, 80)}"`;
|
|
60285
60448
|
}
|
|
60286
|
-
async function jevCategorize(client,
|
|
60287
|
-
assertPrivateModeAllowed(
|
|
60449
|
+
async function jevCategorize(client, crypto4, jev, opts = {}) {
|
|
60450
|
+
assertPrivateModeAllowed(crypto4, opts.env);
|
|
60288
60451
|
const index = await loadCategoryIndex(client);
|
|
60289
|
-
const rows = await fetchUncategorized(client,
|
|
60452
|
+
const rows = await fetchUncategorized(client, crypto4, opts);
|
|
60290
60453
|
const groups = groupRows(rows);
|
|
60291
60454
|
const unparsed = groups.filter((g2) => g2.pattern === UNPARSED).length;
|
|
60292
60455
|
const candidates = groups.filter((g2) => g2.pattern !== UNPARSED);
|
|
@@ -60294,7 +60457,7 @@ async function jevCategorize(client, crypto3, jev, opts = {}) {
|
|
|
60294
60457
|
const suggestions = await mapLimit(candidates, CONCURRENCY, async (g2) => {
|
|
60295
60458
|
const options = optionsFor(g2.type, index);
|
|
60296
60459
|
const sample = g2.rows[0].description;
|
|
60297
|
-
const text =
|
|
60460
|
+
const text = crypto4 ? g2.pattern : sample;
|
|
60298
60461
|
const d2 = await jev.choice({
|
|
60299
60462
|
question: "Which spending category best fits this bank transaction?",
|
|
60300
60463
|
options,
|
|
@@ -60336,7 +60499,7 @@ async function jevCategorize(client, crypto3, jev, opts = {}) {
|
|
|
60336
60499
|
}
|
|
60337
60500
|
}
|
|
60338
60501
|
review.sort((a2, b2) => b2.total - a2.total);
|
|
60339
|
-
const applied = await applyGroupAssignments(client,
|
|
60502
|
+
const applied = await applyGroupAssignments(client, crypto4, accepted.map(({ confidence: _c, ...a2 }) => a2), { dryRun: opts.dryRun, startDate: opts.startDate, endDate: opts.endDate });
|
|
60340
60503
|
const conf = new Map(accepted.map((a2) => [`${a2.pattern}|${a2.type}`, a2.confidence]));
|
|
60341
60504
|
return {
|
|
60342
60505
|
dryRun: !!opts.dryRun,
|
|
@@ -60353,7 +60516,7 @@ async function jevCategorize(client, crypto3, jev, opts = {}) {
|
|
|
60353
60516
|
}
|
|
60354
60517
|
|
|
60355
60518
|
// src/tools/jev.ts
|
|
60356
|
-
function jevTools(client,
|
|
60519
|
+
function jevTools(client, crypto4) {
|
|
60357
60520
|
return [
|
|
60358
60521
|
defineTool({
|
|
60359
60522
|
name: "auto_categorize_jev",
|
|
@@ -60365,9 +60528,9 @@ function jevTools(client, crypto3) {
|
|
|
60365
60528
|
dryRun: exports_external.boolean().optional()
|
|
60366
60529
|
}),
|
|
60367
60530
|
handler: async ({ startDate, endDate, minConfidence, dryRun }) => {
|
|
60368
|
-
const key = assertJevAllowed(
|
|
60369
|
-
const jev = makeJev(key,
|
|
60370
|
-
const result = await jevCategorize(client,
|
|
60531
|
+
const key = assertJevAllowed(crypto4);
|
|
60532
|
+
const jev = makeJev(key, crypto4);
|
|
60533
|
+
const result = await jevCategorize(client, crypto4, jev, {
|
|
60371
60534
|
startDate,
|
|
60372
60535
|
endDate,
|
|
60373
60536
|
minConfidence,
|
|
@@ -60391,12 +60554,12 @@ function profileVariants(client) {
|
|
|
60391
60554
|
function settingsTools(client) {
|
|
60392
60555
|
const profile = profileVariants(client);
|
|
60393
60556
|
const household = householdVariants(client);
|
|
60394
|
-
const
|
|
60557
|
+
const crypto4 = authCryptoVariants(client);
|
|
60395
60558
|
const get = unionTool({
|
|
60396
60559
|
name: "get_settings",
|
|
60397
60560
|
description: "Read settings. profile: your name, email, owner. household: name, currency, locale, owner labels, members. encryption: mode (standard/private) and whether a PIN verifier / recovery key is set.",
|
|
60398
60561
|
key: "section",
|
|
60399
|
-
variants: { profile: profile.get, household: household.get, encryption:
|
|
60562
|
+
variants: { profile: profile.get, household: household.get, encryption: crypto4.status }
|
|
60400
60563
|
});
|
|
60401
60564
|
const update = unionTool({
|
|
60402
60565
|
name: "update_settings",
|
|
@@ -60478,7 +60641,7 @@ async function fetchReconciliationContext(client, accountId, start, end, budget
|
|
|
60478
60641
|
}
|
|
60479
60642
|
var sum2 = (rows) => Math.round(rows.reduce((n2, r2) => n2 + r2.amount, 0));
|
|
60480
60643
|
async function runStatementImport(deps, args) {
|
|
60481
|
-
const { client, crypto:
|
|
60644
|
+
const { client, crypto: crypto4 } = deps;
|
|
60482
60645
|
const accounts = (await client.get("/api/accounts")).data ?? [];
|
|
60483
60646
|
const account = resolveAccount(accounts, args.account);
|
|
60484
60647
|
const format = pickFormat(account, args.format);
|
|
@@ -60497,7 +60660,7 @@ async function runStatementImport(deps, args) {
|
|
|
60497
60660
|
}
|
|
60498
60661
|
const dates = txns.map((t2) => t2.date).sort();
|
|
60499
60662
|
const existing0 = await fetchReconciliationContext(client, account.id, dates[0], dates[dates.length - 1]);
|
|
60500
|
-
const existing =
|
|
60663
|
+
const existing = crypto4 ? (await decryptTransactionFields(existing0, crypto4.key)).data : existing0;
|
|
60501
60664
|
const present = existing.map((r2) => ({ ...r2, isDuplicate: false, isIgnored: false }));
|
|
60502
60665
|
const result = reconcile(present, { ...statement, transactions: txns });
|
|
60503
60666
|
const fresh = result.unmatched;
|
|
@@ -60538,7 +60701,7 @@ async function runStatementImport(deps, args) {
|
|
|
60538
60701
|
if (fresh.length === 0)
|
|
60539
60702
|
return { ...summary, dryRun: false, imported: 0 };
|
|
60540
60703
|
const savings = account.type !== "credit_card";
|
|
60541
|
-
const imported = await importTransactions(client,
|
|
60704
|
+
const imported = await importTransactions(client, crypto4, {
|
|
60542
60705
|
onProgress: deps.onProgress,
|
|
60543
60706
|
accountId: account.id,
|
|
60544
60707
|
source: savings ? "bank_statement" : "cc_statement",
|
|
@@ -60551,7 +60714,7 @@ async function runStatementImport(deps, args) {
|
|
|
60551
60714
|
referenceNumber: t2.referenceNumber ?? undefined
|
|
60552
60715
|
}))
|
|
60553
60716
|
});
|
|
60554
|
-
const rescan = await runRescan(client,
|
|
60717
|
+
const rescan = await runRescan(client, crypto4);
|
|
60555
60718
|
return { ...summary, dryRun: false, ...imported, rescan };
|
|
60556
60719
|
}
|
|
60557
60720
|
|
|
@@ -60587,16 +60750,16 @@ var statementSource = exports_external.enum([
|
|
|
60587
60750
|
"ai_import"
|
|
60588
60751
|
]);
|
|
60589
60752
|
var granularity = exports_external.number().int().min(1).max(5).describe("1=bank, 2=CC, 3=manual, 4=app export, 5=telegram/AI");
|
|
60590
|
-
function statementImportTools(client,
|
|
60753
|
+
function statementImportTools(client, crypto4) {
|
|
60591
60754
|
const pdf = (confirm2) => variant(pdfInput, (args, ctx) => runStatementImport({
|
|
60592
60755
|
client,
|
|
60593
|
-
crypto:
|
|
60756
|
+
crypto: crypto4,
|
|
60594
60757
|
readFile: readFileAsArrayBuffer,
|
|
60595
60758
|
parsers: realParsers,
|
|
60596
60759
|
onProgress: progressFor(ctx)
|
|
60597
60760
|
}, confirm2 ? { ...args, confirm: true } : args));
|
|
60598
60761
|
const csvParse = variant(exports_external.object({ filePath: exports_external.string() }), async ({ filePath }) => {
|
|
60599
|
-
if (
|
|
60762
|
+
if (crypto4)
|
|
60600
60763
|
return CSV_PRIVATE_MODE_ERROR;
|
|
60601
60764
|
const buf = await readFile2(filePath);
|
|
60602
60765
|
const form = new FormData;
|
|
@@ -60631,7 +60794,7 @@ function statementImportTools(client, crypto3) {
|
|
|
60631
60794
|
skipDuplicates: exports_external.boolean().optional(),
|
|
60632
60795
|
force: exports_external.boolean().optional()
|
|
60633
60796
|
}), async ({ filePath, mapping, accountId, owner, skipDuplicates, force }) => {
|
|
60634
|
-
if (
|
|
60797
|
+
if (crypto4)
|
|
60635
60798
|
return CSV_PRIVATE_MODE_ERROR;
|
|
60636
60799
|
const csvText = await readFile2(filePath, "utf8");
|
|
60637
60800
|
const data = await client.post("/api/transactions/import/confirm", {
|
|
@@ -60656,14 +60819,14 @@ function statementImportTools(client, crypto3) {
|
|
|
60656
60819
|
referenceNumber: exports_external.string().optional()
|
|
60657
60820
|
}))
|
|
60658
60821
|
}), async ({ accountId, source, granularity: granularity2, transactions: txns }, ctx) => {
|
|
60659
|
-
const data = await importTransactions(client,
|
|
60822
|
+
const data = await importTransactions(client, crypto4, {
|
|
60660
60823
|
onProgress: progressFor(ctx),
|
|
60661
60824
|
accountId,
|
|
60662
60825
|
source,
|
|
60663
60826
|
granularity: granularity2,
|
|
60664
60827
|
txns
|
|
60665
60828
|
});
|
|
60666
|
-
const rescan = await runRescan(client,
|
|
60829
|
+
const rescan = await runRescan(client, crypto4);
|
|
60667
60830
|
return { import: data, rescan };
|
|
60668
60831
|
}),
|
|
60669
60832
|
json_file: variant(exports_external.object({
|
|
@@ -60687,14 +60850,14 @@ function statementImportTools(client, crypto3) {
|
|
|
60687
60850
|
} catch (e2) {
|
|
60688
60851
|
throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
60689
60852
|
}
|
|
60690
|
-
const data = await importTransactions(client,
|
|
60853
|
+
const data = await importTransactions(client, crypto4, {
|
|
60691
60854
|
onProgress: progressFor(ctx),
|
|
60692
60855
|
accountId,
|
|
60693
60856
|
source,
|
|
60694
60857
|
granularity: granularity2,
|
|
60695
60858
|
txns
|
|
60696
60859
|
});
|
|
60697
|
-
const rescan = await runRescan(client,
|
|
60860
|
+
const rescan = await runRescan(client, crypto4);
|
|
60698
60861
|
return { import: data, count: txns.length, rescan };
|
|
60699
60862
|
})
|
|
60700
60863
|
}
|
|
@@ -60809,7 +60972,14 @@ function scrubCiphertext(value) {
|
|
|
60809
60972
|
}
|
|
60810
60973
|
async function runTool(tool, args, mcp) {
|
|
60811
60974
|
const input2 = tool.input ? await tool.input.parseAsync(args) : args;
|
|
60812
|
-
const ctx = {
|
|
60975
|
+
const ctx = {
|
|
60976
|
+
tool: tool.name,
|
|
60977
|
+
signal: mcp.mcpReq.signal,
|
|
60978
|
+
mcp,
|
|
60979
|
+
confirmSupport: { getClientCapabilities: () => {
|
|
60980
|
+
return;
|
|
60981
|
+
} }
|
|
60982
|
+
};
|
|
60813
60983
|
return tool.handler(input2, ctx);
|
|
60814
60984
|
}
|
|
60815
60985
|
function jsonContents(uri, value) {
|
|
@@ -60844,7 +61014,7 @@ function registerResources2(server2, tools) {
|
|
|
60844
61014
|
// package.json
|
|
60845
61015
|
var package_default = {
|
|
60846
61016
|
name: "paisa-mcp",
|
|
60847
|
-
version: "0.4.
|
|
61017
|
+
version: "0.4.3",
|
|
60848
61018
|
repository: {
|
|
60849
61019
|
type: "git",
|
|
60850
61020
|
url: "git+https://github.com/nimit9/paisa.git",
|
|
@@ -60873,14 +61043,13 @@ var package_default = {
|
|
|
60873
61043
|
"@modelcontextprotocol/server": "^2.1.0",
|
|
60874
61044
|
"@nimit9/signet-ai": "^0.1.11",
|
|
60875
61045
|
"@nimit9/signet-lib": "^0.1.15",
|
|
60876
|
-
"@nimit9/signet-server": "^0.2.
|
|
61046
|
+
"@nimit9/signet-server": "^0.2.7",
|
|
61047
|
+
"@paisa/crypto": "workspace:*",
|
|
60877
61048
|
"@paisa/parsers": "workspace:*",
|
|
60878
61049
|
"@paisa/reconciliation": "workspace:*",
|
|
60879
61050
|
"@paisa/types": "workspace:*",
|
|
60880
|
-
"@scure/bip39": "^2.2.0",
|
|
60881
61051
|
"@types/bun": "^1.3.14",
|
|
60882
61052
|
axios: "^1.16.1",
|
|
60883
|
-
"hash-wasm": "^4.12.0",
|
|
60884
61053
|
typescript: "^7.0.2",
|
|
60885
61054
|
zod: "^4.6.5"
|
|
60886
61055
|
},
|
|
@@ -60899,17 +61068,14 @@ function paisaErrorResult(err) {
|
|
|
60899
61068
|
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }]
|
|
60900
61069
|
});
|
|
60901
61070
|
}
|
|
60902
|
-
|
|
60903
|
-
|
|
60904
|
-
|
|
60905
|
-
};
|
|
60906
|
-
const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)).map((t2) => {
|
|
60907
|
-
const caps = () => initializeCapabilities();
|
|
61071
|
+
var CONFIRM_KEY2 = randomBytes(32);
|
|
61072
|
+
function buildServer(client, crypto4) {
|
|
61073
|
+
const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto4)).map((t2) => {
|
|
60908
61074
|
const describe3 = CONFIRMED_TOOLS[t2.name];
|
|
60909
61075
|
if (describe3)
|
|
60910
|
-
return withConfirmation(t2, describe3
|
|
61076
|
+
return withConfirmation(t2, describe3);
|
|
60911
61077
|
const count = BULK_CONFIRMED_TOOLS[t2.name];
|
|
60912
|
-
return count ? withBulkConfirmation(t2, count
|
|
61078
|
+
return count ? withBulkConfirmation(t2, count) : t2;
|
|
60913
61079
|
}));
|
|
60914
61080
|
for (const t2 of tools)
|
|
60915
61081
|
if (t2.input)
|
|
@@ -60918,9 +61084,9 @@ function buildServer(client, crypto3) {
|
|
|
60918
61084
|
name: "paisa",
|
|
60919
61085
|
version: VERSION2,
|
|
60920
61086
|
tools,
|
|
60921
|
-
errorResult: paisaErrorResult
|
|
61087
|
+
errorResult: paisaErrorResult,
|
|
61088
|
+
confirm: { key: CONFIRM_KEY2 }
|
|
60922
61089
|
});
|
|
60923
|
-
initializeCapabilities = () => server2.server.getClientCapabilities();
|
|
60924
61090
|
registerResources2(server2, tools);
|
|
60925
61091
|
registerPrompts2(server2);
|
|
60926
61092
|
return server2;
|
|
@@ -60947,5 +61113,5 @@ main().catch((err) => {
|
|
|
60947
61113
|
process.exit(1);
|
|
60948
61114
|
});
|
|
60949
61115
|
|
|
60950
|
-
//# debugId=
|
|
61116
|
+
//# debugId=1892106D83105DD764756E2164756E21
|
|
60951
61117
|
//# sourceMappingURL=index.js.map
|