paisa-mcp 0.4.1 → 0.4.2

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 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" };
@@ -53439,10 +53428,153 @@ async function initCrypto(client, pin, userId) {
53439
53428
  return { key };
53440
53429
  }
53441
53430
 
53431
+ // src/server.ts
53432
+ import { randomBytes } from "node:crypto";
53433
+
53442
53434
  // ../../node_modules/.bun/@modelcontextprotocol+server@2.1.0/node_modules/@modelcontextprotocol/server/dist/index.mjs
53443
53435
  var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
53436
+ var PREFIX = "v1.";
53437
+ function bytesToBase64Url(bytes) {
53438
+ let bin = "";
53439
+ for (const b of bytes)
53440
+ bin += String.fromCodePoint(b);
53441
+ return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
53442
+ }
53443
+ function constantTimeTagEqual(a, b) {
53444
+ if (a.length !== b.length)
53445
+ return false;
53446
+ let r = 0;
53447
+ for (let i = 0;i < a.length; i++)
53448
+ r |= a.codePointAt(i) ^ b.codePointAt(i);
53449
+ return r === 0;
53450
+ }
53451
+ function base64UrlToBytes(s) {
53452
+ const b64 = s.replaceAll("-", "+").replaceAll("_", "/");
53453
+ const bin = atob(b64);
53454
+ const bytes = new Uint8Array(bin.length);
53455
+ for (let i = 0;i < bin.length; i++)
53456
+ bytes[i] = bin.codePointAt(i);
53457
+ return bytes;
53458
+ }
53459
+ function createRequestStateCodec(options) {
53460
+ const subtle = globalThis.crypto?.subtle;
53461
+ if (subtle === undefined)
53462
+ 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");
53463
+ const keyBytes = typeof options.key === "string" ? new TextEncoder().encode(options.key) : Uint8Array.from(options.key);
53464
+ if (keyBytes.byteLength < 32)
53465
+ throw new RangeError(`createRequestStateCodec: key must be at least 32 bytes (got ${keyBytes.byteLength})`);
53466
+ const ttlSeconds = options.ttlSeconds ?? 600;
53467
+ if (!Number.isFinite(ttlSeconds))
53468
+ throw new RangeError("createRequestStateCodec: ttlSeconds must be a finite number");
53469
+ const bind2 = options.bind;
53470
+ let cryptoKey;
53471
+ const importedKey = () => cryptoKey ??= subtle.importKey("raw", keyBytes, {
53472
+ name: "HMAC",
53473
+ hash: "SHA-256"
53474
+ }, false, ["sign", "verify"]);
53475
+ const utf8 = new TextEncoder;
53476
+ const BIND_LABEL = "mcp.requestState.bind:";
53477
+ const bindTag = async (value) => {
53478
+ return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(BIND_LABEL + value))).slice(0, 16));
53479
+ };
53480
+ return {
53481
+ async mint(payload, ctx) {
53482
+ const envelope = {
53483
+ p: payload,
53484
+ exp: Math.floor(Date.now() / 1000) + ttlSeconds
53485
+ };
53486
+ if (bind2 !== undefined) {
53487
+ if (ctx === undefined)
53488
+ throw new TypeError("createRequestStateCodec: mint() requires ctx when a bind callback is configured");
53489
+ envelope.b = await bindTag(bind2(ctx));
53490
+ }
53491
+ const body = bytesToBase64Url(utf8.encode(JSON.stringify(envelope)));
53492
+ return `${PREFIX}${body}.${bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(PREFIX + body))))}`;
53493
+ },
53494
+ async verify(state, ctx) {
53495
+ const dot = state.lastIndexOf(".");
53496
+ if (!state.startsWith(PREFIX) || dot <= 3)
53497
+ throw new Error("malformed");
53498
+ const body = state.slice(3, dot);
53499
+ let macBytes;
53500
+ try {
53501
+ macBytes = base64UrlToBytes(state.slice(dot + 1));
53502
+ } catch {
53503
+ throw new Error("malformed");
53504
+ }
53505
+ if (!await subtle.verify("HMAC", await importedKey(), macBytes, utf8.encode(PREFIX + body)))
53506
+ throw new Error("mac");
53507
+ let envelope;
53508
+ try {
53509
+ envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlToBytes(body)));
53510
+ } catch {
53511
+ throw new Error("malformed");
53512
+ }
53513
+ if (typeof envelope.exp !== "number" || envelope.exp < Math.floor(Date.now() / 1000))
53514
+ throw new Error("expired");
53515
+ if (bind2 !== undefined) {
53516
+ const expected = await bindTag(bind2(ctx));
53517
+ if (envelope.b === undefined || !constantTimeTagEqual(envelope.b, expected))
53518
+ throw new Error("bind");
53519
+ } else if (envelope.b !== undefined)
53520
+ throw new Error("bind");
53521
+ return envelope.p;
53522
+ }
53523
+ };
53524
+ }
53444
53525
 
53445
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/interaction.js
53526
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/interaction.js
53527
+ var CONFIRM_KEY = "signet_confirm";
53528
+ function canonicalize(value) {
53529
+ if (Array.isArray(value))
53530
+ return value.map(canonicalize);
53531
+ if (value !== null && typeof value === "object") {
53532
+ const obj = value;
53533
+ return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, canonicalize(obj[k])]));
53534
+ }
53535
+ return value;
53536
+ }
53537
+ async function fingerprint(args) {
53538
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(canonicalize(args ?? null))));
53539
+ return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
53540
+ }
53541
+ async function confirm(ctx, { message, detail, args, key }) {
53542
+ const id = key ?? `${CONFIRM_KEY}:${ctx.tool}`;
53543
+ const view = inputResponse(ctx.mcp.mcpReq.inputResponses, id);
53544
+ if (view.kind === "missing") {
53545
+ if (!ctx.confirmSupport.getClientCapabilities()?.elicitation)
53546
+ return "unsupported";
53547
+ const payload = { tool: ctx.tool, id, fp: await fingerprint(args) };
53548
+ const requestState = await ctx.confirmSupport.mint?.(payload);
53549
+ return inputRequired({
53550
+ inputRequests: {
53551
+ [id]: inputRequired.elicit({
53552
+ message: detail ? `${message}
53553
+
53554
+ ${detail}` : message,
53555
+ requestedSchema: {
53556
+ type: "object",
53557
+ properties: { confirm: { type: "boolean", title: "Confirm" } },
53558
+ required: ["confirm"]
53559
+ }
53560
+ })
53561
+ },
53562
+ ...requestState ? { requestState } : {}
53563
+ });
53564
+ }
53565
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content?.confirm !== true) {
53566
+ return "declined";
53567
+ }
53568
+ if (ctx.confirmSupport.mint) {
53569
+ const state = ctx.mcp.mcpReq.requestState();
53570
+ if (!state || state.tool !== ctx.tool || state.id !== id || state.fp !== await fingerprint(args))
53571
+ return "declined";
53572
+ }
53573
+ return "confirmed";
53574
+ }
53575
+ function isConfirmPending(value) {
53576
+ return isInputRequiredResult(value);
53577
+ }
53446
53578
  async function progress(ctx, done, total, message) {
53447
53579
  const token = ctx.mcp.mcpReq._meta?.progressToken;
53448
53580
  if (token === undefined)
@@ -53459,7 +53591,7 @@ async function progress(ctx, done, total, message) {
53459
53591
  });
53460
53592
  } catch {}
53461
53593
  }
53462
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
53594
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
53463
53595
  var STATUS_CODES = {
53464
53596
  400: "BAD_REQUEST",
53465
53597
  401: "UNAUTHORIZED",
@@ -53473,7 +53605,7 @@ var STATUS_CODES = {
53473
53605
  429: "RATE_LIMITED"
53474
53606
  };
53475
53607
 
53476
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/errors.js
53608
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/errors.js
53477
53609
  function isZodError(err) {
53478
53610
  return err instanceof Error && err.name === "ZodError" && Array.isArray(err.issues);
53479
53611
  }
@@ -53537,7 +53669,7 @@ function report(opts, err, meta3, c) {
53537
53669
  } catch {}
53538
53670
  }
53539
53671
 
53540
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/primitives.js
53672
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/primitives.js
53541
53673
  var RAW = Symbol.for("signet.mcp.rawResult");
53542
53674
  function protocolError(err, deps, meta3) {
53543
53675
  if (ProtocolError.isInstance(err))
@@ -53570,8 +53702,14 @@ function checkScopes(scopes, token) {
53570
53702
  });
53571
53703
  }
53572
53704
  function baseContext(mcp, deps) {
53573
- const { c, token } = deps.context;
53574
- return { signal: mcp.mcpReq.signal, mcp, ...c ? { c } : {}, ...token ? { token } : {} };
53705
+ const { c, token, confirmSupport } = deps.context;
53706
+ return {
53707
+ signal: mcp.mcpReq.signal,
53708
+ mcp,
53709
+ confirmSupport,
53710
+ ...c ? { c } : {},
53711
+ ...token ? { token } : {}
53712
+ };
53575
53713
  }
53576
53714
  function toContents(uri, value, mimeType) {
53577
53715
  if (value !== null && typeof value === "object" && RAW in value)
@@ -53681,7 +53819,7 @@ function registerPrompts(server, prompts, deps) {
53681
53819
  }
53682
53820
  }
53683
53821
 
53684
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
53822
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
53685
53823
  var RAW2 = Symbol.for("signet.mcp.rawResult");
53686
53824
  function toolResult(result) {
53687
53825
  return Object.defineProperty({ ...result }, RAW2, { value: true, enumerable: false });
@@ -53709,9 +53847,14 @@ function createMcpServer(options) {
53709
53847
  ...options.resources?.length ? { resources: { listChanged: false } } : {},
53710
53848
  ...options.prompts?.length ? { prompts: { listChanged: false } } : {}
53711
53849
  };
53850
+ const confirmCodec = options.confirm ? createRequestStateCodec({
53851
+ key: options.confirm.key,
53852
+ ...options.confirm.ttlSeconds !== undefined ? { ttlSeconds: options.confirm.ttlSeconds } : {}
53853
+ }) : undefined;
53712
53854
  const server = new McpServer({ name: options.name, version: options.version }, {
53713
53855
  ...unlisted.size ? {} : { capabilities: toolsCapability },
53714
- ...options.instructions ? { instructions: options.instructions } : {}
53856
+ ...options.instructions ? { instructions: options.instructions } : {},
53857
+ ...confirmCodec ? { requestState: { verify: confirmCodec.verify } } : {}
53715
53858
  });
53716
53859
  let restore;
53717
53860
  if (unlisted.size) {
@@ -53726,6 +53869,10 @@ function createMcpServer(options) {
53726
53869
  tool: tool.name,
53727
53870
  signal: mcp.mcpReq.signal,
53728
53871
  mcp,
53872
+ confirmSupport: {
53873
+ getClientCapabilities: () => server.server.getClientCapabilities(),
53874
+ ...confirmCodec ? { mint: (payload) => confirmCodec.mint(payload, mcp) } : {}
53875
+ },
53729
53876
  ...c ? { c } : {},
53730
53877
  ...token ? { token } : {}
53731
53878
  };
@@ -53764,7 +53911,14 @@ function createMcpServer(options) {
53764
53911
  restore?.();
53765
53912
  restore = undefined;
53766
53913
  }
53767
- const deps = { options, context: { ...c ? { c } : {}, ...token ? { token } : {} } };
53914
+ const confirmSupport = {
53915
+ getClientCapabilities: () => server.server.getClientCapabilities(),
53916
+ ...confirmCodec ? { mint: (payload) => confirmCodec.mint(payload) } : {}
53917
+ };
53918
+ const deps = {
53919
+ options,
53920
+ context: { confirmSupport, ...c ? { c } : {}, ...token ? { token } : {} }
53921
+ };
53768
53922
  if (options.resources?.length)
53769
53923
  registerResources(server, options.resources, deps);
53770
53924
  if (options.prompts?.length)
@@ -53867,84 +54021,34 @@ function applyAnnotations(tools) {
53867
54021
  }
53868
54022
 
53869
54023
  // src/lib/confirm.ts
53870
- import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
53871
- var ANSWER_KEY = "confirm";
53872
- function clientCanConfirm(mcp, initializeCapabilities) {
53873
- const envelope = mcp.mcpReq.envelope;
53874
- const caps = envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? initializeCapabilities();
53875
- return Boolean(caps?.elicitation);
53876
- }
53877
- function planFingerprint(plan) {
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);
54024
+ var CANCELLED = { cancelled: true, message: "Cancelled by the user. Nothing was changed." };
54025
+ var PLAN_CHANGED = {
54026
+ planChanged: true,
54027
+ message: "The request changed after you confirmed, so nothing was changed. Run it again to see the new plan."
54028
+ };
54029
+ function acceptedDifferentPlan(ctx) {
54030
+ const view = inputResponse(ctx.mcp.mcpReq.inputResponses, `signet_confirm:${ctx.tool}`);
54031
+ return view.kind === "elicit" && view.action === "accept" && view.content?.confirm === true;
53898
54032
  }
53899
- function confirmPlan(mcp, message, plan, initializeCapabilities = () => {
53900
- return;
53901
- }) {
53902
- if (!clientCanConfirm(mcp, initializeCapabilities))
53903
- return { kind: "unsupported" };
53904
- const fingerprint = planFingerprint(plan);
53905
- const responses = mcp.mcpReq.inputResponses;
53906
- const answered = responses !== undefined && ANSWER_KEY in responses;
53907
- const echoed = mcp.mcpReq.requestState?.();
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
- };
54033
+ async function askThenRun(ctx, message, plan, run, fallback = run) {
54034
+ const answer = await confirm(ctx, { message, args: plan });
54035
+ if (isConfirmPending(answer))
54036
+ return answer;
54037
+ if (answer === "confirmed")
54038
+ return run();
54039
+ if (answer === "unsupported")
54040
+ return fallback();
54041
+ return acceptedDifferentPlan(ctx) ? PLAN_CHANGED : CANCELLED;
53935
54042
  }
53936
- function withConfirmation(tool, describe3, initializeCapabilities) {
54043
+ function withConfirmation(tool, describe3) {
53937
54044
  const run = tool.handler;
53938
54045
  return {
53939
54046
  ...tool,
53940
- handler: (args, ctx) => {
54047
+ handler: async (args, ctx) => {
53941
54048
  const message = describe3(args);
53942
54049
  if (message === null || !ctx?.mcp)
53943
54050
  return run(args, ctx);
53944
- const outcome = confirmPlan(ctx.mcp, message, { tool: tool.name, args }, initializeCapabilities);
53945
- if (outcome.kind === "confirmed" || outcome.kind === "unsupported")
53946
- return run(args, ctx);
53947
- return outcome.result;
54051
+ return askThenRun(ctx, message, { tool: tool.name, args }, () => run(args, ctx));
53948
54052
  }
53949
54053
  };
53950
54054
  }
@@ -54021,11 +54125,7 @@ var BULK_CONFIRMED_TOOLS = {
54021
54125
  }),
54022
54126
  bulk_link_entity: async (args) => ({ n: Array.isArray(args.links) ? args.links.length : 0 })
54023
54127
  };
54024
- var PLAN_CHANGED = {
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) {
54128
+ function withBulkConfirmation(tool, count) {
54029
54129
  const run = tool.handler;
54030
54130
  const runPlanned = async (args, ctx, hash2) => {
54031
54131
  const result = await run(hash2 === undefined ? args : { ...args, expectedPlanHash: hash2 }, ctx);
@@ -54034,18 +54134,13 @@ function withBulkConfirmation(tool, count, initializeCapabilities) {
54034
54134
  return {
54035
54135
  ...tool,
54036
54136
  handler: async (args, ctx) => {
54037
- if (args.dryRun === true || !ctx?.mcp || !clientCanConfirm(ctx.mcp, initializeCapabilities))
54137
+ const canAsk = Boolean(ctx?.confirmSupport?.getClientCapabilities()?.elicitation);
54138
+ if (args.dryRun === true || !ctx?.mcp || !canAsk)
54038
54139
  return run(args, ctx);
54039
54140
  const { n, planHash: hash2 } = await count(args, async (a) => run(a, ctx));
54040
54141
  if (n <= BULK_CONFIRM_THRESHOLD)
54041
54142
  return runPlanned(args, ctx, hash2);
54042
- const message = `Update ${plural2(n, "transaction")} in one go?`;
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;
54143
+ 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
54144
  }
54050
54145
  };
54051
54146
  }
@@ -54718,7 +54813,7 @@ function goalTools(client) {
54718
54813
  return [
54719
54814
  defineTool({
54720
54815
  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.",
54816
+ 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
54817
  input: exports_external.object({
54723
54818
  type: exports_external.enum(["goal", "emi", "sinking_fund"]),
54724
54819
  id: exports_external.string().optional(),
@@ -54739,7 +54834,8 @@ function goalTools(client) {
54739
54834
  startDate: exports_external.string().optional(),
54740
54835
  endDate: exports_external.string().optional(),
54741
54836
  totalMonths: exports_external.number().optional(),
54742
- paidMonths: exports_external.number().optional()
54837
+ paidMonths: exports_external.number().optional(),
54838
+ status: exports_external.enum(["active", "paused", "completed", "cancelled", "foreclosed"]).optional().describe("goal: active|paused|completed|cancelled; emi: active|completed|foreclosed")
54743
54839
  }),
54744
54840
  handler: async ({ type, ...fields }) => {
54745
54841
  const require2 = (keys) => {
@@ -54748,6 +54844,14 @@ function goalTools(client) {
54748
54844
  throw new Error(`upsert_savings type='${type}' requires: ${missing.join(", ")}`);
54749
54845
  }
54750
54846
  };
54847
+ const allowedStatus = {
54848
+ goal: ["active", "paused", "completed", "cancelled"],
54849
+ emi: ["active", "completed", "foreclosed"],
54850
+ sinking_fund: []
54851
+ };
54852
+ if (fields.status !== undefined && !allowedStatus[type].includes(fields.status)) {
54853
+ 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(", ")}`);
54854
+ }
54751
54855
  if (type === "goal") {
54752
54856
  const {
54753
54857
  id: id2,
@@ -54760,7 +54864,8 @@ function goalTools(client) {
54760
54864
  owner,
54761
54865
  icon,
54762
54866
  color,
54763
- notes
54867
+ notes,
54868
+ status
54764
54869
  } = fields;
54765
54870
  const body2 = {
54766
54871
  type: goalType,
@@ -54772,7 +54877,8 @@ function goalTools(client) {
54772
54877
  owner,
54773
54878
  icon,
54774
54879
  color,
54775
- notes
54880
+ notes,
54881
+ status
54776
54882
  };
54777
54883
  if (!id2)
54778
54884
  require2(["name", "targetAmount", "goalType"]);
@@ -54790,7 +54896,8 @@ function goalTools(client) {
54790
54896
  totalMonths,
54791
54897
  paidMonths,
54792
54898
  interestRate,
54793
- outstandingPrincipal
54899
+ outstandingPrincipal,
54900
+ status
54794
54901
  } = fields;
54795
54902
  const { owner } = fields;
54796
54903
  if (!id2) {
@@ -54814,7 +54921,8 @@ function goalTools(client) {
54814
54921
  paidMonths,
54815
54922
  interestRate,
54816
54923
  outstandingPrincipal,
54817
- owner
54924
+ owner,
54925
+ status
54818
54926
  };
54819
54927
  const data2 = id2 ? await client.patch(`/api/goals/emis/${id2}`, body2) : await client.post("/api/goals/emis", body2);
54820
54928
  return data2;
@@ -59025,9 +59133,9 @@ function parseAmount4(raw) {
59025
59133
  return Number.isNaN(num) ? 0 : Math.abs(num);
59026
59134
  }
59027
59135
  // src/lib/plan-hash.ts
59028
- import { createHash as createHash2 } from "node:crypto";
59136
+ import { createHash } from "node:crypto";
59029
59137
  function planHash(ids) {
59030
- return createHash2("sha256").update([...ids].sort().join(",")).digest("hex");
59138
+ return createHash("sha256").update([...ids].sort().join(",")).digest("hex");
59031
59139
  }
59032
59140
 
59033
59141
  // src/lib/txn-rows.ts
@@ -59105,6 +59213,9 @@ function toLeanRow(r2, personNames) {
59105
59213
  function normalizePersonName(name) {
59106
59214
  return name.toLowerCase().trim().replace(/\s+/g, " ");
59107
59215
  }
59216
+ function normalizeUpiHandle(handle) {
59217
+ return handle.toLowerCase().trim().split("@")[0];
59218
+ }
59108
59219
  async function loadDecryptedPersons(client, crypto3) {
59109
59220
  const res = await client.get("/api/persons");
59110
59221
  const persons = [];
@@ -59116,7 +59227,7 @@ async function loadDecryptedPersons(client, crypto3) {
59116
59227
  } catch {}
59117
59228
  }
59118
59229
  if (name)
59119
- persons.push({ id: p2.id, name });
59230
+ persons.push({ id: p2.id, name, upiHandles: p2.upiHandles ?? [] });
59120
59231
  }
59121
59232
  return persons;
59122
59233
  }
@@ -59156,10 +59267,15 @@ function isPersonLikeNarration(description) {
59156
59267
  async function backfillPersons(client, crypto3, opts = {}) {
59157
59268
  const existing = await loadDecryptedPersons(client, crypto3);
59158
59269
  const personIdByNorm = new Map;
59159
- for (const p2 of existing)
59270
+ const personIdByHandle = new Map;
59271
+ for (const p2 of existing) {
59160
59272
  personIdByNorm.set(normalizePersonName(p2.name), p2.id);
59273
+ for (const h2 of p2.upiHandles)
59274
+ personIdByHandle.set(normalizeUpiHandle(h2), p2.id);
59275
+ }
59161
59276
  const { rows, truncated } = await fetchAllRowsPaged(client, crypto3, { unresolved: "true" });
59162
59277
  const candidates = rows.filter((r2) => !r2.merchantId && !r2.personId && !r2.isTransfer && !r2.isIgnored);
59278
+ const directLinks = [];
59163
59279
  const groups = new Map;
59164
59280
  for (const t2 of candidates) {
59165
59281
  const { name, upiId, paymentMethod } = parseNarration(t2.description);
@@ -59167,6 +59283,11 @@ async function backfillPersons(client, crypto3, opts = {}) {
59167
59283
  continue;
59168
59284
  if (classifyEntity({ paymentMethod, extractedName: name }) !== "person")
59169
59285
  continue;
59286
+ const handlePersonId = upiId ? personIdByHandle.get(normalizeUpiHandle(upiId)) : undefined;
59287
+ if (handlePersonId) {
59288
+ directLinks.push({ transactionId: t2.id, personId: handlePersonId, upiHandle: upiId });
59289
+ continue;
59290
+ }
59170
59291
  const norm = normalizePersonName(name);
59171
59292
  if (!norm)
59172
59293
  continue;
@@ -59182,7 +59303,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
59182
59303
  }
59183
59304
  const plan = [];
59184
59305
  const newGroups = [];
59185
- let reused = 0;
59306
+ const reusedPersonIds = new Set;
59186
59307
  for (const [norm, g2] of groups) {
59187
59308
  const upiHandles = [...g2.upiHandles];
59188
59309
  const isReuse = personIdByNorm.has(norm);
@@ -59194,11 +59315,34 @@ async function backfillPersons(client, crypto3, opts = {}) {
59194
59315
  action: isReuse ? "reuse" : "create"
59195
59316
  });
59196
59317
  if (isReuse)
59197
- reused++;
59318
+ reusedPersonIds.add(personIdByNorm.get(norm));
59198
59319
  else
59199
59320
  newGroups.push({ norm, displayName: g2.displayName, upiHandles });
59200
59321
  }
59201
- const hash2 = planHash([...groups.values()].flatMap((g2) => g2.txnIds));
59322
+ const directByPerson = new Map;
59323
+ for (const d2 of directLinks) {
59324
+ const e2 = directByPerson.get(d2.personId) ?? { txnIds: [], handles: new Set };
59325
+ e2.txnIds.push(d2.transactionId);
59326
+ if (d2.upiHandle)
59327
+ e2.handles.add(d2.upiHandle);
59328
+ directByPerson.set(d2.personId, e2);
59329
+ }
59330
+ for (const [personId, e2] of directByPerson) {
59331
+ const person = existing.find((p2) => p2.id === personId);
59332
+ plan.push({
59333
+ name: person?.name ?? personId,
59334
+ normalized: person ? normalizePersonName(person.name) : personId,
59335
+ txnCount: e2.txnIds.length,
59336
+ upiHandles: [...e2.handles],
59337
+ action: "reuse"
59338
+ });
59339
+ reusedPersonIds.add(personId);
59340
+ }
59341
+ const reused = reusedPersonIds.size;
59342
+ const hash2 = planHash([
59343
+ ...[...groups.values()].flatMap((g2) => g2.txnIds),
59344
+ ...directLinks.map((d2) => d2.transactionId)
59345
+ ]);
59202
59346
  if (!opts.dryRun && opts.expectedPlanHash !== undefined && opts.expectedPlanHash !== hash2) {
59203
59347
  return {
59204
59348
  dryRun: false,
@@ -59240,6 +59384,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
59240
59384
  for (const txnId of g2.txnIds)
59241
59385
  links.push({ transactionId: txnId, personId });
59242
59386
  }
59387
+ links.push(...directLinks);
59243
59388
  }
59244
59389
  if (!opts.dryRun && links.length > 0) {
59245
59390
  const CHUNK = 25;
@@ -59380,7 +59525,13 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59380
59525
  }
59381
59526
  for (const g2 of hits) {
59382
59527
  claimed.add(g2);
59383
- plan.push({ group: g2, categoryId: cat.id, slug: cat.slug, learn: a2.learn !== false });
59528
+ plan.push({
59529
+ group: g2,
59530
+ categoryId: cat.id,
59531
+ slug: cat.slug,
59532
+ learn: a2.learn !== false,
59533
+ confirmMerchant: !!a2.confirmMerchant
59534
+ });
59384
59535
  }
59385
59536
  }
59386
59537
  result.planHash = planHash(plan.flatMap((p2) => p2.group.rows.map((r2) => r2.id)));
@@ -59414,10 +59565,10 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59414
59565
  const learnPlans = [];
59415
59566
  const directPlans = [];
59416
59567
  for (const p2 of plan) {
59417
- const { group, learn } = p2;
59568
+ const { group, learn, confirmMerchant } = p2;
59418
59569
  if (!learn || group.pattern === UNPARSED) {
59419
59570
  directPlans.push({ p: p2, reason: learn ? UNPARSED : undefined });
59420
- } else if (crypto3 && group.rows.some((r2) => isPersonLikeNarration(r2.description))) {
59571
+ } else if (crypto3 && !confirmMerchant && group.rows.some((r2) => isPersonLikeNarration(r2.description))) {
59421
59572
  directPlans.push({ p: p2, reason: `${group.pattern} (private mode: person-like)` });
59422
59573
  } else if (taught.has(group.pattern) && taught.get(group.pattern) !== p2.categoryId) {
59423
59574
  directPlans.push({ p: p2, reason: `${group.pattern} (conflicting category for same pattern)` });
@@ -59723,7 +59874,8 @@ function transactionTools(client, crypto3) {
59723
59874
  pattern: exports_external.string().describe("Exactly as returned"),
59724
59875
  type: exports_external.enum(["debit", "credit"]).optional(),
59725
59876
  categorySlug: exports_external.string(),
59726
- learn: exports_external.boolean().optional().describe("false = no rule (short/generic patterns)")
59877
+ learn: exports_external.boolean().optional().describe("false = no rule (short/generic patterns)"),
59878
+ 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
59879
  })).min(1).max(100),
59728
59880
  startDate: exports_external.string().optional().describe("Same range as the listing"),
59729
59881
  endDate: exports_external.string().optional(),
@@ -60809,7 +60961,14 @@ function scrubCiphertext(value) {
60809
60961
  }
60810
60962
  async function runTool(tool, args, mcp) {
60811
60963
  const input2 = tool.input ? await tool.input.parseAsync(args) : args;
60812
- const ctx = { tool: tool.name, signal: mcp.mcpReq.signal, mcp };
60964
+ const ctx = {
60965
+ tool: tool.name,
60966
+ signal: mcp.mcpReq.signal,
60967
+ mcp,
60968
+ confirmSupport: { getClientCapabilities: () => {
60969
+ return;
60970
+ } }
60971
+ };
60813
60972
  return tool.handler(input2, ctx);
60814
60973
  }
60815
60974
  function jsonContents(uri, value) {
@@ -60844,7 +61003,7 @@ function registerResources2(server2, tools) {
60844
61003
  // package.json
60845
61004
  var package_default = {
60846
61005
  name: "paisa-mcp",
60847
- version: "0.4.1",
61006
+ version: "0.4.2",
60848
61007
  repository: {
60849
61008
  type: "git",
60850
61009
  url: "git+https://github.com/nimit9/paisa.git",
@@ -60873,7 +61032,7 @@ var package_default = {
60873
61032
  "@modelcontextprotocol/server": "^2.1.0",
60874
61033
  "@nimit9/signet-ai": "^0.1.11",
60875
61034
  "@nimit9/signet-lib": "^0.1.15",
60876
- "@nimit9/signet-server": "^0.2.6",
61035
+ "@nimit9/signet-server": "^0.2.7",
60877
61036
  "@paisa/parsers": "workspace:*",
60878
61037
  "@paisa/reconciliation": "workspace:*",
60879
61038
  "@paisa/types": "workspace:*",
@@ -60899,17 +61058,14 @@ function paisaErrorResult(err) {
60899
61058
  content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }]
60900
61059
  });
60901
61060
  }
61061
+ var CONFIRM_KEY2 = randomBytes(32);
60902
61062
  function buildServer(client, crypto3) {
60903
- let initializeCapabilities = () => {
60904
- return;
60905
- };
60906
61063
  const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)).map((t2) => {
60907
- const caps = () => initializeCapabilities();
60908
61064
  const describe3 = CONFIRMED_TOOLS[t2.name];
60909
61065
  if (describe3)
60910
- return withConfirmation(t2, describe3, caps);
61066
+ return withConfirmation(t2, describe3);
60911
61067
  const count = BULK_CONFIRMED_TOOLS[t2.name];
60912
- return count ? withBulkConfirmation(t2, count, caps) : t2;
61068
+ return count ? withBulkConfirmation(t2, count) : t2;
60913
61069
  }));
60914
61070
  for (const t2 of tools)
60915
61071
  if (t2.input)
@@ -60918,9 +61074,9 @@ function buildServer(client, crypto3) {
60918
61074
  name: "paisa",
60919
61075
  version: VERSION2,
60920
61076
  tools,
60921
- errorResult: paisaErrorResult
61077
+ errorResult: paisaErrorResult,
61078
+ confirm: { key: CONFIRM_KEY2 }
60922
61079
  });
60923
- initializeCapabilities = () => server2.server.getClientCapabilities();
60924
61080
  registerResources2(server2, tools);
60925
61081
  registerPrompts2(server2);
60926
61082
  return server2;
@@ -60947,5 +61103,5 @@ main().catch((err) => {
60947
61103
  process.exit(1);
60948
61104
  });
60949
61105
 
60950
- //# debugId=882108748FF621AF64756E2164756E21
61106
+ //# debugId=24F552E300AC132164756E2164756E21
60951
61107
  //# sourceMappingURL=index.js.map