paisa-mcp 0.0.20 → 0.0.21

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
@@ -28092,7 +28092,7 @@ function finalize(ctx, schema) {
28092
28092
  result.$schema = "http://json-schema.org/draft-07/schema#";
28093
28093
  } else if (ctx.target === "draft-04") {
28094
28094
  result.$schema = "http://json-schema.org/draft-04/schema#";
28095
- } else if (ctx.target === "openapi-3.0") {} else {}
28095
+ } else if (ctx.target === "openapi-3.0") {}
28096
28096
  if (ctx.external?.uri) {
28097
28097
  const id = ctx.external.registry.get(schema)?.id;
28098
28098
  if (!id)
@@ -28340,7 +28340,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
28340
28340
  if (val === undefined) {
28341
28341
  if (ctx.unrepresentable === "throw") {
28342
28342
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
28343
- } else {}
28343
+ }
28344
28344
  } else if (typeof val === "bigint") {
28345
28345
  if (ctx.unrepresentable === "throw") {
28346
28346
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -42773,7 +42773,7 @@ function registerHeatmapTools(server, client) {
42773
42773
  });
42774
42774
  }
42775
42775
 
42776
- // src/tools/household-learning.ts
42776
+ // src/lib/learning-bag.ts
42777
42777
  var EMPTY_BAG = {
42778
42778
  merchantAliases: [],
42779
42779
  personAliases: [],
@@ -42787,12 +42787,11 @@ async function loadBag(client) {
42787
42787
  return { bag: res.data.bag, version: res.data.version };
42788
42788
  }
42789
42789
  async function saveBag(client, bag, expectedVersion) {
42790
- const res = await client.put("/api/household-learning", {
42791
- bag,
42792
- expectedVersion
42793
- });
42790
+ const res = await client.put("/api/household-learning", { bag, expectedVersion });
42794
42791
  return { version: res.data.version };
42795
42792
  }
42793
+
42794
+ // src/tools/household-learning.ts
42796
42795
  function registerHouseholdLearningTools(server, client, _crypto) {
42797
42796
  server.registerTool("list_learning", {
42798
42797
  description: "List a slice of what the user has taught the categorizer for this household: " + "merchant->category rules, person->category rules, merchant aliases, or person aliases.",
@@ -43026,309 +43025,630 @@ function registerInvestmentTools(server, client) {
43026
43025
  });
43027
43026
  }
43028
43027
 
43029
- // src/tools/merchants.ts
43030
- async function categorySlugToId(client) {
43031
- const res = await client.get("/api/categories");
43032
- const map2 = new Map;
43033
- for (const root of res.data ?? []) {
43034
- if (root.slug)
43035
- map2.set(root.slug, root.id);
43036
- for (const child of root.children ?? []) {
43037
- if (child.slug)
43038
- map2.set(child.slug, child.id);
43039
- }
43028
+ // ../../node_modules/.bun/@typesafe-ai+sdk@0.6.0/node_modules/@typesafe-ai/sdk/dist/index.mjs
43029
+ var requestIdFrom = (headers) => headers.get("x-typesafe-request-id") ?? undefined;
43030
+ var APIPromise = class APIPromise2 extends Promise {
43031
+ #responsePromise;
43032
+ #parseResponse;
43033
+ #parsed;
43034
+ constructor(responsePromise, parseResponse) {
43035
+ super((resolve) => resolve(undefined));
43036
+ this.#responsePromise = responsePromise;
43037
+ this.#parseResponse = parseResponse;
43040
43038
  }
43041
- return map2;
43042
- }
43043
- function registerMerchantTools(server, client, crypto3) {
43044
- server.registerTool("upsert_merchant", {
43045
- description: "Create or update a merchant in the dictionary. Omit `id` to add a new merchant (future transactions from it are auto-categorised); pass `id` to update an existing merchant's name, category, or flags.",
43046
- inputSchema: {
43047
- id: exports_external.string().optional().describe("Merchant UUID — pass to update, omit to create"),
43048
- rawId: exports_external.string().optional().describe("Raw identifier — UPI ID or normalised name (required when creating)"),
43049
- cleanName: exports_external.string().optional().describe('Human-friendly name (e.g. "DMart")'),
43050
- categorySlug: exports_external.string().optional().describe("Category slug, e.g. 'food_delivery'"),
43051
- isPerson: exports_external.boolean().optional(),
43052
- isRecurring: exports_external.boolean().optional(),
43053
- notes: exports_external.string().optional()
43054
- }
43055
- }, async ({ id, categorySlug, ...rest }) => {
43056
- const payload = { ...rest };
43057
- if (categorySlug) {
43058
- const slugMap = await categorySlugToId(client);
43059
- const categoryId = slugMap.get(categorySlug);
43060
- if (!categoryId) {
43061
- throw new Error(`Unknown categorySlug "${categorySlug}". Use list_categories to see valid slugs.`);
43039
+ asResponse() {
43040
+ return this.#responsePromise;
43041
+ }
43042
+ async withResponse() {
43043
+ const [data, response] = await Promise.all([this.#parse(), this.#responsePromise]);
43044
+ return {
43045
+ data,
43046
+ response,
43047
+ requestId: requestIdFrom(response.headers)
43048
+ };
43049
+ }
43050
+ map(fn) {
43051
+ return new APIPromise2(this.#responsePromise, () => this.#parse().then(fn));
43052
+ }
43053
+ #parse() {
43054
+ this.#parsed ??= this.#responsePromise.then(this.#parseResponse);
43055
+ return this.#parsed;
43056
+ }
43057
+ then(onfulfilled, onrejected) {
43058
+ return this.#parse().then(onfulfilled, onrejected);
43059
+ }
43060
+ catch(onrejected) {
43061
+ return this.#parse().catch(onrejected);
43062
+ }
43063
+ finally(onfinally) {
43064
+ return this.#parse().finally(onfinally);
43065
+ }
43066
+ };
43067
+ var ENV = {
43068
+ apiKey: "TYPESAFE_API_KEY",
43069
+ baseURL: "TYPESAFE_BASE_URL",
43070
+ defaultModel: "TYPESAFE_DEFAULT_MODEL",
43071
+ logLevel: "TYPESAFE_LOG_LEVEL"
43072
+ };
43073
+ var readEnv = (name) => {
43074
+ if (typeof process === "undefined" || !process.env)
43075
+ return;
43076
+ return process.env[name]?.trim() || undefined;
43077
+ };
43078
+ var fromCodeOrEnv = (fromCode, envVar) => fromCode ?? readEnv(envVar);
43079
+ var range = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
43080
+ var DEFAULT_RETRY_POLICY = {
43081
+ maxRetries: 2,
43082
+ backoffInitialMs: 500,
43083
+ backoffMaxMs: 5000,
43084
+ backoffJitter: 0.25,
43085
+ httpStatuses: /* @__PURE__ */ new Set([
43086
+ 408,
43087
+ 429,
43088
+ ...range(500, 600)
43089
+ ]),
43090
+ respectRetryAfter: true,
43091
+ maxRetryAfterMs: 60000,
43092
+ apiConnectionError: true,
43093
+ apiTimeoutError: true
43094
+ };
43095
+ DEFAULT_RETRY_POLICY.maxRetries;
43096
+ var isRetryableStatus = (status, policy = DEFAULT_RETRY_POLICY) => policy.httpStatuses.has(status);
43097
+ var parseRetryAfter = (headers, now = Date.now()) => {
43098
+ const ms = Number(headers.get("retry-after-ms"));
43099
+ if (headers.has("retry-after-ms") && Number.isFinite(ms) && ms >= 0)
43100
+ return ms;
43101
+ const raw = headers.get("retry-after");
43102
+ if (raw === null)
43103
+ return;
43104
+ const seconds = Number(raw);
43105
+ if (Number.isFinite(seconds))
43106
+ return seconds >= 0 ? seconds * 1000 : undefined;
43107
+ const date4 = Date.parse(raw);
43108
+ if (!Number.isNaN(date4))
43109
+ return Math.max(0, date4 - now);
43110
+ };
43111
+ var retryDelayMs = (attempt, headers, policy = DEFAULT_RETRY_POLICY, random = Math.random) => {
43112
+ if (policy.respectRetryAfter && headers !== undefined) {
43113
+ const retryAfter = parseRetryAfter(headers);
43114
+ if (retryAfter !== undefined && retryAfter <= policy.maxRetryAfterMs)
43115
+ return retryAfter;
43116
+ }
43117
+ const exponential = Math.min(policy.backoffInitialMs * 2 ** attempt, policy.backoffMaxMs);
43118
+ return Math.round(exponential * (1 - random() * policy.backoffJitter));
43119
+ };
43120
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
43121
+ if (signal?.aborted)
43122
+ return reject(signal.reason);
43123
+ const onAbort = () => {
43124
+ clearTimeout(timer);
43125
+ reject(signal?.reason);
43126
+ };
43127
+ const timer = setTimeout(() => {
43128
+ signal?.removeEventListener("abort", onAbort);
43129
+ resolve();
43130
+ }, ms);
43131
+ signal?.addEventListener("abort", onAbort, { once: true });
43132
+ });
43133
+ var TypeSafeError = class extends Error {
43134
+ constructor(message, options) {
43135
+ super(message, options);
43136
+ this.name = new.target.name;
43137
+ }
43138
+ };
43139
+ var isRecord = (value) => typeof value === "object" && value !== null;
43140
+ var extractMessage = (body) => {
43141
+ if (typeof body === "string")
43142
+ return body || undefined;
43143
+ if (!isRecord(body))
43144
+ return;
43145
+ const { error: error2, message, detail } = body;
43146
+ if (typeof error2 === "string")
43147
+ return error2;
43148
+ if (isRecord(error2) && typeof error2.message === "string")
43149
+ return error2.message;
43150
+ if (typeof message === "string")
43151
+ return message;
43152
+ if (typeof detail === "string")
43153
+ return detail;
43154
+ if (isRecord(detail) && typeof detail.message === "string")
43155
+ return detail.message;
43156
+ if (Array.isArray(detail))
43157
+ return describeValidationErrors(detail);
43158
+ };
43159
+ var describeValidationErrors = (errors4) => {
43160
+ const parts = errors4.flatMap((e) => {
43161
+ if (!isRecord(e) || typeof e.msg !== "string")
43162
+ return [];
43163
+ const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x !== "body").join(".") : "";
43164
+ return [loc ? `${loc}: ${e.msg}` : e.msg];
43165
+ });
43166
+ return parts.length > 0 ? parts.join("; ") : undefined;
43167
+ };
43168
+ var MAX_RAW_BODY_IN_MESSAGE = 200;
43169
+ var APIError = class APIError2 extends TypeSafeError {
43170
+ status;
43171
+ headers;
43172
+ body;
43173
+ requestId;
43174
+ constructor(status, body, headers, message) {
43175
+ super(message ?? APIError2.describe(status, body));
43176
+ this.status = status;
43177
+ this.body = body;
43178
+ this.headers = headers;
43179
+ this.requestId = requestIdFrom(headers);
43180
+ }
43181
+ static describe(status, body) {
43182
+ const detail = extractMessage(body);
43183
+ if (detail)
43184
+ return `${status} ${detail}`;
43185
+ if (body === undefined)
43186
+ return `${status} status code (no body)`;
43187
+ const raw = typeof body === "string" ? body : JSON.stringify(body);
43188
+ return `${status} ${raw.length > MAX_RAW_BODY_IN_MESSAGE ? `${raw.slice(0, MAX_RAW_BODY_IN_MESSAGE)}…` : raw}`;
43189
+ }
43190
+ static fromResponse(status, body, headers) {
43191
+ if (status === 400)
43192
+ return new BadRequestError(status, body, headers);
43193
+ if (status === 401)
43194
+ return new AuthenticationError(status, body, headers);
43195
+ if (status === 403)
43196
+ return new PermissionDeniedError(status, body, headers);
43197
+ if (status === 404)
43198
+ return new NotFoundError(status, body, headers);
43199
+ if (status === 422)
43200
+ return new UnprocessableEntityError(status, body, headers);
43201
+ if (status === 429)
43202
+ return new RateLimitError(status, body, headers);
43203
+ if (status >= 500)
43204
+ return new InternalServerError(status, body, headers);
43205
+ return new APIError2(status, body, headers);
43206
+ }
43207
+ };
43208
+ var BadRequestError = class extends APIError {
43209
+ };
43210
+ var AuthenticationError = class extends APIError {
43211
+ };
43212
+ var PermissionDeniedError = class extends APIError {
43213
+ };
43214
+ var NotFoundError = class extends APIError {
43215
+ };
43216
+ var UnprocessableEntityError = class extends APIError {
43217
+ };
43218
+ var RateLimitError = class extends APIError {
43219
+ retryAfterMs = parseRetryAfter(this.headers);
43220
+ };
43221
+ var InternalServerError = class extends APIError {
43222
+ };
43223
+ var APIConnectionError = class extends TypeSafeError {
43224
+ constructor(message = "Connection error.", options) {
43225
+ super(message, options);
43226
+ }
43227
+ };
43228
+ var APITimeoutError = class extends APIConnectionError {
43229
+ timeoutMs;
43230
+ constructor(timeoutMs, options) {
43231
+ super(`Request timed out after ${timeoutMs}ms.`, options);
43232
+ this.timeoutMs = timeoutMs;
43233
+ }
43234
+ };
43235
+ var APIUserAbortError = class extends TypeSafeError {
43236
+ constructor(message = "Request was aborted.", options) {
43237
+ super(message, options);
43238
+ }
43239
+ };
43240
+ var LOG_LEVELS = [
43241
+ "debug",
43242
+ "info",
43243
+ "warn",
43244
+ "error",
43245
+ "off"
43246
+ ];
43247
+ var DEFAULT_LOG_LEVEL = "warn";
43248
+ var isLogLevel = (value) => LOG_LEVELS.includes(value);
43249
+ var parseLogLevel = (value, source) => {
43250
+ if (isLogLevel(value))
43251
+ return value;
43252
+ throw new TypeSafeError(`Invalid log level "${value}" from ${source}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
43253
+ };
43254
+ var PREFIX = "[typesafe-sdk]";
43255
+ var consoleLogger = {
43256
+ debug: (message, ...args) => console.debug(`${PREFIX} ${message}`, ...args),
43257
+ info: (message, ...args) => console.info(`${PREFIX} ${message}`, ...args),
43258
+ warn: (message, ...args) => console.warn(`${PREFIX} ${message}`, ...args),
43259
+ error: (message, ...args) => console.error(`${PREFIX} ${message}`, ...args)
43260
+ };
43261
+ var RANK = {
43262
+ debug: 0,
43263
+ info: 1,
43264
+ warn: 2,
43265
+ error: 3,
43266
+ off: 4
43267
+ };
43268
+ var drop = () => {};
43269
+ var withLevel = (sink, level) => {
43270
+ const enabled = (at) => RANK[at] >= RANK[level];
43271
+ return {
43272
+ debug: enabled("debug") ? (message, ...args) => sink.debug(message, ...args) : drop,
43273
+ info: enabled("info") ? (message, ...args) => sink.info(message, ...args) : drop,
43274
+ warn: enabled("warn") ? (message, ...args) => sink.warn(message, ...args) : drop,
43275
+ error: enabled("error") ? (message, ...args) => sink.error(message, ...args) : drop
43276
+ };
43277
+ };
43278
+ var KEY_HEADERS = /* @__PURE__ */ new Set([
43279
+ "authorization",
43280
+ "proxy-authorization",
43281
+ "x-api-key"
43282
+ ]);
43283
+ var OPAQUE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
43284
+ var redactKey = (value) => {
43285
+ const [scheme, secret] = value.includes(" ") ? value.split(/\s+/, 2) : [undefined, value];
43286
+ const tail = secret && secret.length > 8 ? secret.slice(-4) : "";
43287
+ return `${scheme ? `${scheme} ` : ""}***${tail}`;
43288
+ };
43289
+ var redact = (name, value) => {
43290
+ const lower = name.toLowerCase();
43291
+ if (KEY_HEADERS.has(lower))
43292
+ return redactKey(value);
43293
+ if (OPAQUE_HEADERS.has(lower))
43294
+ return "***";
43295
+ return value;
43296
+ };
43297
+ var redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, redact(name, value)]));
43298
+ var choice = (instructions, criteria) => {
43299
+ if (Array.isArray(criteria))
43300
+ throw new TypeSafeError("Choice criteria must be a map of labels to descriptions, not a list.");
43301
+ return {
43302
+ type: "choice",
43303
+ instructions,
43304
+ criteria
43305
+ };
43306
+ };
43307
+ var validateQuestions = (questions) => {
43308
+ if (Object.keys(questions).length === 0)
43309
+ throw new TypeSafeError("At least one question is required.");
43310
+ for (const [name, question] of Object.entries(questions)) {
43311
+ if (question.type !== "score")
43312
+ continue;
43313
+ if (!Array.isArray(question.criteria))
43314
+ throw new TypeSafeError(`Score question "${name}" has criteria that are not a list; score criteria must be a list of descriptions indexed by score from zero.`);
43315
+ if (question.criteria.length < 2)
43316
+ throw new TypeSafeError(`Score question "${name}" has ${question.criteria.length} criteria; at least two scores are required.`);
43317
+ }
43318
+ };
43319
+ var Models = class {
43320
+ #transport;
43321
+ constructor(transport) {
43322
+ this.#transport = transport;
43323
+ }
43324
+ list(options = {}) {
43325
+ return this.#transport.request("GET", "/v1/models", options).map(unwrapModels);
43326
+ }
43327
+ };
43328
+ var unwrapModels = (wire) => {
43329
+ if (Array.isArray(wire?.models))
43330
+ return wire.models;
43331
+ throw new TypeSafeError("Unexpected response shape from GET /v1/models; expected { models: [...] }.");
43332
+ };
43333
+ var g = globalThis;
43334
+ var isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
43335
+ var describeRuntime = () => {
43336
+ const platform = g.process?.platform && g.process?.arch ? ` (${g.process.platform}; ${g.process.arch})` : "";
43337
+ if (g.Bun?.version)
43338
+ return `bun/${g.Bun.version}${platform}`;
43339
+ if (g.Deno?.version?.deno)
43340
+ return `deno/${g.Deno.version.deno}${platform}`;
43341
+ if (g.EdgeRuntime !== undefined)
43342
+ return "vercel-edge";
43343
+ if (g.navigator?.userAgent === "Cloudflare-Workers")
43344
+ return "cloudflare-workers";
43345
+ if (g.process?.versions?.node)
43346
+ return `node/${g.process.versions.node}${platform}`;
43347
+ if (isBrowser())
43348
+ return "browser";
43349
+ return "unknown";
43350
+ };
43351
+ var VERSION2 = "0.6.0";
43352
+ var missingApiKey = () => {
43353
+ throw new TypeSafeError(`No API key was provided. Pass \`apiKey\` to the TypeSafeClient constructor or set the ${ENV.apiKey} environment variable.`);
43354
+ };
43355
+ var missingFetch = () => {
43356
+ throw new TypeSafeError("No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.");
43357
+ };
43358
+ var refuseBrowser = () => {
43359
+ throw new TypeSafeError("TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
43360
+ };
43361
+ var defaultFetch = (input, init) => globalThis.fetch(input, init);
43362
+ var assertNonNegativeInteger = (name, value) => {
43363
+ if (!Number.isInteger(value) || value < 0)
43364
+ throw new TypeSafeError(`\`${name}\` must be a non-negative integer, got ${String(value)}.`);
43365
+ return value;
43366
+ };
43367
+ var assertPositiveMs = (name, value) => {
43368
+ if (!Number.isFinite(value) || value <= 0)
43369
+ throw new TypeSafeError(`\`${name}\` must be a positive number of milliseconds, got ${String(value)}.`);
43370
+ return value;
43371
+ };
43372
+ var assertNonNegativeMs = (name, value) => {
43373
+ if (!Number.isFinite(value) || value < 0)
43374
+ throw new TypeSafeError(`\`${name}\` must be a non-negative number of milliseconds, got ${String(value)}.`);
43375
+ return value;
43376
+ };
43377
+ var assertFraction = (name, value) => {
43378
+ if (!Number.isFinite(value) || value < 0 || value > 1)
43379
+ throw new TypeSafeError(`\`${name}\` must be between 0 and 1, got ${String(value)}.`);
43380
+ return value;
43381
+ };
43382
+ var assertStatusSet = (name, statuses) => {
43383
+ for (const status of statuses)
43384
+ if (!Number.isInteger(status) || status < 100 || status > 999)
43385
+ throw new TypeSafeError(`\`${name}\` must contain HTTP status codes, got ${String(status)}.`);
43386
+ return statuses;
43387
+ };
43388
+ var resolveRetryPolicy = (base, overrides) => {
43389
+ const o = overrides ?? {};
43390
+ return {
43391
+ maxRetries: o.maxRetries === undefined ? base.maxRetries : assertNonNegativeInteger("retry.maxRetries", o.maxRetries),
43392
+ backoffInitialMs: o.backoffInitialMs === undefined ? base.backoffInitialMs : assertNonNegativeMs("retry.backoffInitialMs", o.backoffInitialMs),
43393
+ backoffMaxMs: o.backoffMaxMs === undefined ? base.backoffMaxMs : assertNonNegativeMs("retry.backoffMaxMs", o.backoffMaxMs),
43394
+ backoffJitter: o.backoffJitter === undefined ? base.backoffJitter : assertFraction("retry.backoffJitter", o.backoffJitter),
43395
+ httpStatuses: new Set(o.httpStatuses === undefined ? base.httpStatuses : assertStatusSet("retry.httpStatuses", o.httpStatuses)),
43396
+ respectRetryAfter: o.respectRetryAfter ?? base.respectRetryAfter,
43397
+ maxRetryAfterMs: o.maxRetryAfterMs === undefined ? base.maxRetryAfterMs : assertNonNegativeMs("retry.maxRetryAfterMs", o.maxRetryAfterMs),
43398
+ apiConnectionError: o.apiConnectionError ?? base.apiConnectionError,
43399
+ apiTimeoutError: o.apiTimeoutError ?? base.apiTimeoutError
43400
+ };
43401
+ };
43402
+ var isRetryableError = (err, policy) => {
43403
+ if (err instanceof APITimeoutError)
43404
+ return policy.apiTimeoutError;
43405
+ if (err instanceof APIConnectionError)
43406
+ return policy.apiConnectionError;
43407
+ return false;
43408
+ };
43409
+ var resolveLogLevel = (fromCode) => {
43410
+ if (fromCode !== undefined)
43411
+ return parseLogLevel(fromCode, "the `logLevel` option");
43412
+ const fromEnv = readEnv(ENV.logLevel);
43413
+ if (fromEnv !== undefined)
43414
+ return parseLogLevel(fromEnv, ENV.logLevel);
43415
+ return DEFAULT_LOG_LEVEL;
43416
+ };
43417
+ var stripTrailingSlashes = (url2) => url2.replace(/\/+$/, "");
43418
+ var mergeHeaders = (...sources) => {
43419
+ const entries = /* @__PURE__ */ new Map;
43420
+ for (const source of sources)
43421
+ for (const [name, value] of Object.entries(source))
43422
+ if (value === undefined)
43423
+ entries.delete(name.toLowerCase());
43424
+ else
43425
+ entries.set(name.toLowerCase(), [name, value]);
43426
+ return Object.fromEntries(entries.values());
43427
+ };
43428
+ var bufferResponse = async (response, signal) => {
43429
+ const reader = response.clone().body?.getReader();
43430
+ if (!reader)
43431
+ return;
43432
+ const cancel = () => {
43433
+ reader.cancel(signal.reason).catch(() => {});
43434
+ response.body?.cancel(signal.reason).catch(() => {});
43435
+ };
43436
+ signal.addEventListener("abort", cancel, { once: true });
43437
+ try {
43438
+ if (signal.aborted)
43439
+ cancel();
43440
+ signal.throwIfAborted();
43441
+ while (!(await reader.read()).done)
43442
+ signal.throwIfAborted();
43443
+ signal.throwIfAborted();
43444
+ } finally {
43445
+ signal.removeEventListener("abort", cancel);
43446
+ reader.releaseLock();
43447
+ }
43448
+ };
43449
+ var RUNTIME = describeRuntime();
43450
+ var TypeSafeClient = class {
43451
+ #apiKey;
43452
+ baseURL;
43453
+ defaultModel;
43454
+ logLevel;
43455
+ logger;
43456
+ retry;
43457
+ timeout;
43458
+ defaultHeaders;
43459
+ fetch;
43460
+ models;
43461
+ #requestCount = 0;
43462
+ constructor(config2 = {}) {
43463
+ if (isBrowser() && !config2.dangerouslyAllowBrowser)
43464
+ refuseBrowser();
43465
+ this.#apiKey = fromCodeOrEnv(config2.apiKey, ENV.apiKey) ?? missingApiKey();
43466
+ this.baseURL = stripTrailingSlashes(fromCodeOrEnv(config2.baseURL, ENV.baseURL) ?? "https://api.typesafe.ai");
43467
+ this.defaultModel = fromCodeOrEnv(config2.defaultModel, ENV.defaultModel) ?? "jev-latest";
43468
+ this.logLevel = resolveLogLevel(config2.logLevel);
43469
+ this.logger = withLevel(config2.logger ?? consoleLogger, this.logLevel);
43470
+ this.retry = resolveRetryPolicy(DEFAULT_RETRY_POLICY, config2.retry);
43471
+ this.timeout = assertPositiveMs("timeout", config2.timeout ?? 1e4);
43472
+ this.defaultHeaders = { ...config2.defaultHeaders };
43473
+ if (config2.fetch === undefined && typeof globalThis.fetch !== "function")
43474
+ missingFetch();
43475
+ this.fetch = config2.fetch ?? defaultFetch;
43476
+ const transport = {
43477
+ request: (method, path, options) => this.#request(method, path, options),
43478
+ defaultModel: this.defaultModel
43479
+ };
43480
+ this.models = new Models(transport);
43481
+ }
43482
+ systemOne(request, options = {}) {
43483
+ validateQuestions(request.questions);
43484
+ const body = {
43485
+ ...request,
43486
+ model: request.model ?? this.defaultModel
43487
+ };
43488
+ return this.#request("POST", "/v1/systemone", {
43489
+ ...options,
43490
+ body
43491
+ });
43492
+ }
43493
+ #request(method, path, options = {}) {
43494
+ const resolved = {
43495
+ method,
43496
+ path,
43497
+ body: options.body,
43498
+ headers: mergeHeaders(this.defaultHeaders, options.headers ?? {}),
43499
+ signal: options.signal,
43500
+ timeout: options.timeout === undefined ? this.timeout : assertPositiveMs("timeout", options.timeout),
43501
+ retry: resolveRetryPolicy(this.retry, options.retry)
43502
+ };
43503
+ const tag = `#${++this.#requestCount} ${method} ${path}`;
43504
+ return new APIPromise(this.fetchWithRetries(tag, resolved), async (res) => {
43505
+ const parsed = await parseBody(res);
43506
+ this.logger.debug(`${tag} <- body`, parsed);
43507
+ return parsed;
43508
+ });
43509
+ }
43510
+ async fetchWithRetries(tag, req) {
43511
+ const url2 = `${this.baseURL}${req.path}`;
43512
+ const headers = mergeHeaders(req.headers, {
43513
+ Authorization: `Bearer ${this.#apiKey}`,
43514
+ Accept: "application/json",
43515
+ "User-Agent": `typesafe-sdk/${VERSION2}`,
43516
+ "X-TypeSafe-SDK": `typesafe-sdk/${VERSION2}`,
43517
+ "X-TypeSafe-Runtime": RUNTIME,
43518
+ "Content-Type": req.body === undefined ? undefined : "application/json",
43519
+ "X-TypeSafe-Retry-Count": undefined
43520
+ });
43521
+ const body = req.body === undefined ? undefined : JSON.stringify(req.body);
43522
+ for (let attempt = 0;; attempt++) {
43523
+ const retriesLeft = req.retry.maxRetries - attempt;
43524
+ const attemptHeaders = attempt === 0 ? headers : {
43525
+ ...headers,
43526
+ "X-TypeSafe-Retry-Count": String(attempt)
43527
+ };
43528
+ this.logger.debug(`${tag} -> ${url2}`, {
43529
+ headers: redactHeaders(attemptHeaders),
43530
+ body: req.body
43531
+ });
43532
+ const started = Date.now();
43533
+ let res;
43534
+ try {
43535
+ res = await this.attempt(tag, url2, {
43536
+ method: req.method,
43537
+ headers: attemptHeaders,
43538
+ body
43539
+ }, req);
43540
+ } catch (err) {
43541
+ if (err instanceof APIUserAbortError || retriesLeft <= 0)
43542
+ throw err;
43543
+ if (!isRetryableError(err, req.retry))
43544
+ throw err;
43545
+ await this.backOff(tag, attempt, retriesLeft, err.message, undefined, req);
43546
+ continue;
43062
43547
  }
43063
- payload.categoryId = categoryId;
43064
- }
43065
- const data = id ? await client.patch(`/api/merchants/${id}`, payload) : await client.post("/api/merchants", payload);
43066
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43067
- });
43068
- server.registerTool("list_merchants", {
43069
- description: "List merchants in the dictionary with their categories and transaction counts. Pass `rawId` to look up a single merchant by its raw ID (UPI ID or normalised name) instead of listing all.",
43070
- inputSchema: {
43071
- rawId: exports_external.string().optional().describe("Raw identifier to look up a single merchant; omit to list all")
43548
+ const requestId = requestIdFrom(res.headers);
43549
+ this.logger.info(`${tag} <- ${res.status} in ${Date.now() - started}ms${requestId ? ` (request ${requestId})` : ""}`);
43550
+ if (res.ok)
43551
+ return res;
43552
+ const errorBody = await parseBody(res);
43553
+ this.logger.debug(`${tag} <- error body`, errorBody);
43554
+ const error2 = APIError.fromResponse(res.status, errorBody, res.headers);
43555
+ if (retriesLeft <= 0 || !isRetryableStatus(res.status, req.retry))
43556
+ throw error2;
43557
+ await this.backOff(tag, attempt, retriesLeft, `${res.status}`, res.headers, req);
43072
43558
  }
43073
- }, async ({ rawId }) => {
43074
- if (crypto3 && rawId) {
43075
- const data2 = await client.get("/api/merchants");
43076
- if (data2.data)
43077
- await decryptMerchantFields(data2.data, crypto3.key);
43078
- const needle = rawId.toLowerCase();
43079
- const merchants = (data2.data ?? []).filter((m) => String(m.rawId ?? "").toLowerCase() === needle || String(m.cleanName ?? "").toLowerCase().includes(needle));
43080
- return {
43081
- content: [{ type: "text", text: JSON.stringify({ ...data2, data: merchants }) }]
43082
- };
43559
+ }
43560
+ async attempt(tag, url2, init, { signal, timeout }) {
43561
+ const controller = new AbortController;
43562
+ const abortFromCaller = () => controller.abort(signal?.reason);
43563
+ if (signal?.aborted)
43564
+ abortFromCaller();
43565
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
43566
+ let timedOut = false;
43567
+ const timer = setTimeout(() => {
43568
+ timedOut = true;
43569
+ controller.abort();
43570
+ }, timeout);
43571
+ const started = Date.now();
43572
+ const elapsed = () => `${Date.now() - started}ms`;
43573
+ try {
43574
+ const response = await this.fetch(url2, {
43575
+ ...init,
43576
+ signal: controller.signal
43577
+ });
43578
+ await bufferResponse(response, controller.signal);
43579
+ return response;
43580
+ } catch (err) {
43581
+ if (signal?.aborted) {
43582
+ this.logger.info(`${tag} aborted by caller after ${elapsed()}`);
43583
+ throw new APIUserAbortError(undefined, { cause: err });
43584
+ }
43585
+ if (timedOut) {
43586
+ this.logger.info(`${tag} timed out after ${elapsed()}`);
43587
+ throw new APITimeoutError(timeout, { cause: err });
43588
+ }
43589
+ this.logger.info(`${tag} connection error after ${elapsed()}`, err);
43590
+ throw new APIConnectionError(err instanceof Error ? `Connection error: ${err.message}` : undefined, { cause: err });
43591
+ } finally {
43592
+ clearTimeout(timer);
43593
+ signal?.removeEventListener("abort", abortFromCaller);
43083
43594
  }
43084
- const data = rawId ? await client.get("/api/merchants", {
43085
- search: rawId
43086
- }) : await client.get("/api/merchants");
43087
- if (crypto3 && data.data)
43088
- await decryptMerchantFields(data.data, crypto3.key);
43089
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43090
- });
43091
- server.registerTool("delete_merchant", {
43092
- description: "Delete merchant(s) from the dictionary. Provide `id` for a single merchant or `ids` for several. Transactions referencing a deleted merchant have their merchantId cleared. PERMANENT.",
43093
- inputSchema: {
43094
- id: exports_external.string().optional().describe("Single merchant UUID"),
43095
- ids: exports_external.array(exports_external.string()).min(1).optional().describe("Multiple merchant UUIDs (bulk delete)")
43595
+ }
43596
+ async backOff(tag, attempt, retriesLeft, reason, headers, { retry, signal }) {
43597
+ const delay = retryDelayMs(attempt, headers, retry);
43598
+ const nth = attempt + 1;
43599
+ const total = attempt + retriesLeft;
43600
+ this.logger.info(`${tag} retrying in ${delay}ms (retry ${nth}/${total}) after ${reason}`);
43601
+ try {
43602
+ await sleep(delay, signal);
43603
+ } catch (err) {
43604
+ this.logger.info(`${tag} aborted by caller while waiting to retry`);
43605
+ throw new APIUserAbortError(undefined, { cause: err });
43096
43606
  }
43097
- }, async ({ id, ids }) => {
43098
- if (!id && (!ids || ids.length === 0)) {
43099
- throw new Error("Provide either id or ids");
43607
+ }
43608
+ };
43609
+ var parseBody = async (res) => {
43610
+ const text = await res.text();
43611
+ if (text.length === 0)
43612
+ return;
43613
+ if ((res.headers.get("content-type") ?? "").includes("application/json"))
43614
+ try {
43615
+ return JSON.parse(text);
43616
+ } catch {
43617
+ return text;
43100
43618
  }
43101
- const data = ids ? await client.post("/api/merchants/bulk-delete", { ids }) : await client.delete(`/api/merchants/${id}`);
43102
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43103
- });
43104
- }
43619
+ try {
43620
+ return JSON.parse(text);
43621
+ } catch {
43622
+ return text;
43623
+ }
43624
+ };
43105
43625
 
43106
- // src/tools/net-worth.ts
43107
- function registerNetWorthTools(server, client) {
43108
- server.registerTool("get_net_worth", {
43109
- description: "Current net worth snapshot and history (up to 12 months). Includes liquid cash, investments, emergency fund, liabilities.",
43110
- inputSchema: {}
43111
- }, async () => {
43112
- const data = await client.get("/api/net-worth");
43113
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43114
- });
43115
- server.registerTool("update_net_worth", {
43116
- description: "Log a net worth snapshot for a specific month. Upserts — safe to call multiple times for the same month. mode=manual (default) records the provided liquidCash/investments/emergencyFund/liabilities/breakdown values. mode=auto ignores those and auto-calculates net worth from DB (investments + FDs + accounts) — use it when the data is already in the system.",
43117
- inputSchema: {
43118
- mode: exports_external.enum(["manual", "auto"]).default("manual"),
43119
- month: exports_external.number().min(1).max(12),
43120
- year: exports_external.number().min(2020).max(2100),
43121
- liquidCash: exports_external.string().optional(),
43122
- investments: exports_external.string().optional(),
43123
- emergencyFund: exports_external.string().optional(),
43124
- liabilities: exports_external.string().optional(),
43125
- breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional()
43126
- }
43127
- }, async ({ mode, ...body }) => {
43128
- let data;
43129
- if (mode === "auto") {
43130
- data = await client.post("/api/net-worth/auto", { month: body.month, year: body.year });
43131
- } else {
43132
- const liquidCash = parseFloat(body.liquidCash ?? "0");
43133
- const investments = parseFloat(body.investments ?? "0");
43134
- const emergencyFund = parseFloat(body.emergencyFund ?? "0");
43135
- const liabilities = parseFloat(body.liabilities ?? "0");
43136
- const totalNetWorth = (liquidCash + investments + emergencyFund - liabilities).toFixed(2);
43137
- data = await client.post("/api/net-worth", {
43138
- ...body,
43139
- emergencyFund: body.emergencyFund ?? "0",
43140
- totalNetWorth
43141
- });
43626
+ // src/lib/categories.ts
43627
+ async function loadCategoryIndex(client) {
43628
+ const res = await client.get("/api/categories");
43629
+ const bySlug = new Map;
43630
+ const byId = new Map;
43631
+ const parents = new Set;
43632
+ for (const root of res.data ?? []) {
43633
+ if (root.slug && (root.children?.length ?? 0) > 0)
43634
+ parents.add(root.slug);
43635
+ for (const c of [root, ...root.children ?? []]) {
43636
+ if (!c.slug)
43637
+ continue;
43638
+ bySlug.set(c.slug, { id: c.id, type: c.type });
43639
+ byId.set(c.id, { slug: c.slug, type: c.type });
43142
43640
  }
43143
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43144
- });
43641
+ }
43642
+ return { bySlug, byId, parents };
43643
+ }
43644
+ function requireCategory(index, slug) {
43645
+ const hit = index.bySlug.get(slug);
43646
+ if (!hit) {
43647
+ throw new Error(`Unknown categorySlug "${slug}". Valid: ${[...index.bySlug.keys()].sort().join(", ")}`);
43648
+ }
43649
+ return { ...hit, slug };
43145
43650
  }
43146
43651
 
43147
- // src/tools/persons.ts
43148
- function registerPersonTools(server, client, crypto3) {
43149
- server.registerTool("upsert_person", {
43150
- description: "Create or update a person (UPI counterparty / individual) in this household's encrypted directory. Omit `id` to add a new person; pass `id` to update any combination of an existing person's fields.",
43151
- inputSchema: {
43152
- id: exports_external.string().optional().describe("Person UUID — pass to update, omit to create"),
43153
- name: exports_external.string().optional().describe('Display name (e.g. "Priya") — required when creating'),
43154
- upiHandles: exports_external.array(exports_external.string()).optional().describe("UPI ids the person uses"),
43155
- phoneNumbers: exports_external.array(exports_external.string()).optional(),
43156
- relationship: exports_external.string().optional().describe('e.g. "sister", "friend", "landlord"'),
43157
- notes: exports_external.string().optional()
43158
- }
43159
- }, async ({ id, ...updates }) => {
43160
- if (crypto3) {
43161
- if (id) {
43162
- const existing = await client.get(`/api/persons/${id}`);
43163
- const personData = existing.data;
43164
- let current;
43165
- if (personData.cipher) {
43166
- current = await decryptPersonCipher(personData.cipher, crypto3.key);
43167
- } else {
43168
- current = {
43169
- name: personData.name,
43170
- upiHandles: personData.upiHandles,
43171
- phoneNumbers: personData.phoneNumbers,
43172
- relationship: personData.relationship,
43173
- notes: personData.notes
43174
- };
43175
- }
43176
- const merged = {
43177
- name: updates.name ?? current.name,
43178
- upiHandles: updates.upiHandles ?? current.upiHandles,
43179
- phoneNumbers: updates.phoneNumbers ?? current.phoneNumbers,
43180
- relationship: updates.relationship ?? current.relationship ?? null,
43181
- notes: updates.notes ?? current.notes ?? null
43182
- };
43183
- const payload2 = await encryptPersonCipher(merged, crypto3.key);
43184
- const data3 = await client.patch(`/api/persons/${id}`, payload2);
43185
- return { content: [{ type: "text", text: JSON.stringify(data3) }] };
43186
- }
43187
- if (!updates.name)
43188
- throw new Error("name is required when creating a person");
43189
- const fields = {
43190
- name: updates.name,
43191
- upiHandles: updates.upiHandles ?? [],
43192
- phoneNumbers: updates.phoneNumbers ?? [],
43193
- relationship: updates.relationship ?? null,
43194
- notes: updates.notes ?? null
43195
- };
43196
- const payload = await encryptPersonCipher(fields, crypto3.key);
43197
- const data2 = await client.post("/api/persons", payload);
43198
- return { content: [{ type: "text", text: JSON.stringify(data2) }] };
43199
- }
43200
- const data = id ? await client.patch(`/api/persons/${id}`, updates) : await client.post("/api/persons", updates);
43201
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43202
- });
43203
- server.registerTool("list_persons", {
43204
- description: "List all persons in the current household. Pass `id` to get a single person by id instead of listing all.",
43205
- inputSchema: {
43206
- id: exports_external.string().optional().describe("Person UUID to fetch a single person; omit to list all")
43207
- }
43208
- }, async ({ id }) => {
43209
- if (id) {
43210
- const data2 = await client.get(`/api/persons/${id}`);
43211
- if (crypto3 && data2.data.cipher) {
43212
- const fields = await decryptPersonCipher(data2.data.cipher, crypto3.key);
43213
- Object.assign(data2.data, fields, { cipher: null });
43214
- }
43215
- return { content: [{ type: "text", text: JSON.stringify(data2) }] };
43216
- }
43217
- const data = await client.get("/api/persons");
43218
- if (crypto3 && data.data) {
43219
- for (const person of data.data) {
43220
- const cipher = person.cipher;
43221
- if (cipher) {
43222
- try {
43223
- const fields = await decryptPersonCipher(cipher, crypto3.key);
43224
- Object.assign(person, fields, { cipher: null });
43225
- } catch {}
43226
- }
43227
- }
43228
- }
43229
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43230
- });
43231
- server.registerTool("delete_person", {
43232
- description: "Delete a person. Returns 409 if any transactions still reference this person.",
43233
- inputSchema: { id: exports_external.string().describe("Person UUID") }
43234
- }, async ({ id }) => {
43235
- const data = await client.delete(`/api/persons/${id}`);
43236
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43237
- });
43238
- }
43239
-
43240
- // src/tools/profile.ts
43241
- function registerProfileTools(server, client) {
43242
- server.registerTool("get_profile", {
43243
- description: "Get the current user's profile (name, email, owner).",
43244
- inputSchema: {}
43245
- }, async () => {
43246
- const data = await client.get("/api/profile");
43247
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43248
- });
43249
- server.registerTool("update_profile", {
43250
- description: "Update your display name.",
43251
- inputSchema: {
43252
- name: exports_external.string().min(1).describe("Display name")
43253
- }
43254
- }, async (body) => {
43255
- const data = await client.patch("/api/profile", body);
43256
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43257
- });
43258
- }
43259
-
43260
- // src/tools/push.ts
43261
- function registerPushTools(server, client) {
43262
- server.registerTool("list_push_subscriptions", {
43263
- description: "List the household's registered web-push subscriptions (browser/device endpoints that receive alert notifications).",
43264
- inputSchema: {}
43265
- }, async () => {
43266
- const data = await client.get("/api/push/subscriptions");
43267
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43268
- });
43269
- server.registerTool("send_test_push", {
43270
- description: "Send a test web-push notification to all of the household's subscriptions. Use to verify push delivery is working.",
43271
- inputSchema: {}
43272
- }, async () => {
43273
- const data = await client.post("/api/push/test");
43274
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43275
- });
43276
- }
43277
-
43278
- // src/tools/recurring.ts
43279
- function registerRecurringTools(server, client) {
43280
- server.registerTool("list_recurring", {
43281
- description: "List all recurring transactions (subscriptions, rent, salaries).",
43282
- inputSchema: {}
43283
- }, async () => {
43284
- const data = await client.get("/api/recurring");
43285
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43286
- });
43287
- server.registerTool("add_recurring", {
43288
- description: "Add a recurring transaction to track subscriptions or regular income.",
43289
- inputSchema: {
43290
- name: exports_external.string(),
43291
- amount: exports_external.string(),
43292
- type: exports_external.enum(["debit", "credit"]),
43293
- frequency: exports_external.enum(["daily", "weekly", "monthly", "yearly"]),
43294
- dayOfMonth: exports_external.number().optional().describe("For monthly — day of month (1-31)"),
43295
- categorySlug: exports_external.string().optional(),
43296
- owner: exports_external.string(),
43297
- startDate: exports_external.string().optional().describe("YYYY-MM-DD"),
43298
- notes: exports_external.string().optional()
43299
- }
43300
- }, async (body) => {
43301
- const data = await client.post("/api/recurring", body);
43302
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43303
- });
43304
- }
43305
-
43306
- // src/tools/reports.ts
43307
- function registerReportTools(server, client) {
43308
- server.registerTool("generate_monthly_report", {
43309
- description: "Generate a detailed monthly financial report. Computes income, expenses, savings rate, top categories, and insights. Persists to DB.",
43310
- inputSchema: {
43311
- month: exports_external.number().min(1).max(12),
43312
- year: exports_external.number().min(2020).max(2100)
43313
- }
43314
- }, async (body) => {
43315
- const data = await client.post("/api/reports", body);
43316
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43317
- });
43318
- server.registerTool("get_reports", {
43319
- description: "Monthly financial reports for the last N months — income, expenses, savings rate, net worth.",
43320
- inputSchema: {
43321
- months: exports_external.number().optional().describe("Number of months (default: 6)")
43322
- }
43323
- }, async (params) => {
43324
- const data = await client.get("/api/reports", params.months ? { months: params.months } : undefined);
43325
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
43326
- });
43327
- }
43328
-
43329
- // src/tools/transactions.ts
43330
- import { readFile } from "node:fs/promises";
43331
-
43332
43652
  // ../reconciliation/src/aliases.ts
43333
43653
  var MERCHANT_ALIASES = [
43334
43654
  ["Swiggy", ["swiggy", "bundl technologies", "swiggy bundl"]],
@@ -43520,9 +43840,9 @@ var i = function() {
43520
43840
  return _has(u, e3);
43521
43841
  };
43522
43842
  }, SequenceMatcher.prototype.findLongestMatch = function(e2, t2, r2, n2) {
43523
- var o2, a2, i2, s, c, u, l, f, p, h, v, _, d, y, g, m, b, k, j, $, w;
43843
+ var o2, a2, i2, s, c, u, l, f, p, h, v, _, d, y, g2, m, b, k, j, $, w;
43524
43844
  for (o2 = (m = [this.a, this.b, this.b2j, this.isbjunk])[0], a2 = m[1], i2 = m[2], f = m[3], s = (b = [e2, r2, 0])[0], c = b[1], u = b[2], h = {}, l = d = e2;e2 <= t2 ? d < t2 : d > t2; l = e2 <= t2 ? ++d : --d) {
43525
- for (_ = {}, y = 0, g = (k = _has(i2, o2[l]) ? i2[o2[l]] : []).length;y < g; y++)
43845
+ for (_ = {}, y = 0, g2 = (k = _has(i2, o2[l]) ? i2[o2[l]] : []).length;y < g2; y++)
43526
43846
  if (!((p = k[y]) < r2)) {
43527
43847
  if (p >= n2)
43528
43848
  break;
@@ -43540,11 +43860,11 @@ var i = function() {
43540
43860
  u++;
43541
43861
  return [s, c, u];
43542
43862
  }, SequenceMatcher.prototype.getMatchingBlocks = function() {
43543
- var e2, t2, r2, n2, o2, a2, i2, s, c, u, l, f, p, h, v, _, d, y, g, m, b, k, j, $, w, O;
43863
+ var e2, t2, r2, n2, o2, a2, i2, s, c, u, l, f, p, h, v, _, d, y, g2, m, b, k, j, $, w, O;
43544
43864
  if (this.matchingBlocks)
43545
43865
  return this.matchingBlocks;
43546
43866
  for (y = [[0, h = (k = [this.a.length, this.b.length])[0], 0, v = k[1]]], _ = [];y.length; )
43547
- t2 = (j = y.pop())[0], e2 = j[1], n2 = j[2], r2 = j[3], o2 = ($ = g = this.findLongestMatch(t2, e2, n2, r2))[0], s = $[1], (l = $[2]) && (_.push(g), t2 < o2 && n2 < s && y.push([t2, o2, n2, s]), o2 + l < e2 && s + l < r2 && y.push([o2 + l, e2, s + l, r2]));
43867
+ t2 = (j = y.pop())[0], e2 = j[1], n2 = j[2], r2 = j[3], o2 = ($ = g2 = this.findLongestMatch(t2, e2, n2, r2))[0], s = $[1], (l = $[2]) && (_.push(g2), t2 < o2 && n2 < s && y.push([t2, o2, n2, s]), o2 + l < e2 && s + l < r2 && y.push([o2 + l, e2, s + l, r2]));
43548
43868
  for (_.sort(_arrayCmp), a2 = c = f = 0, d = [], m = 0, b = _.length;m < b; m++)
43549
43869
  i2 = (w = _[m])[0], u = w[1], p = w[2], a2 + f === i2 && c + f === u ? f += p : (f && d.push([a2, c, f]), a2 = (O = [i2, u, p])[0], c = O[1], f = O[2]);
43550
43870
  return f && d.push([a2, c, f]), d.push([h, v, 0]), this.matchingBlocks = d;
@@ -43692,11 +44012,11 @@ var v = p || h || Function("return this")();
43692
44012
  var _ = v.Symbol;
43693
44013
  var d = _;
43694
44014
  var y = Object.prototype;
43695
- var g = y.hasOwnProperty;
44015
+ var g2 = y.hasOwnProperty;
43696
44016
  var m = y.toString;
43697
44017
  var b = d ? d.toStringTag : undefined;
43698
44018
  var k = function getRawTag$1(e2) {
43699
- var t2 = g.call(e2, b), r2 = e2[b];
44019
+ var t2 = g2.call(e2, b), r2 = e2[b];
43700
44020
  try {
43701
44021
  e2[b] = undefined;
43702
44022
  var n2 = true;
@@ -44283,8 +44603,8 @@ var Xr = function equalObjects$1(e2, t2, r2, n2, o2, a2) {
44283
44603
  v2 || (v2 = l2 == "constructor");
44284
44604
  }
44285
44605
  if (h2 && !v2) {
44286
- var g2 = e2.constructor, m2 = t2.constructor;
44287
- g2 == m2 || !("constructor" in e2) || !("constructor" in t2) || typeof g2 == "function" && g2 instanceof g2 && typeof m2 == "function" && m2 instanceof m2 || (h2 = false);
44606
+ var g3 = e2.constructor, m2 = t2.constructor;
44607
+ g3 == m2 || !("constructor" in e2) || !("constructor" in t2) || typeof g3 == "function" && g3 instanceof g3 && typeof m2 == "function" && m2 instanceof m2 || (h2 = false);
44288
44608
  }
44289
44609
  return a2.delete(e2), a2.delete(t2), h2;
44290
44610
  };
@@ -44654,15 +44974,15 @@ var qo = function leven2(e2, t2, r2, n2) {
44654
44974
  return 0;
44655
44975
  } else
44656
44976
  f2 = (l2 = r2.wildcards[0].toLowerCase()).charCodeAt(0);
44657
- for (var y2 = 0, g2 = 0;y2 < c2; )
44977
+ for (var y2 = 0, g3 = 0;y2 < c2; )
44658
44978
  a2[y2] = e2.charCodeAt(y2), o2[y2] = ++y2;
44659
44979
  if (i2)
44660
- for (;g2 < u2; )
44661
- for (p2 = t2.charCodeAt(g2), v2 = g2++, h2 = g2, y2 = 0;y2 < c2; y2++)
44980
+ for (;g3 < u2; )
44981
+ for (p2 = t2.charCodeAt(g3), v2 = g3++, h2 = g3, y2 = 0;y2 < c2; y2++)
44662
44982
  _2 = Io.compare(String.fromCharCode(p2), String.fromCharCode(a2[y2])) === 0 || p2 === f2 || a2[y2] === f2 ? v2 : v2 + s2, v2 = o2[y2], h2 = o2[y2] = v2 > h2 ? _2 > h2 ? h2 + 1 : _2 : _2 > v2 ? v2 + 1 : _2;
44663
44983
  else
44664
- for (;g2 < u2; )
44665
- for (p2 = t2.charCodeAt(g2), v2 = g2++, h2 = g2, y2 = 0;y2 < c2; y2++)
44984
+ for (;g3 < u2; )
44985
+ for (p2 = t2.charCodeAt(g3), v2 = g3++, h2 = g3, y2 = 0;y2 < c2; y2++)
44666
44986
  _2 = p2 === a2[y2] || p2 === f2 || a2[y2] === f2 ? v2 : v2 + s2, v2 = o2[y2], h2 = o2[y2] = v2 > h2 ? _2 > h2 ? h2 + 1 : _2 : _2 > v2 ? v2 + 1 : _2;
44667
44987
  return h2;
44668
44988
  }
@@ -44867,7 +45187,7 @@ var Lo = { exports: {} };
44867
45187
  }, n3.isCustomFunc = function(e4) {
44868
45188
  return typeof e4 != "function" || e4.name !== "token_set_ratio" && e4.name !== "partial_token_set_ratio" && e4.name !== "token_sort_ratio" && e4.name !== "partial_token_sort_ratio" && e4.name !== "QRatio" && e4.name !== "WRatio" && e4.name !== "distance" && e4.name !== "partial_ratio";
44869
45189
  }, n3;
44870
- }(Mo._uniq, Mo._uniqWith, l2), y2 = d2.validate, g2 = d2.process_and_sort, m2 = d2.tokenize, b2 = d2.full_process, k2 = d2.clone_and_set_option_defaults, j2 = d2.isCustomFunc, $2 = Lo.exports(k2, p2, QRatio, extract).dedupe;
45190
+ }(Mo._uniq, Mo._uniqWith, l2), y2 = d2.validate, g3 = d2.process_and_sort, m2 = d2.tokenize, b2 = d2.full_process, k2 = d2.clone_and_set_option_defaults, j2 = d2.isCustomFunc, $2 = Lo.exports(k2, p2, QRatio, extract).dedupe;
44871
45191
  function QRatio(e3, t3, r2) {
44872
45192
  var n3 = k2(r2);
44873
45193
  return e3 = n3.normalize ? e3.normalize() : e3, t3 = n3.normalize ? t3.normalize() : t3, e3 = n3.full_process ? b2(e3, n3) : e3, t3 = n3.full_process ? b2(t3, n3) : t3, y2(e3) && y2(t3) ? _ratio(e3, t3, n3) : 0;
@@ -44882,11 +45202,11 @@ var Lo = { exports: {} };
44882
45202
  }
44883
45203
  function token_sort_ratio(e3, t3, r2) {
44884
45204
  var n3 = k2(r2);
44885
- return e3 = n3.normalize ? e3.normalize() : e3, t3 = n3.normalize ? t3.normalize() : t3, e3 = n3.full_process ? b2(e3, n3) : e3, t3 = n3.full_process ? b2(t3, n3) : t3, y2(e3) && y2(t3) ? (n3.proc_sorted || (e3 = g2(e3), t3 = g2(t3)), _ratio(e3, t3, n3)) : 0;
45205
+ return e3 = n3.normalize ? e3.normalize() : e3, t3 = n3.normalize ? t3.normalize() : t3, e3 = n3.full_process ? b2(e3, n3) : e3, t3 = n3.full_process ? b2(t3, n3) : t3, y2(e3) && y2(t3) ? (n3.proc_sorted || (e3 = g3(e3), t3 = g3(t3)), _ratio(e3, t3, n3)) : 0;
44886
45206
  }
44887
45207
  function partial_token_sort_ratio(e3, t3, r2) {
44888
45208
  var n3 = k2(r2);
44889
- return e3 = n3.normalize ? e3.normalize() : e3, t3 = n3.normalize ? t3.normalize() : t3, e3 = n3.full_process ? b2(e3, n3) : e3, t3 = n3.full_process ? b2(t3, n3) : t3, y2(e3) && y2(t3) ? (n3.partial = true, n3.proc_sorted || (e3 = g2(e3), t3 = g2(t3)), _partial_ratio(e3, t3, n3)) : 0;
45209
+ return e3 = n3.normalize ? e3.normalize() : e3, t3 = n3.normalize ? t3.normalize() : t3, e3 = n3.full_process ? b2(e3, n3) : e3, t3 = n3.full_process ? b2(t3, n3) : t3, y2(e3) && y2(t3) ? (n3.partial = true, n3.proc_sorted || (e3 = g3(e3), t3 = g3(t3)), _partial_ratio(e3, t3, n3)) : 0;
44890
45210
  }
44891
45211
  function extract(e3, r2, n3) {
44892
45212
  var o3, a3 = k2(n3);
@@ -44916,7 +45236,7 @@ var Lo = { exports: {} };
44916
45236
  i2 || (a3.astral && a3.normalize && (a3.normalize = false, String.prototype.normalize ? (s3 = true, e3 = e3.normalize()) : typeof console !== undefined && console.warn("Normalization not supported in your environment")), e3 = pre_processor(e3, a3), a3.full_process = false, e3.length === 0 && typeof console !== undefined && console.warn("Processed query is empty string"));
44917
45237
  var c2, u3, l3, h3, v3 = [], _3 = false, d3 = false, y3 = false;
44918
45238
  if (a3.scorer.name === "token_sort_ratio" || a3.scorer.name === "partial_token_sort_ratio") {
44919
- var $3 = g2(e3);
45239
+ var $3 = g3(e3);
44920
45240
  d3 = true;
44921
45241
  } else if (a3.scorer.name === "token_set_ratio" || a3.scorer.name === "partial_token_set_ratio") {
44922
45242
  var w3 = m2(e3, a3);
@@ -44931,7 +45251,7 @@ var Lo = { exports: {} };
44931
45251
  }, h3 = function(e4, t3) {
44932
45252
  return t3[1] - e4[1];
44933
45253
  }), f2(r2, function(t3, r3) {
44934
- a3.tokens = undefined, a3.proc_sorted = false, d3 ? (a3.proc_sorted = true, t3 && t3.proc_sorted ? u3 = t3.proc_sorted : (u3 = pre_processor(s3 ? a3.processor(t3).normalize() : a3.processor(t3), a3), u3 = g2(u3)), c2 = a3.scorer($3, u3, a3)) : y3 ? (u3 = "x", t3 && t3.tokens ? (a3.tokens = [w3, t3.tokens], a3.trySimple && (u3 = pre_processor(a3.processor(t3), a3))) : (u3 = pre_processor(s3 ? a3.processor(t3).normalize() : a3.processor(t3), a3), a3.tokens = [w3, m2(u3, a3)]), c2 = a3.scorer(e3, u3, a3)) : i2 ? (u3 = a3.processor(t3), c2 = a3.scorer(e3, u3, a3)) : (typeof (u3 = pre_processor(s3 ? a3.processor(t3).normalize() : a3.processor(t3), a3)) == "string" && u3.length !== 0 || (_3 = true), c2 = a3.scorer(e3, u3, a3)), c2 > a3.cutoff && (a3.returnObjects ? v3.push({ choice: t3, score: c2, key: r3 }) : v3.push([t3, c2, r3]));
45254
+ a3.tokens = undefined, a3.proc_sorted = false, d3 ? (a3.proc_sorted = true, t3 && t3.proc_sorted ? u3 = t3.proc_sorted : (u3 = pre_processor(s3 ? a3.processor(t3).normalize() : a3.processor(t3), a3), u3 = g3(u3)), c2 = a3.scorer($3, u3, a3)) : y3 ? (u3 = "x", t3 && t3.tokens ? (a3.tokens = [w3, t3.tokens], a3.trySimple && (u3 = pre_processor(a3.processor(t3), a3))) : (u3 = pre_processor(s3 ? a3.processor(t3).normalize() : a3.processor(t3), a3), a3.tokens = [w3, m2(u3, a3)]), c2 = a3.scorer(e3, u3, a3)) : i2 ? (u3 = a3.processor(t3), c2 = a3.scorer(e3, u3, a3)) : (typeof (u3 = pre_processor(s3 ? a3.processor(t3).normalize() : a3.processor(t3), a3)) == "string" && u3.length !== 0 || (_3 = true), c2 = a3.scorer(e3, u3, a3)), c2 > a3.cutoff && (a3.returnObjects ? v3.push({ choice: t3, score: c2, key: r3 }) : v3.push([t3, c2, r3]));
44935
45255
  }), _3 && typeof console !== undefined && console.log("One or more choices were empty. (post-processing if applied)"), a3.limit && typeof a3.limit == "number" && a3.limit > 0 && a3.limit < o3 && !a3.unsorted ? v3 = t2.nlargest(v3, a3.limit, l3) : a3.unsorted || (v3 = v3.sort(h3)), v3;
44936
45256
  }
44937
45257
  function extractAsync(e3, r2, n3, o3) {
@@ -44967,7 +45287,7 @@ var Lo = { exports: {} };
44967
45287
  f3 || (s3.astral && s3.normalize && (s3.normalize = false, String.prototype.normalize ? (h3 = true, e3 = e3.normalize()) : typeof console !== undefined && console.warn("Normalization not supported in your environment")), e3 = pre_processor(e3, s3), s3.full_process = false, e3.length === 0 && typeof console !== undefined && console.warn("Processed query is empty string"));
44968
45288
  var v3, _3, d3, y3, $3, w3 = [], O3 = false, S3 = false, C2 = false;
44969
45289
  if (s3.scorer.name === "token_sort_ratio" || s3.scorer.name === "partial_token_sort_ratio") {
44970
- var z2 = g2(e3);
45290
+ var z2 = g3(e3);
44971
45291
  S3 = true;
44972
45292
  } else if (s3.scorer.name === "token_set_ratio" || s3.scorer.name === "partial_token_set_ratio") {
44973
45293
  var A2 = m2(e3, s3);
@@ -44986,7 +45306,7 @@ var Lo = { exports: {} };
44986
45306
  l3 ? searchLoop(0) : searchLoop(x2[0], 0);
44987
45307
  }
44988
45308
  function searchLoop(n4, p3) {
44989
- (l3 || r2.hasOwnProperty(n4)) && (s3.tokens = undefined, s3.proc_sorted = false, S3 ? (s3.proc_sorted = true, r2[n4] && r2[n4].proc_sorted ? _3 = r2[n4].proc_sorted : (_3 = pre_processor(h3 ? s3.processor(r2[n4]).normalize() : s3.processor(r2[n4]), s3), _3 = g2(_3)), d3 = s3.scorer(z2, _3, s3)) : C2 ? (_3 = "x", r2[n4] && r2[n4].tokens ? (s3.tokens = [A2, r2[n4].tokens], s3.trySimple && (_3 = pre_processor(s3.processor(r2[n4]), s3))) : (_3 = pre_processor(h3 ? s3.processor(r2[n4]).normalize() : s3.processor(r2[n4]), s3), s3.tokens = [A2, m2(_3, s3)]), d3 = s3.scorer(e3, _3, s3)) : f3 ? (_3 = s3.processor(r2[n4]), d3 = s3.scorer(e3, _3, s3)) : (typeof (_3 = pre_processor(h3 ? s3.processor(r2[n4]).normalize() : s3.processor(r2[n4]), s3)) == "string" && _3.length !== 0 || (O3 = true), d3 = s3.scorer(e3, _3, s3)), v3 = l3 ? parseInt(n4) : n4, d3 > s3.cutoff && (s3.returnObjects ? w3.push({ choice: r2[n4], score: d3, key: v3 }) : w3.push([r2[n4], d3, v3]))), a3 && a3.signal.aborted === true ? o3(new Error("aborted")) : i2 && i2.canceled === true ? o3(new Error("canceled")) : l3 && n4 < r2.length - 1 ? n4 % c2 == 0 ? setImmediate(function() {
45309
+ (l3 || r2.hasOwnProperty(n4)) && (s3.tokens = undefined, s3.proc_sorted = false, S3 ? (s3.proc_sorted = true, r2[n4] && r2[n4].proc_sorted ? _3 = r2[n4].proc_sorted : (_3 = pre_processor(h3 ? s3.processor(r2[n4]).normalize() : s3.processor(r2[n4]), s3), _3 = g3(_3)), d3 = s3.scorer(z2, _3, s3)) : C2 ? (_3 = "x", r2[n4] && r2[n4].tokens ? (s3.tokens = [A2, r2[n4].tokens], s3.trySimple && (_3 = pre_processor(s3.processor(r2[n4]), s3))) : (_3 = pre_processor(h3 ? s3.processor(r2[n4]).normalize() : s3.processor(r2[n4]), s3), s3.tokens = [A2, m2(_3, s3)]), d3 = s3.scorer(e3, _3, s3)) : f3 ? (_3 = s3.processor(r2[n4]), d3 = s3.scorer(e3, _3, s3)) : (typeof (_3 = pre_processor(h3 ? s3.processor(r2[n4]).normalize() : s3.processor(r2[n4]), s3)) == "string" && _3.length !== 0 || (O3 = true), d3 = s3.scorer(e3, _3, s3)), v3 = l3 ? parseInt(n4) : n4, d3 > s3.cutoff && (s3.returnObjects ? w3.push({ choice: r2[n4], score: d3, key: v3 }) : w3.push([r2[n4], d3, v3]))), a3 && a3.signal.aborted === true ? o3(new Error("aborted")) : i2 && i2.canceled === true ? o3(new Error("canceled")) : l3 && n4 < r2.length - 1 ? n4 % c2 == 0 ? setImmediate(function() {
44990
45310
  searchLoop(n4 + 1);
44991
45311
  }) : searchLoop(n4 + 1) : p3 < x2.length - 1 ? p3 % c2 == 0 ? setImmediate(function() {
44992
45312
  searchLoop(x2[p3 + 1], p3 + 1);
@@ -45072,11 +45392,11 @@ var Lo = { exports: {} };
45072
45392
  }, f3 = a2(n3, i2, wildCompare), p3 = u2(n3, i2, wildCompare), h3 = u2(i2, n3, wildCompare);
45073
45393
  else
45074
45394
  f3 = o2(n3, i2), p3 = s2(n3, i2), h3 = s2(i2, n3);
45075
- var d3 = f3.sort().join(" "), y3 = p3.sort(), g3 = h3.sort();
45395
+ var d3 = f3.sort().join(" "), y3 = p3.sort(), g4 = h3.sort();
45076
45396
  if (r2.sortBySimilarity)
45077
- var b3 = _order_token_lists(e3, y3, t3, g3), k3 = b3[0], j3 = b3[1], $3 = k3.join(" "), w3 = _token_similarity_sort(k3, j3, r2).join(" ");
45397
+ var b3 = _order_token_lists(e3, y3, t3, g4), k3 = b3[0], j3 = b3[1], $3 = k3.join(" "), w3 = _token_similarity_sort(k3, j3, r2).join(" ");
45078
45398
  else
45079
- $3 = y3.join(" "), w3 = g3.join(" ");
45399
+ $3 = y3.join(" "), w3 = g4.join(" ");
45080
45400
  var O3 = d3 + " " + $3, S3 = d3 + " " + w3;
45081
45401
  d3 = d3.trim(), O3 = O3.trim(), S3 = S3.trim();
45082
45402
  var C2 = _ratio;
@@ -45161,7 +45481,7 @@ var Lo = { exports: {} };
45161
45481
  }
45162
45482
  var p3 = token_sort_ratio(e3, t3, n3) * a3, h3 = token_set_ratio(e3, t3, n3) * a3;
45163
45483
  return Math.round(Math.max(s3, p3, h3));
45164
- }, full_process: b2, extract, extractAsync, extractAsPromised: O2, process_and_sort: g2, unique_tokens: m2, dedupe: $2 };
45484
+ }, full_process: b2, extract, extractAsync, extractAsPromised: O2, process_and_sort: g3, unique_tokens: m2, dedupe: $2 };
45165
45485
  r.exports = S2;
45166
45486
  }();
45167
45487
  var Ro = r.exports;
@@ -45405,97 +45725,835 @@ function settlementEnrichedData(description, type, source) {
45405
45725
  out.settlementCardLast4 = last4;
45406
45726
  return out;
45407
45727
  }
45408
- // ../parsers/src/classify.ts
45409
- var CORPORATE_TOKENS = [
45410
- "PVT",
45411
- "PRIVATE",
45412
- "LIMITED",
45413
- "LTD",
45414
- "LLP",
45415
- "INC",
45416
- "CORP",
45417
- "ENTERPRISES",
45418
- "TECHNOLOGIES",
45419
- "SOLUTIONS",
45420
- "SERVICES"
45421
- ];
45422
- var CORPORATE_REGEX = new RegExp(`\\b(${CORPORATE_TOKENS.join("|")})\\b`);
45423
- function classifyEntity({ paymentMethod, extractedName }) {
45424
- if (paymentMethod === "atm")
45425
- return "merchant";
45426
- if (paymentMethod === "pos")
45427
- return "merchant";
45428
- const upper = (extractedName ?? "").toUpperCase();
45429
- if (upper && CORPORATE_REGEX.test(upper))
45430
- return "merchant";
45431
- if (paymentMethod === "imps")
45432
- return "person";
45433
- if (paymentMethod === "upi")
45434
- return "person";
45435
- if (paymentMethod === "neft" || paymentMethod === "rtgs")
45436
- return "merchant";
45437
- if (paymentMethod === "ach")
45438
- return "merchant";
45439
- return "unknown";
45440
- }
45441
- // ../parsers/src/hdfc/narration.ts
45442
- function parseHdfcNarration(narration) {
45443
- const trimmed = narration.trim();
45444
- if (trimmed.startsWith("UPI-")) {
45445
- return parseUpi(trimmed);
45728
+ // src/lib/link-batches.ts
45729
+ var MAX_LINKS = 500;
45730
+ var MAX_TARGETS = 25;
45731
+ function chunkLinks(links) {
45732
+ const batches = [];
45733
+ let cur = [];
45734
+ let targets = new Set;
45735
+ for (const l2 of links) {
45736
+ const key = `${l2.merchantId ?? ""}|${l2.personId ?? ""}|${l2.categoryId ?? ""}`;
45737
+ if (cur.length >= MAX_LINKS || !targets.has(key) && targets.size >= MAX_TARGETS) {
45738
+ batches.push(cur);
45739
+ cur = [];
45740
+ targets = new Set;
45741
+ }
45742
+ cur.push(l2);
45743
+ targets.add(key);
45446
45744
  }
45447
- if (trimmed.startsWith("NEFT CR-") || trimmed.startsWith("NEFT DR-")) {
45448
- return parseNeft(trimmed);
45745
+ if (cur.length)
45746
+ batches.push(cur);
45747
+ return batches;
45748
+ }
45749
+ async function postLinks(client, links) {
45750
+ for (const batch of chunkLinks(links)) {
45751
+ await client.post("/api/transactions/bulk-link-entity", { links: batch });
45449
45752
  }
45450
- if (trimmed.startsWith("ACH D-") || trimmed.startsWith("ACH C-")) {
45451
- return parseAch(trimmed);
45753
+ }
45754
+
45755
+ // src/lib/txn-rows.ts
45756
+ function toTxnRow(r2) {
45757
+ return {
45758
+ id: String(r2.id),
45759
+ date: String(r2.date),
45760
+ amount: Number(r2.amount),
45761
+ type: r2.type === "credit" ? "credit" : "debit",
45762
+ description: String(r2.description ?? ""),
45763
+ merchantId: r2.merchantId ?? null,
45764
+ personId: r2.personId ?? null,
45765
+ categoryId: r2.categoryId ?? null,
45766
+ isTransfer: !!r2.isTransfer,
45767
+ isIgnored: !!r2.isIgnored,
45768
+ isDuplicate: !!r2.isDuplicate
45769
+ };
45770
+ }
45771
+ async function fetchAllRows(client, crypto3, params = {}, maxPages = 100) {
45772
+ const out = [];
45773
+ let cursor;
45774
+ for (let page = 0;page < maxPages; page++) {
45775
+ const q2 = { ...params, limit: 200, view: "match" };
45776
+ if (cursor)
45777
+ q2.cursor = cursor;
45778
+ const res = await client.get("/api/transactions", q2);
45779
+ const batch = res.data ?? [];
45780
+ if (crypto3)
45781
+ await decryptTransactionFields(batch, crypto3.key);
45782
+ out.push(...batch.map(toTxnRow));
45783
+ if (!res.hasMore || !res.nextCursor || batch.length === 0 || res.nextCursor === cursor)
45784
+ break;
45785
+ cursor = res.nextCursor;
45452
45786
  }
45453
- if (trimmed.startsWith("FT-") || trimmed.startsWith("FT -")) {
45454
- return parseFt(trimmed);
45787
+ return out;
45788
+ }
45789
+ var MAX_DESC = 70;
45790
+ function toLeanRow(r2, personNames) {
45791
+ const merchant = r2.merchant;
45792
+ const category = r2.category;
45793
+ const merchantName = r2.merchantName ?? merchant?.cleanName;
45794
+ const categorySlug = typeof category === "string" ? category : category?.slug;
45795
+ const personId = r2.personId;
45796
+ const person = personId ? personNames.get(personId) ?? null : null;
45797
+ let flags = r2.flags;
45798
+ if (!flags) {
45799
+ flags = [];
45800
+ if (r2.isTransfer)
45801
+ flags.push("transfer");
45802
+ if (r2.isIgnored)
45803
+ flags.push("ignored");
45804
+ if (r2.isDuplicate)
45805
+ flags.push("duplicate");
45806
+ }
45807
+ const desc = String(r2.description ?? "");
45808
+ return {
45809
+ id: r2.id,
45810
+ date: r2.date,
45811
+ amt: Number(r2.amount),
45812
+ t: r2.type === "credit" ? "cr" : "dr",
45813
+ desc: desc.length > MAX_DESC ? `${desc.slice(0, MAX_DESC)}…` : desc,
45814
+ ...categorySlug ? { cat: categorySlug } : {},
45815
+ ...merchantName ? { merchant: merchantName } : {},
45816
+ ...person ? { person } : {},
45817
+ ...flags.length ? { flags } : {}
45818
+ };
45819
+ }
45820
+
45821
+ // src/lib/categorize-groups.ts
45822
+ var NOISE = new Set([
45823
+ "upi",
45824
+ "pos",
45825
+ "imps",
45826
+ "neft",
45827
+ "rtgs",
45828
+ "ach",
45829
+ "nach",
45830
+ "ecs",
45831
+ "txn",
45832
+ "transaction",
45833
+ "payment",
45834
+ "paid",
45835
+ "pay",
45836
+ "to",
45837
+ "from",
45838
+ "ref",
45839
+ "refno",
45840
+ "debit",
45841
+ "credit",
45842
+ "card",
45843
+ "mob",
45844
+ "mobile",
45845
+ "bank",
45846
+ "transfer",
45847
+ "trf",
45848
+ "by",
45849
+ "for",
45850
+ "the",
45851
+ "via",
45852
+ "ecom",
45853
+ "purchase",
45854
+ "online",
45855
+ "inr",
45856
+ "rs",
45857
+ "towards",
45858
+ "ibl",
45859
+ "ybl",
45860
+ "oksbi",
45861
+ "okhdfcbank",
45862
+ "okicici",
45863
+ "okaxis",
45864
+ "axl",
45865
+ "apl",
45866
+ "paytm",
45867
+ "icici",
45868
+ "hdfc",
45869
+ "sbi",
45870
+ "axis",
45871
+ "utib",
45872
+ "sbin",
45873
+ "ifsc",
45874
+ "vpa",
45875
+ "bill",
45876
+ "cas",
45877
+ "dr",
45878
+ "cr"
45879
+ ]);
45880
+ var GOOD_TOKEN = /^[a-z][a-z&']{2,}$/;
45881
+ var MAX_RUN = 3;
45882
+ function descriptionPattern(description) {
45883
+ const tokens = normaliseName(description).split(" ").filter(Boolean);
45884
+ const run = [];
45885
+ for (const tok of tokens) {
45886
+ const good = GOOD_TOKEN.test(tok) && !NOISE.has(tok);
45887
+ if (!good) {
45888
+ if (run.length)
45889
+ break;
45890
+ continue;
45891
+ }
45892
+ if (run.includes(tok))
45893
+ break;
45894
+ run.push(tok);
45895
+ if (run.length === MAX_RUN)
45896
+ break;
45455
45897
  }
45456
- if (trimmed.startsWith("IMPS-") || trimmed.startsWith("IMPS/")) {
45457
- return parseImps(trimmed);
45898
+ return run.join(" ");
45899
+ }
45900
+ var UNPARSED = "(unparsed)";
45901
+ function groupRows(rows) {
45902
+ const map2 = new Map;
45903
+ for (const r2 of rows) {
45904
+ const pattern = descriptionPattern(r2.description) || UNPARSED;
45905
+ const key = `${pattern}|${r2.type}`;
45906
+ const g3 = map2.get(key) ?? { pattern, type: r2.type, rows: [], total: 0 };
45907
+ g3.rows.push(r2);
45908
+ g3.total += r2.amount;
45909
+ map2.set(key, g3);
45910
+ }
45911
+ return [...map2.values()].sort((a2, b2) => b2.total - a2.total);
45912
+ }
45913
+ function isCategorizable(r2) {
45914
+ return !r2.categoryId && !r2.isTransfer && !r2.isIgnored && !r2.isDuplicate;
45915
+ }
45916
+ async function fetchUncategorized(client, crypto3, range2 = {}) {
45917
+ const params = { uncategorized: "true" };
45918
+ if (range2.startDate)
45919
+ params.startDate = range2.startDate;
45920
+ if (range2.endDate)
45921
+ params.endDate = range2.endDate;
45922
+ return (await fetchAllRows(client, crypto3, params)).filter(isCategorizable);
45923
+ }
45924
+ var titleCase = (s2) => s2.replace(/\b[a-z]/g, (c2) => c2.toUpperCase());
45925
+ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
45926
+ const catIndex = await loadCategoryIndex(client);
45927
+ const resolved = assignments.map((a2) => ({ a: a2, cat: requireCategory(catIndex, a2.categorySlug) }));
45928
+ const groups = groupRows(await fetchUncategorized(client, crypto3, opts));
45929
+ const result = { applied: [], notFound: [], rulesSaved: 0, unlearnable: [] };
45930
+ const plan = [];
45931
+ const claimed = new Set;
45932
+ for (const { a: a2, cat } of resolved) {
45933
+ const hits = groups.filter((g3) => g3.pattern === a2.pattern && (!a2.type || g3.type === a2.type) && !claimed.has(g3));
45934
+ if (hits.length === 0) {
45935
+ result.notFound.push(a2.pattern);
45936
+ continue;
45937
+ }
45938
+ for (const g3 of hits) {
45939
+ claimed.add(g3);
45940
+ plan.push({ group: g3, categoryId: cat.id, slug: cat.slug, learn: a2.learn !== false });
45941
+ }
45458
45942
  }
45459
- if (trimmed.startsWith("ATW-") || trimmed.startsWith("ATM-") || trimmed.startsWith("ATM/")) {
45460
- return {
45461
- paymentMethod: "atm",
45462
- name: "ATM Withdrawal",
45463
- upiId: null,
45464
- referenceNumber: null,
45465
- description: trimmed
45466
- };
45943
+ if (opts.dryRun) {
45944
+ result.applied = plan.map((p2) => ({
45945
+ pattern: p2.group.pattern,
45946
+ type: p2.group.type,
45947
+ rows: p2.group.rows.length,
45948
+ category: p2.slug
45949
+ }));
45950
+ return result;
45467
45951
  }
45468
- if (trimmed.startsWith("POS ")) {
45469
- return parsePos(trimmed);
45952
+ if (plan.length === 0)
45953
+ return result;
45954
+ const learnable = plan.filter((p2) => p2.learn && p2.group.pattern !== UNPARSED);
45955
+ const merchantByRawId = new Map;
45956
+ if (learnable.length > 0) {
45957
+ const ms = await client.get("/api/merchants");
45958
+ for (const m2 of ms.data ?? [])
45959
+ if (m2.rawId && m2.householdId)
45960
+ merchantByRawId.set(m2.rawId, m2.id);
45961
+ }
45962
+ const { bag, version: version2 } = await loadBag(client);
45963
+ const now = new Date().toISOString();
45964
+ const links = [];
45965
+ const direct = [];
45966
+ for (const { group, categoryId, slug, learn } of plan) {
45967
+ result.applied.push({
45968
+ pattern: group.pattern,
45969
+ type: group.type,
45970
+ rows: group.rows.length,
45971
+ category: slug
45972
+ });
45973
+ if (!learn || group.pattern === UNPARSED) {
45974
+ if (learn)
45975
+ result.unlearnable.push(UNPARSED);
45976
+ direct.push({ ids: group.rows.map((r2) => r2.id), categoryId });
45977
+ continue;
45978
+ }
45979
+ const rawId = `pattern:${group.pattern}`;
45980
+ let merchantId = merchantByRawId.get(rawId);
45981
+ if (!merchantId) {
45982
+ const created = await client.post("/api/merchants", {
45983
+ rawId,
45984
+ cleanName: titleCase(group.pattern),
45985
+ categoryId
45986
+ });
45987
+ merchantId = created.data.id;
45988
+ merchantByRawId.set(rawId, merchantId);
45989
+ }
45990
+ if (!bag.merchantAliases.some((al) => al.pattern === group.pattern)) {
45991
+ bag.merchantAliases.push({ pattern: group.pattern, merchantId, createdAt: now });
45992
+ }
45993
+ bag.merchantRules[merchantId] = { categoryId, lastUserCategoryAt: now };
45994
+ result.rulesSaved += 1;
45995
+ const withPerson = group.rows.filter((r2) => r2.personId);
45996
+ if (withPerson.length)
45997
+ direct.push({ ids: withPerson.map((r2) => r2.id), categoryId });
45998
+ for (const r2 of group.rows) {
45999
+ if (!r2.personId)
46000
+ links.push({ transactionId: r2.id, merchantId, categoryId });
46001
+ }
46002
+ }
46003
+ if (result.rulesSaved > 0)
46004
+ await saveBag(client, bag, version2);
46005
+ await postLinks(client, links);
46006
+ for (const { ids, categoryId } of direct) {
46007
+ const categoryType = [...catIndex.byId.entries()].find(([id]) => id === categoryId)?.[1].type;
46008
+ for (let i2 = 0;i2 < ids.length; i2 += 200) {
46009
+ await client.patch("/api/transactions/bulk", {
46010
+ ids: ids.slice(i2, i2 + 200),
46011
+ categoryId,
46012
+ ...categoryType ? { categoryType } : {}
46013
+ });
46014
+ }
45470
46015
  }
45471
- return parseFallback(trimmed);
46016
+ return result;
45472
46017
  }
45473
- function parseUpi(narration) {
45474
- const rest = narration.slice(4);
45475
- const parts = rest.split("-");
45476
- if (parts.length < 4) {
45477
- return {
45478
- paymentMethod: "upi",
45479
- name: rest,
45480
- upiId: null,
45481
- referenceNumber: null,
45482
- description: null
45483
- };
46018
+
46019
+ // src/lib/jev-categorize.ts
46020
+ var JEV_MODEL = "jev-1.13.0";
46021
+ var DEFAULT_MIN_CONFIDENCE = 0.95;
46022
+ var TIMEOUT_MS = 8000;
46023
+ var CONCURRENCY = 8;
46024
+ var MIN_LEARN_PATTERN_LEN = 5;
46025
+ function assertPrivateModeAllowed(crypto3, env2 = process.env) {
46026
+ if (crypto3 && env2.JEV_ALLOW_PRIVATE_MODE !== "1") {
46027
+ 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.");
45484
46028
  }
45485
- const name = parts[0].trim();
45486
- const upiId = parts[1]?.trim() || null;
45487
- const referenceNumber = parts[3]?.trim() || null;
45488
- const description = parts.length > 4 ? parts.slice(4).join("-").trim() : null;
45489
- return {
45490
- paymentMethod: "upi",
45491
- name: cleanName(name),
45492
- upiId,
45493
- referenceNumber,
45494
- description: description || null
45495
- };
45496
46029
  }
45497
- function parseNeft(narration) {
45498
- const isCredit = narration.startsWith("NEFT CR-");
46030
+ function assertJevAllowed(crypto3, env2 = process.env) {
46031
+ assertPrivateModeAllowed(crypto3, env2);
46032
+ const key = env2.JEV_API_KEY;
46033
+ if (!key)
46034
+ throw new Error("JEV_API_KEY is not set — Jev categorization is off.");
46035
+ return key;
46036
+ }
46037
+ function makeJevClassifier(apiKey) {
46038
+ const client = new TypeSafeClient({ apiKey, timeout: TIMEOUT_MS });
46039
+ return async (state, options) => {
46040
+ const { answers } = await client.systemOne({
46041
+ model: JEV_MODEL,
46042
+ state,
46043
+ questions: {
46044
+ category: choice("Which spending category best fits this bank transaction?", options)
46045
+ }
46046
+ });
46047
+ return { choice: answers.category.choice, confidence: answers.category.confidence };
46048
+ };
46049
+ }
46050
+ var UNSURE = "unsure";
46051
+ function sanitizeNarration(s2) {
46052
+ return s2.replace(/\d{3,}/g, " ").replace(/\s+/g, " ").trim();
46053
+ }
46054
+ function optionsFor(type, index) {
46055
+ const allowed = type === "credit" ? new Set(["income", "transfer"]) : new Set(["needs", "wants", "investments", "transfer"]);
46056
+ const out = {};
46057
+ for (const [slug, c2] of index.bySlug) {
46058
+ if (index.parents.has(slug) || !allowed.has(c2.type))
46059
+ continue;
46060
+ out[slug] = `${c2.type}: ${slug.replace(/_/g, " ")}`;
46061
+ }
46062
+ out[UNSURE] = "unclear, or a payment to an individual person";
46063
+ return out;
46064
+ }
46065
+ function groupState(type, sample) {
46066
+ const dir = type === "credit" ? "Money received" : "Money paid out";
46067
+ return `${dir}. Bank narration: "${sample.slice(0, 80)}"`;
46068
+ }
46069
+ async function mapLimit(items, limit, fn2) {
46070
+ const out = new Array(items.length);
46071
+ let next = 0;
46072
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
46073
+ while (next < items.length) {
46074
+ const i2 = next++;
46075
+ out[i2] = await fn2(items[i2]);
46076
+ }
46077
+ }));
46078
+ return out;
46079
+ }
46080
+ async function jevCategorize(client, crypto3, classify, opts = {}) {
46081
+ assertPrivateModeAllowed(crypto3, opts.env);
46082
+ const min = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
46083
+ const index = await loadCategoryIndex(client);
46084
+ const rows = await fetchUncategorized(client, crypto3, opts);
46085
+ const groups = groupRows(rows);
46086
+ const unparsed = groups.filter((g3) => g3.pattern === UNPARSED).length;
46087
+ const candidates = groups.filter((g3) => g3.pattern !== UNPARSED);
46088
+ let errors4 = 0;
46089
+ const suggestions = await mapLimit(candidates, CONCURRENCY, async (g3) => {
46090
+ try {
46091
+ const options = optionsFor(g3.type, index);
46092
+ const sample = g3.rows[0].description;
46093
+ const text = crypto3 ? sanitizeNarration(sample) : sample;
46094
+ const r2 = await classify(groupState(g3.type, text), options);
46095
+ return r2.choice in options ? { g: g3, ...r2 } : null;
46096
+ } catch {
46097
+ errors4 += 1;
46098
+ return null;
46099
+ }
46100
+ });
46101
+ const accepted = [];
46102
+ const review = [];
46103
+ for (const s2 of suggestions) {
46104
+ if (!s2)
46105
+ continue;
46106
+ const isTransfer = index.bySlug.get(s2.choice)?.type === "transfer";
46107
+ if (s2.choice !== UNSURE && !isTransfer && s2.confidence >= min) {
46108
+ accepted.push({
46109
+ pattern: s2.g.pattern,
46110
+ type: s2.g.type,
46111
+ categorySlug: s2.choice,
46112
+ learn: s2.g.pattern.length >= MIN_LEARN_PATTERN_LEN,
46113
+ confidence: s2.confidence
46114
+ });
46115
+ } else {
46116
+ review.push({
46117
+ pattern: s2.g.pattern,
46118
+ type: s2.g.type === "credit" ? "cr" : "dr",
46119
+ best: s2.choice,
46120
+ confidence: Math.round(s2.confidence * 100) / 100,
46121
+ total: Math.round(s2.g.total)
46122
+ });
46123
+ }
46124
+ }
46125
+ review.sort((a2, b2) => b2.total - a2.total);
46126
+ const applied = await applyGroupAssignments(client, crypto3, accepted.map(({ confidence: _c, ...a2 }) => a2), { dryRun: opts.dryRun, startDate: opts.startDate, endDate: opts.endDate });
46127
+ const conf = new Map(accepted.map((a2) => [`${a2.pattern}|${a2.type}`, a2.confidence]));
46128
+ return {
46129
+ dryRun: !!opts.dryRun,
46130
+ applied: applied.applied.map((a2) => ({
46131
+ pattern: a2.pattern,
46132
+ category: a2.category,
46133
+ confidence: Math.round((conf.get(`${a2.pattern}|${a2.type}`) ?? 0) * 100) / 100,
46134
+ rows: a2.rows
46135
+ })),
46136
+ review: review.slice(0, 30),
46137
+ skipped: { unparsed, errors: errors4 },
46138
+ rulesSaved: applied.rulesSaved
46139
+ };
46140
+ }
46141
+
46142
+ // src/tools/jev.ts
46143
+ function registerJevTools(server, client, crypto3) {
46144
+ server.registerTool("auto_categorize_jev", {
46145
+ description: "Auto-categorize uncategorized transactions with Jev (TypeSafe's classifier) — zero LLM tokens, ~ms per group. " + "OPT-IN: needs JEV_API_KEY. Refuses in private mode (it would send transaction text to a third party) unless the owner sets JEV_ALLOW_PRIVATE_MODE=1; in private mode only the sanitized merchant pattern is sent. " + "Groups by merchant pattern; groups at/above `minConfidence` (default 0.85) are categorized and a reusable rule is saved; " + "the rest come back in `review` (top 30 by ₹) for you to decide with `categorize_groups`. Use dryRun to preview.",
46146
+ inputSchema: {
46147
+ startDate: exports_external.string().optional().describe("YYYY-MM-DD"),
46148
+ endDate: exports_external.string().optional().describe("YYYY-MM-DD"),
46149
+ minConfidence: exports_external.number().min(0.5).max(1).optional().describe(`Apply at/above this confidence (default ${DEFAULT_MIN_CONFIDENCE})`),
46150
+ dryRun: exports_external.boolean().optional()
46151
+ }
46152
+ }, async ({ startDate, endDate, minConfidence, dryRun }) => {
46153
+ const key = assertJevAllowed(crypto3);
46154
+ const result = await jevCategorize(client, crypto3, makeJevClassifier(key), {
46155
+ startDate,
46156
+ endDate,
46157
+ minConfidence,
46158
+ dryRun
46159
+ });
46160
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
46161
+ });
46162
+ }
46163
+
46164
+ // src/tools/merchants.ts
46165
+ async function categorySlugToId(client) {
46166
+ const res = await client.get("/api/categories");
46167
+ const map2 = new Map;
46168
+ for (const root of res.data ?? []) {
46169
+ if (root.slug)
46170
+ map2.set(root.slug, root.id);
46171
+ for (const child of root.children ?? []) {
46172
+ if (child.slug)
46173
+ map2.set(child.slug, child.id);
46174
+ }
46175
+ }
46176
+ return map2;
46177
+ }
46178
+ function registerMerchantTools(server, client, crypto3) {
46179
+ server.registerTool("upsert_merchant", {
46180
+ description: "Create or update a merchant in the dictionary. Omit `id` to add a new merchant (future transactions from it are auto-categorised); pass `id` to update an existing merchant's name, category, or flags.",
46181
+ inputSchema: {
46182
+ id: exports_external.string().optional().describe("Merchant UUID — pass to update, omit to create"),
46183
+ rawId: exports_external.string().optional().describe("Raw identifier — UPI ID or normalised name (required when creating)"),
46184
+ cleanName: exports_external.string().optional().describe('Human-friendly name (e.g. "DMart")'),
46185
+ categorySlug: exports_external.string().optional().describe("Category slug, e.g. 'food_delivery'"),
46186
+ isPerson: exports_external.boolean().optional(),
46187
+ isRecurring: exports_external.boolean().optional(),
46188
+ notes: exports_external.string().optional()
46189
+ }
46190
+ }, async ({ id, categorySlug, ...rest }) => {
46191
+ const payload = { ...rest };
46192
+ if (categorySlug) {
46193
+ const slugMap = await categorySlugToId(client);
46194
+ const categoryId = slugMap.get(categorySlug);
46195
+ if (!categoryId) {
46196
+ throw new Error(`Unknown categorySlug "${categorySlug}". Use list_categories to see valid slugs.`);
46197
+ }
46198
+ payload.categoryId = categoryId;
46199
+ }
46200
+ const data = id ? await client.patch(`/api/merchants/${id}`, payload) : await client.post("/api/merchants", payload);
46201
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46202
+ });
46203
+ server.registerTool("list_merchants", {
46204
+ description: "List merchants in the dictionary with their categories and transaction counts. Pass `rawId` to look up a single merchant by its raw ID (UPI ID or normalised name) instead of listing all.",
46205
+ inputSchema: {
46206
+ rawId: exports_external.string().optional().describe("Raw identifier to look up a single merchant; omit to list all")
46207
+ }
46208
+ }, async ({ rawId }) => {
46209
+ if (crypto3 && rawId) {
46210
+ const data2 = await client.get("/api/merchants");
46211
+ if (data2.data)
46212
+ await decryptMerchantFields(data2.data, crypto3.key);
46213
+ const needle = rawId.toLowerCase();
46214
+ const merchants = (data2.data ?? []).filter((m2) => String(m2.rawId ?? "").toLowerCase() === needle || String(m2.cleanName ?? "").toLowerCase().includes(needle));
46215
+ return {
46216
+ content: [{ type: "text", text: JSON.stringify({ ...data2, data: merchants }) }]
46217
+ };
46218
+ }
46219
+ const data = rawId ? await client.get("/api/merchants", {
46220
+ search: rawId
46221
+ }) : await client.get("/api/merchants");
46222
+ if (crypto3 && data.data)
46223
+ await decryptMerchantFields(data.data, crypto3.key);
46224
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46225
+ });
46226
+ server.registerTool("delete_merchant", {
46227
+ description: "Delete merchant(s) from the dictionary. Provide `id` for a single merchant or `ids` for several. Transactions referencing a deleted merchant have their merchantId cleared. PERMANENT.",
46228
+ inputSchema: {
46229
+ id: exports_external.string().optional().describe("Single merchant UUID"),
46230
+ ids: exports_external.array(exports_external.string()).min(1).optional().describe("Multiple merchant UUIDs (bulk delete)")
46231
+ }
46232
+ }, async ({ id, ids }) => {
46233
+ if (!id && (!ids || ids.length === 0)) {
46234
+ throw new Error("Provide either id or ids");
46235
+ }
46236
+ const data = ids ? await client.post("/api/merchants/bulk-delete", { ids }) : await client.delete(`/api/merchants/${id}`);
46237
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46238
+ });
46239
+ }
46240
+
46241
+ // src/tools/net-worth.ts
46242
+ function registerNetWorthTools(server, client) {
46243
+ server.registerTool("get_net_worth", {
46244
+ description: "Current net worth snapshot and history (up to 12 months). Includes liquid cash, investments, emergency fund, liabilities.",
46245
+ inputSchema: {}
46246
+ }, async () => {
46247
+ const data = await client.get("/api/net-worth");
46248
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46249
+ });
46250
+ server.registerTool("update_net_worth", {
46251
+ description: "Log a net worth snapshot for a specific month. Upserts — safe to call multiple times for the same month. mode=manual (default) records the provided liquidCash/investments/emergencyFund/liabilities/breakdown values. mode=auto ignores those and auto-calculates net worth from DB (investments + FDs + accounts) — use it when the data is already in the system.",
46252
+ inputSchema: {
46253
+ mode: exports_external.enum(["manual", "auto"]).default("manual"),
46254
+ month: exports_external.number().min(1).max(12),
46255
+ year: exports_external.number().min(2020).max(2100),
46256
+ liquidCash: exports_external.string().optional(),
46257
+ investments: exports_external.string().optional(),
46258
+ emergencyFund: exports_external.string().optional(),
46259
+ liabilities: exports_external.string().optional(),
46260
+ breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional()
46261
+ }
46262
+ }, async ({ mode, ...body }) => {
46263
+ let data;
46264
+ if (mode === "auto") {
46265
+ data = await client.post("/api/net-worth/auto", { month: body.month, year: body.year });
46266
+ } else {
46267
+ const liquidCash = parseFloat(body.liquidCash ?? "0");
46268
+ const investments = parseFloat(body.investments ?? "0");
46269
+ const emergencyFund = parseFloat(body.emergencyFund ?? "0");
46270
+ const liabilities = parseFloat(body.liabilities ?? "0");
46271
+ const totalNetWorth = (liquidCash + investments + emergencyFund - liabilities).toFixed(2);
46272
+ data = await client.post("/api/net-worth", {
46273
+ ...body,
46274
+ emergencyFund: body.emergencyFund ?? "0",
46275
+ totalNetWorth
46276
+ });
46277
+ }
46278
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46279
+ });
46280
+ }
46281
+
46282
+ // src/tools/persons.ts
46283
+ function registerPersonTools(server, client, crypto3) {
46284
+ server.registerTool("upsert_person", {
46285
+ description: "Create or update a person (UPI counterparty / individual) in this household's encrypted directory. Omit `id` to add a new person; pass `id` to update any combination of an existing person's fields.",
46286
+ inputSchema: {
46287
+ id: exports_external.string().optional().describe("Person UUID — pass to update, omit to create"),
46288
+ name: exports_external.string().optional().describe('Display name (e.g. "Priya") — required when creating'),
46289
+ upiHandles: exports_external.array(exports_external.string()).optional().describe("UPI ids the person uses"),
46290
+ phoneNumbers: exports_external.array(exports_external.string()).optional(),
46291
+ relationship: exports_external.string().optional().describe('e.g. "sister", "friend", "landlord"'),
46292
+ notes: exports_external.string().optional()
46293
+ }
46294
+ }, async ({ id, ...updates }) => {
46295
+ if (crypto3) {
46296
+ if (id) {
46297
+ const existing = await client.get(`/api/persons/${id}`);
46298
+ const personData = existing.data;
46299
+ let current;
46300
+ if (personData.cipher) {
46301
+ current = await decryptPersonCipher(personData.cipher, crypto3.key);
46302
+ } else {
46303
+ current = {
46304
+ name: personData.name,
46305
+ upiHandles: personData.upiHandles,
46306
+ phoneNumbers: personData.phoneNumbers,
46307
+ relationship: personData.relationship,
46308
+ notes: personData.notes
46309
+ };
46310
+ }
46311
+ const merged = {
46312
+ name: updates.name ?? current.name,
46313
+ upiHandles: updates.upiHandles ?? current.upiHandles,
46314
+ phoneNumbers: updates.phoneNumbers ?? current.phoneNumbers,
46315
+ relationship: updates.relationship ?? current.relationship ?? null,
46316
+ notes: updates.notes ?? current.notes ?? null
46317
+ };
46318
+ const payload2 = await encryptPersonCipher(merged, crypto3.key);
46319
+ const data3 = await client.patch(`/api/persons/${id}`, payload2);
46320
+ return { content: [{ type: "text", text: JSON.stringify(data3) }] };
46321
+ }
46322
+ if (!updates.name)
46323
+ throw new Error("name is required when creating a person");
46324
+ const fields = {
46325
+ name: updates.name,
46326
+ upiHandles: updates.upiHandles ?? [],
46327
+ phoneNumbers: updates.phoneNumbers ?? [],
46328
+ relationship: updates.relationship ?? null,
46329
+ notes: updates.notes ?? null
46330
+ };
46331
+ const payload = await encryptPersonCipher(fields, crypto3.key);
46332
+ const data2 = await client.post("/api/persons", payload);
46333
+ return { content: [{ type: "text", text: JSON.stringify(data2) }] };
46334
+ }
46335
+ const data = id ? await client.patch(`/api/persons/${id}`, updates) : await client.post("/api/persons", updates);
46336
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46337
+ });
46338
+ server.registerTool("list_persons", {
46339
+ description: "List all persons in the current household. Pass `id` to get a single person by id instead of listing all.",
46340
+ inputSchema: {
46341
+ id: exports_external.string().optional().describe("Person UUID to fetch a single person; omit to list all")
46342
+ }
46343
+ }, async ({ id }) => {
46344
+ if (id) {
46345
+ const data2 = await client.get(`/api/persons/${id}`);
46346
+ if (crypto3 && data2.data.cipher) {
46347
+ const fields = await decryptPersonCipher(data2.data.cipher, crypto3.key);
46348
+ Object.assign(data2.data, fields, { cipher: null });
46349
+ }
46350
+ return { content: [{ type: "text", text: JSON.stringify(data2) }] };
46351
+ }
46352
+ const data = await client.get("/api/persons");
46353
+ if (crypto3 && data.data) {
46354
+ for (const person of data.data) {
46355
+ const cipher = person.cipher;
46356
+ if (cipher) {
46357
+ try {
46358
+ const fields = await decryptPersonCipher(cipher, crypto3.key);
46359
+ Object.assign(person, fields, { cipher: null });
46360
+ } catch {}
46361
+ }
46362
+ }
46363
+ }
46364
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46365
+ });
46366
+ server.registerTool("delete_person", {
46367
+ description: "Delete a person. Returns 409 if any transactions still reference this person.",
46368
+ inputSchema: { id: exports_external.string().describe("Person UUID") }
46369
+ }, async ({ id }) => {
46370
+ const data = await client.delete(`/api/persons/${id}`);
46371
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46372
+ });
46373
+ }
46374
+
46375
+ // src/tools/profile.ts
46376
+ function registerProfileTools(server, client) {
46377
+ server.registerTool("get_profile", {
46378
+ description: "Get the current user's profile (name, email, owner).",
46379
+ inputSchema: {}
46380
+ }, async () => {
46381
+ const data = await client.get("/api/profile");
46382
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46383
+ });
46384
+ server.registerTool("update_profile", {
46385
+ description: "Update your display name.",
46386
+ inputSchema: {
46387
+ name: exports_external.string().min(1).describe("Display name")
46388
+ }
46389
+ }, async (body) => {
46390
+ const data = await client.patch("/api/profile", body);
46391
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46392
+ });
46393
+ }
46394
+
46395
+ // src/tools/push.ts
46396
+ function registerPushTools(server, client) {
46397
+ server.registerTool("list_push_subscriptions", {
46398
+ description: "List the household's registered web-push subscriptions (browser/device endpoints that receive alert notifications).",
46399
+ inputSchema: {}
46400
+ }, async () => {
46401
+ const data = await client.get("/api/push/subscriptions");
46402
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46403
+ });
46404
+ server.registerTool("send_test_push", {
46405
+ description: "Send a test web-push notification to all of the household's subscriptions. Use to verify push delivery is working.",
46406
+ inputSchema: {}
46407
+ }, async () => {
46408
+ const data = await client.post("/api/push/test");
46409
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46410
+ });
46411
+ }
46412
+
46413
+ // src/tools/recurring.ts
46414
+ function registerRecurringTools(server, client) {
46415
+ server.registerTool("list_recurring", {
46416
+ description: "List all recurring transactions (subscriptions, rent, salaries).",
46417
+ inputSchema: {}
46418
+ }, async () => {
46419
+ const data = await client.get("/api/recurring");
46420
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46421
+ });
46422
+ server.registerTool("add_recurring", {
46423
+ description: "Add a recurring transaction to track subscriptions or regular income.",
46424
+ inputSchema: {
46425
+ name: exports_external.string(),
46426
+ amount: exports_external.string(),
46427
+ type: exports_external.enum(["debit", "credit"]),
46428
+ frequency: exports_external.enum(["daily", "weekly", "monthly", "yearly"]),
46429
+ dayOfMonth: exports_external.number().optional().describe("For monthly — day of month (1-31)"),
46430
+ categorySlug: exports_external.string().optional(),
46431
+ owner: exports_external.string(),
46432
+ startDate: exports_external.string().optional().describe("YYYY-MM-DD"),
46433
+ notes: exports_external.string().optional()
46434
+ }
46435
+ }, async (body) => {
46436
+ const data = await client.post("/api/recurring", body);
46437
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46438
+ });
46439
+ }
46440
+
46441
+ // src/tools/reports.ts
46442
+ function registerReportTools(server, client) {
46443
+ server.registerTool("generate_monthly_report", {
46444
+ description: "Generate a detailed monthly financial report. Computes income, expenses, savings rate, top categories, and insights. Persists to DB.",
46445
+ inputSchema: {
46446
+ month: exports_external.number().min(1).max(12),
46447
+ year: exports_external.number().min(2020).max(2100)
46448
+ }
46449
+ }, async (body) => {
46450
+ const data = await client.post("/api/reports", body);
46451
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46452
+ });
46453
+ server.registerTool("get_reports", {
46454
+ description: "Monthly financial reports for the last N months — income, expenses, savings rate, net worth.",
46455
+ inputSchema: {
46456
+ months: exports_external.number().optional().describe("Number of months (default: 6)")
46457
+ }
46458
+ }, async (params) => {
46459
+ const data = await client.get("/api/reports", params.months ? { months: params.months } : undefined);
46460
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46461
+ });
46462
+ }
46463
+
46464
+ // src/tools/transactions.ts
46465
+ import { readFile } from "node:fs/promises";
46466
+ // ../parsers/src/classify.ts
46467
+ var CORPORATE_TOKENS = [
46468
+ "PVT",
46469
+ "PRIVATE",
46470
+ "LIMITED",
46471
+ "LTD",
46472
+ "LLP",
46473
+ "INC",
46474
+ "CORP",
46475
+ "ENTERPRISES",
46476
+ "TECHNOLOGIES",
46477
+ "SOLUTIONS",
46478
+ "SERVICES"
46479
+ ];
46480
+ var CORPORATE_REGEX = new RegExp(`\\b(${CORPORATE_TOKENS.join("|")})\\b`);
46481
+ function classifyEntity({ paymentMethod, extractedName }) {
46482
+ if (paymentMethod === "atm")
46483
+ return "merchant";
46484
+ if (paymentMethod === "pos")
46485
+ return "merchant";
46486
+ const upper = (extractedName ?? "").toUpperCase();
46487
+ if (upper && CORPORATE_REGEX.test(upper))
46488
+ return "merchant";
46489
+ if (paymentMethod === "imps")
46490
+ return "person";
46491
+ if (paymentMethod === "upi")
46492
+ return "person";
46493
+ if (paymentMethod === "neft" || paymentMethod === "rtgs")
46494
+ return "merchant";
46495
+ if (paymentMethod === "ach")
46496
+ return "merchant";
46497
+ return "unknown";
46498
+ }
46499
+ // ../parsers/src/hdfc/narration.ts
46500
+ function parseHdfcNarration(narration) {
46501
+ const trimmed = narration.trim();
46502
+ if (trimmed.startsWith("UPI-")) {
46503
+ return parseUpi(trimmed);
46504
+ }
46505
+ if (trimmed.startsWith("NEFT CR-") || trimmed.startsWith("NEFT DR-")) {
46506
+ return parseNeft(trimmed);
46507
+ }
46508
+ if (trimmed.startsWith("ACH D-") || trimmed.startsWith("ACH C-")) {
46509
+ return parseAch(trimmed);
46510
+ }
46511
+ if (trimmed.startsWith("FT-") || trimmed.startsWith("FT -")) {
46512
+ return parseFt(trimmed);
46513
+ }
46514
+ if (trimmed.startsWith("IMPS-") || trimmed.startsWith("IMPS/")) {
46515
+ return parseImps(trimmed);
46516
+ }
46517
+ if (trimmed.startsWith("ATW-") || trimmed.startsWith("ATM-") || trimmed.startsWith("ATM/")) {
46518
+ return {
46519
+ paymentMethod: "atm",
46520
+ name: "ATM Withdrawal",
46521
+ upiId: null,
46522
+ referenceNumber: null,
46523
+ description: trimmed
46524
+ };
46525
+ }
46526
+ if (trimmed.startsWith("POS ")) {
46527
+ return parsePos(trimmed);
46528
+ }
46529
+ return parseFallback(trimmed);
46530
+ }
46531
+ function parseUpi(narration) {
46532
+ const rest = narration.slice(4);
46533
+ const parts = rest.split("-");
46534
+ if (parts.length < 4) {
46535
+ return {
46536
+ paymentMethod: "upi",
46537
+ name: rest,
46538
+ upiId: null,
46539
+ referenceNumber: null,
46540
+ description: null
46541
+ };
46542
+ }
46543
+ const name = parts[0].trim();
46544
+ const upiId = parts[1]?.trim() || null;
46545
+ const referenceNumber = parts[3]?.trim() || null;
46546
+ const description = parts.length > 4 ? parts.slice(4).join("-").trim() : null;
46547
+ return {
46548
+ paymentMethod: "upi",
46549
+ name: cleanName(name),
46550
+ upiId,
46551
+ referenceNumber,
46552
+ description: description || null
46553
+ };
46554
+ }
46555
+ function parseNeft(narration) {
46556
+ const isCredit = narration.startsWith("NEFT CR-");
45499
46557
  const rest = narration.slice(isCredit ? 8 : 8);
45500
46558
  const parts = rest.split("-");
45501
46559
  if (parts.length < 2) {
@@ -45708,33 +46766,33 @@ async function backfillPersons(client, crypto3, opts = {}) {
45708
46766
  const norm = normalizePersonName(name);
45709
46767
  if (!norm)
45710
46768
  continue;
45711
- const g2 = groups.get(norm) ?? {
46769
+ const g3 = groups.get(norm) ?? {
45712
46770
  displayName: name.trim(),
45713
46771
  upiHandles: new Set,
45714
46772
  txnIds: []
45715
46773
  };
45716
46774
  if (upiId)
45717
- g2.upiHandles.add(upiId);
45718
- g2.txnIds.push(t2.id);
45719
- groups.set(norm, g2);
46775
+ g3.upiHandles.add(upiId);
46776
+ g3.txnIds.push(t2.id);
46777
+ groups.set(norm, g3);
45720
46778
  }
45721
46779
  const plan = [];
45722
46780
  const newGroups = [];
45723
46781
  let reused = 0;
45724
- for (const [norm, g2] of groups) {
45725
- const upiHandles = [...g2.upiHandles];
46782
+ for (const [norm, g3] of groups) {
46783
+ const upiHandles = [...g3.upiHandles];
45726
46784
  const isReuse = personIdByNorm.has(norm);
45727
46785
  plan.push({
45728
- name: g2.displayName,
46786
+ name: g3.displayName,
45729
46787
  normalized: norm,
45730
- txnCount: g2.txnIds.length,
46788
+ txnCount: g3.txnIds.length,
45731
46789
  upiHandles,
45732
46790
  action: isReuse ? "reuse" : "create"
45733
46791
  });
45734
46792
  if (isReuse)
45735
46793
  reused++;
45736
46794
  else
45737
- newGroups.push({ norm, displayName: g2.displayName, upiHandles });
46795
+ newGroups.push({ norm, displayName: g3.displayName, upiHandles });
45738
46796
  }
45739
46797
  if (!opts.dryRun && newGroups.length > 0) {
45740
46798
  const bodies = await Promise.all(newGroups.map(async ({ displayName, upiHandles }) => {
@@ -45757,11 +46815,11 @@ async function backfillPersons(client, crypto3, opts = {}) {
45757
46815
  const created = newGroups.length;
45758
46816
  const links = [];
45759
46817
  if (!opts.dryRun) {
45760
- for (const [norm, g2] of groups) {
46818
+ for (const [norm, g3] of groups) {
45761
46819
  const personId = personIdByNorm.get(norm);
45762
46820
  if (!personId)
45763
46821
  continue;
45764
- for (const txnId of g2.txnIds)
46822
+ for (const txnId of g3.txnIds)
45765
46823
  links.push({ transactionId: txnId, personId });
45766
46824
  }
45767
46825
  }
@@ -45788,31 +46846,23 @@ function flagBatchDuplicates(txns) {
45788
46846
  return false;
45789
46847
  });
45790
46848
  }
46849
+ var fileRowsSchema = exports_external.array(exports_external.object({
46850
+ date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD"),
46851
+ amount: exports_external.number().positive(),
46852
+ type: exports_external.enum(["debit", "credit"]),
46853
+ description: exports_external.string(),
46854
+ referenceNumber: exports_external.string().optional()
46855
+ }));
45791
46856
  async function importTransactions(client, crypto3, opts) {
45792
46857
  let txns = opts.txns;
45793
46858
  let duplicatesSkipped = 0;
45794
46859
  if (txns.length > 0) {
45795
46860
  const dates = txns.map((t2) => t2.date).sort();
45796
- const existing = [];
45797
- let cursor;
45798
- for (let page = 0;page < 100; page++) {
45799
- const params = {
45800
- limit: 200,
45801
- startDate: dates[0],
45802
- endDate: dates[dates.length - 1]
45803
- };
45804
- if (cursor)
45805
- params.cursor = cursor;
45806
- const res = await client.get("/api/transactions", params);
45807
- const batch = res.data ?? [];
45808
- if (crypto3)
45809
- await decryptTransactionFields(batch, crypto3.key);
45810
- existing.push(...batch);
45811
- if (!res.hasMore || !res.nextCursor || batch.length === 0)
45812
- break;
45813
- cursor = res.nextCursor;
45814
- }
45815
- const seen = new Set(existing.map((r2) => dedupeKey(String(r2.date), String(r2.description ?? ""), Number(r2.amount), String(r2.type))));
46861
+ const existing = await fetchAllRows(client, crypto3, {
46862
+ startDate: dates[0],
46863
+ endDate: dates[dates.length - 1]
46864
+ });
46865
+ const seen = new Set(existing.map((r2) => dedupeKey(r2.date, r2.description, r2.amount, r2.type)));
45816
46866
  const before = txns.length;
45817
46867
  txns = txns.filter((t2) => !seen.has(dedupeKey(t2.date, t2.description ?? "", t2.amount, t2.type)));
45818
46868
  duplicatesSkipped = before - txns.length;
@@ -45840,16 +46890,9 @@ async function importTransactions(client, crypto3, opts) {
45840
46890
  return { imported, duplicatesSkipped, duplicatesFlagged };
45841
46891
  }
45842
46892
  async function runRescan(client, crypto3) {
45843
- const EMPTY_BAG2 = {
45844
- merchantAliases: [],
45845
- personAliases: [],
45846
- merchantRules: {},
45847
- personRules: {}
45848
- };
45849
- let bag = EMPTY_BAG2;
46893
+ let bag = structuredClone(EMPTY_BAG);
45850
46894
  try {
45851
- const learningRes = await client.get("/api/household-learning");
45852
- bag = learningRes.data?.bag ?? EMPTY_BAG2;
46895
+ bag = (await loadBag(client)).bag;
45853
46896
  } catch {}
45854
46897
  const merchantsRes = await client.get("/api/merchants");
45855
46898
  const allMerchants = merchantsRes.data ?? [];
@@ -45859,23 +46902,8 @@ async function runRescan(client, crypto3) {
45859
46902
  const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45860
46903
  const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
45861
46904
  const persons = await loadDecryptedPersons(client, crypto3);
45862
- const allRows = [];
45863
- const limit = 200;
45864
- let cursor;
45865
- const MAX_PAGES = 100;
45866
- for (let page = 0;page < MAX_PAGES; page++) {
45867
- const params = { limit };
45868
- if (cursor)
45869
- params.cursor = cursor;
45870
- const res = await client.get("/api/transactions", params);
45871
- const rows = res.data ?? [];
45872
- if (crypto3)
45873
- await decryptTransactionFields(rows, crypto3.key);
45874
- allRows.push(...rows);
45875
- if (!res.hasMore || !res.nextCursor || rows.length === 0)
45876
- break;
45877
- cursor = res.nextCursor;
45878
- }
46905
+ const allRows = await fetchAllRows(client, crypto3, { unresolved: "true" });
46906
+ const categoryByTxn = new Map(allRows.map((r2) => [r2.id, r2.categoryId]));
45879
46907
  const plan = autoCategorizeFromDescriptions({
45880
46908
  rows: allRows,
45881
46909
  bag,
@@ -45886,25 +46914,23 @@ async function runRescan(client, crypto3) {
45886
46914
  let categorized = 0;
45887
46915
  const links = plan.suggestions.flatMap((s2) => {
45888
46916
  const categoryId = s2.categoryId ?? (s2.entity.type === "merchant" ? merchantCategoryById.get(s2.entity.merchantId) ?? null : null);
45889
- if (categoryId)
45890
- categorized += s2.transactionIds.length;
45891
- return s2.transactionIds.map((txId) => ({
45892
- transactionId: txId,
45893
- ...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
45894
- ...categoryId ? { categoryId } : {}
45895
- }));
46917
+ return s2.transactionIds.map((txId) => {
46918
+ const applyCategory = categoryId && !categoryByTxn.get(txId);
46919
+ if (applyCategory)
46920
+ categorized += 1;
46921
+ return {
46922
+ transactionId: txId,
46923
+ ...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
46924
+ ...applyCategory ? { categoryId } : {}
46925
+ };
46926
+ });
45896
46927
  });
45897
- if (links.length > 0) {
45898
- const CHUNK = 500;
45899
- for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
45900
- await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
45901
- }
45902
- }
46928
+ await postLinks(client, links);
45903
46929
  return { linked: links.length, categorized, unresolved: plan.unresolvedCount };
45904
46930
  }
45905
46931
  function registerTransactionTools(server, client, crypto3) {
45906
46932
  server.registerTool("get_transactions", {
45907
- description: "Search and filter transactions. Returns paginated results with merchant and category details. To find everything paid to a specific counterparty, pass `personName` (resolved to a person via the household's persons list) or `personId` directly — this is an indexed lookup, not a description scan. Note: free-text `search` only matches plaintext merchant names, not encrypted descriptions.",
46933
+ description: "Search/filter transactions. Returns COMPACT rows {id,date,amt,t:'dr'|'cr',desc,cat,merchant,person,flags} " + "(default 25, max 100) plus `more`/`cursor` for the next page and, on page 1, `spent`/`income` totals over the whole filter. " + "To find everything paid to a counterparty pass `personName` or `personId` (indexed). " + "Free-text `search` only matches plaintext merchant names, not encrypted descriptions. " + "Pass `full: true` for the raw joined rows (much larger — avoid unless needed).",
45908
46934
  inputSchema: {
45909
46935
  search: exports_external.string().optional().describe("Search merchant name (NOT encrypted descriptions)"),
45910
46936
  personId: exports_external.string().optional().describe("Filter to one person (exact, indexed)"),
@@ -45919,64 +46945,106 @@ function registerTransactionTools(server, client, crypto3) {
45919
46945
  amountMin: exports_external.number().optional(),
45920
46946
  amountMax: exports_external.number().optional(),
45921
46947
  uncategorized: exports_external.boolean().optional().describe("Only transactions with no category (server-side filter)"),
45922
- limit: exports_external.number().optional().describe("Max results (default 50, max 200)"),
45923
- cursor: exports_external.string().optional().describe("Opaque cursor from a prior page's nextCursor")
46948
+ limit: exports_external.number().optional().describe("Max results (default 25, max 100)"),
46949
+ cursor: exports_external.string().optional().describe("Opaque cursor from a prior page's `cursor`"),
46950
+ full: exports_external.boolean().optional().describe("Return raw joined rows instead of compact rows")
45924
46951
  }
45925
46952
  }, async (params) => {
45926
- const { personName, categorySlug, ...rest } = params;
46953
+ const { personName, categorySlug, full, limit, ...rest } = params;
45927
46954
  const apiParams = { ...rest };
45928
46955
  if (apiParams.uncategorized !== true)
45929
46956
  delete apiParams.uncategorized;
45930
46957
  if (categorySlug) {
45931
- const cats = await client.get("/api/categories");
45932
- const all2 = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
45933
- const match = all2.find((c2) => c2.slug === categorySlug);
45934
- if (match)
45935
- apiParams.categoryId = match.id;
46958
+ apiParams.categoryId = requireCategory(await loadCategoryIndex(client), categorySlug).id;
45936
46959
  }
45937
46960
  if (personName && !apiParams.personId) {
45938
46961
  const persons = await loadDecryptedPersons(client, crypto3);
45939
46962
  const match = resolvePersonByName(persons, personName);
46963
+ const err = (error2) => ({
46964
+ content: [{ type: "text", text: JSON.stringify({ error: error2 }) }]
46965
+ });
45940
46966
  if (!match) {
45941
- return {
45942
- content: [
45943
- {
45944
- type: "text",
45945
- text: JSON.stringify({
45946
- error: `No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`
45947
- })
45948
- }
45949
- ]
45950
- };
46967
+ return err(`No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`);
45951
46968
  }
45952
46969
  if ("candidates" in match) {
45953
- return {
45954
- content: [
45955
- {
45956
- type: "text",
45957
- text: JSON.stringify({
45958
- error: `Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`
45959
- })
45960
- }
45961
- ]
45962
- };
46970
+ return err(`Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`);
45963
46971
  }
45964
46972
  apiParams.personId = match.personId;
45965
46973
  }
46974
+ apiParams.limit = Math.min(Math.max(limit ?? 25, 1), full ? 200 : 100);
46975
+ if (!full)
46976
+ apiParams.view = "lean";
45966
46977
  const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
45967
- if (crypto3 && data.data) {
45968
- await decryptTransactionFields(data.data, crypto3.key);
45969
- if (data.data.some((r2) => r2.personId)) {
45970
- const persons = await loadDecryptedPersons(client, crypto3);
45971
- const nameById = new Map(persons.map((p2) => [p2.id, p2.name]));
45972
- for (const r2 of data.data) {
45973
- const person = r2.person;
45974
- if (person && nameById.has(person.id))
45975
- person.name = nameById.get(person.id);
45976
- }
45977
- }
45978
- }
45979
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
46978
+ const rows = data.data ?? [];
46979
+ if (crypto3)
46980
+ await decryptTransactionFields(rows, crypto3.key);
46981
+ const personNames = new Map;
46982
+ if (rows.some((r2) => r2.personId)) {
46983
+ for (const p2 of await loadDecryptedPersons(client, crypto3))
46984
+ personNames.set(p2.id, p2.name);
46985
+ }
46986
+ if (full) {
46987
+ for (const r2 of rows) {
46988
+ const person = r2.person;
46989
+ if (person && personNames.has(person.id))
46990
+ person.name = personNames.get(person.id) ?? "";
46991
+ }
46992
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
46993
+ }
46994
+ const out = {
46995
+ rows: rows.map((r2) => toLeanRow(r2, personNames)),
46996
+ ...data.hasMore ? { more: true, cursor: data.nextCursor } : {},
46997
+ ...data.summary ? { spent: data.summary.spent, income: data.summary.income } : {}
46998
+ };
46999
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
47000
+ });
47001
+ server.registerTool("get_uncategorized_groups", {
47002
+ description: "Token-cheap way to categorize: groups still-uncategorized transactions by merchant-like pattern " + "(descriptions are decrypted locally in private mode) and returns only " + "{pattern,t:'dr'|'cr',n,total,sample} per group, biggest ₹ first — never individual rows. " + "Then call `categorize_groups` with a category per pattern. Skip tiny groups via `minTotal`.",
47003
+ inputSchema: {
47004
+ startDate: exports_external.string().optional().describe("YYYY-MM-DD"),
47005
+ endDate: exports_external.string().optional().describe("YYYY-MM-DD"),
47006
+ minTotal: exports_external.number().optional().describe("Hide groups whose total ₹ is below this"),
47007
+ limit: exports_external.number().optional().describe("Max groups (default 40, max 100)")
47008
+ }
47009
+ }, async ({ startDate, endDate, minTotal, limit }) => {
47010
+ const rows = await fetchUncategorized(client, crypto3, { startDate, endDate });
47011
+ const all2 = groupRows(rows);
47012
+ const shown = all2.filter((g3) => g3.total >= (minTotal ?? 0)).slice(0, Math.min(Math.max(limit ?? 40, 1), 100));
47013
+ const shownRows = shown.reduce((n2, g3) => n2 + g3.rows.length, 0);
47014
+ const out = {
47015
+ groups: shown.map((g3) => ({
47016
+ pattern: g3.pattern,
47017
+ t: g3.type === "credit" ? "cr" : "dr",
47018
+ n: g3.rows.length,
47019
+ total: Math.round(g3.total),
47020
+ sample: g3.rows[0].description.slice(0, 50)
47021
+ })),
47022
+ rows: rows.length,
47023
+ groupsTotal: all2.length,
47024
+ rowsCovered: shownRows
47025
+ };
47026
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
47027
+ });
47028
+ server.registerTool("categorize_groups", {
47029
+ description: "Assign a category to whole groups from `get_uncategorized_groups` in ONE call. " + "For each pattern it categorizes every matching uncategorized transaction and saves a learning rule " + "(merchant + alias + category) so future imports auto-categorize it. " + "Unknown categorySlug fails before any write. Use dryRun to preview.",
47030
+ inputSchema: {
47031
+ assignments: exports_external.array(exports_external.object({
47032
+ pattern: exports_external.string().describe("Group pattern exactly as returned"),
47033
+ type: exports_external.enum(["debit", "credit"]).optional().describe("Limit to debits or credits"),
47034
+ categorySlug: exports_external.string().describe("Category slug (see list_categories)"),
47035
+ learn: exports_external.boolean().optional().describe("false = don't save a reusable rule (use for short/generic patterns)")
47036
+ })).min(1).max(100),
47037
+ startDate: exports_external.string().optional().describe("Same range used when listing groups"),
47038
+ endDate: exports_external.string().optional(),
47039
+ dryRun: exports_external.boolean().optional()
47040
+ }
47041
+ }, async ({ assignments, startDate, endDate, dryRun }) => {
47042
+ const result = await applyGroupAssignments(client, crypto3, assignments, {
47043
+ dryRun,
47044
+ startDate,
47045
+ endDate
47046
+ });
47047
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
45980
47048
  });
45981
47049
  server.registerTool("backfill_persons", {
45982
47050
  description: "Extract UPI/IMPS counterparties from unlinked transactions, create person records for them (deduped by name), and link transactions via personId — so you can later filter all payments to a person. Idempotent: re-running reuses existing persons and skips already-linked rows. Pass dryRun to preview without writing.",
@@ -46005,13 +47073,9 @@ function registerTransactionTools(server, client, crypto3) {
46005
47073
  if (crypto3 && body.notes)
46006
47074
  body.notes = await encryptField(body.notes, crypto3.key);
46007
47075
  if (body.categorySlug) {
46008
- const cats = await client.get("/api/categories");
46009
- const allCats = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
46010
- const cat = allCats.find((c2) => c2.slug === body.categorySlug);
46011
- if (cat) {
46012
- body.categoryId = cat.id;
46013
- body.categoryType = cat.type;
46014
- }
47076
+ const cat = requireCategory(await loadCategoryIndex(client), body.categorySlug);
47077
+ body.categoryId = cat.id;
47078
+ body.categoryType = cat.type;
46015
47079
  delete body.categorySlug;
46016
47080
  }
46017
47081
  const data = await client.patch(`/api/transactions/${id}`, body);
@@ -46050,8 +47114,8 @@ function registerTransactionTools(server, client, crypto3) {
46050
47114
  }
46051
47115
  if (action === "list") {
46052
47116
  const data2 = await client.get("/api/transactions/duplicates");
46053
- if (crypto3 && data2.duplicates)
46054
- await decryptTransactionFields(data2.duplicates, crypto3.key);
47117
+ if (crypto3 && data2.data)
47118
+ await decryptTransactionFields(data2.data, crypto3.key);
46055
47119
  return { content: [{ type: "text", text: JSON.stringify(data2) }] };
46056
47120
  }
46057
47121
  const data = await client.post("/api/transactions/duplicates/resolve", {
@@ -46080,7 +47144,7 @@ function registerTransactionTools(server, client, crypto3) {
46080
47144
  source: exports_external.enum(["bank_statement", "cc_statement", "manual", "telegram", "wisprflow", "ai_import"]).describe("Origin of the statement"),
46081
47145
  granularity: exports_external.number().int().min(1).max(5).describe("Source trust level: 1=bank statement, 2=CC statement, 3=manual, 4=app export, 5=telegram/AI"),
46082
47146
  transactions: exports_external.array(exports_external.object({
46083
- date: exports_external.string().describe("YYYY-MM-DD"),
47147
+ date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD"),
46084
47148
  amount: exports_external.number().positive(),
46085
47149
  type: exports_external.enum(["debit", "credit"]),
46086
47150
  description: exports_external.string(),
@@ -46119,7 +47183,12 @@ function registerTransactionTools(server, client, crypto3) {
46119
47183
  const parsed = JSON.parse(raw);
46120
47184
  if (!Array.isArray(parsed))
46121
47185
  throw new Error("File must contain a JSON array");
46122
- txns = parsed;
47186
+ const result = fileRowsSchema.safeParse(parsed);
47187
+ if (!result.success) {
47188
+ const i2 = result.error.issues[0];
47189
+ throw new Error(`row ${String(i2.path[0])}: ${i2.path.slice(1).join(".")} ${i2.message}`);
47190
+ }
47191
+ txns = result.data;
46123
47192
  } catch (e2) {
46124
47193
  throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
46125
47194
  }
@@ -46148,9 +47217,13 @@ function registerTransactionTools(server, client, crypto3) {
46148
47217
  })).min(1).max(500)
46149
47218
  }
46150
47219
  }, async ({ links }) => {
46151
- const catsRes = await client.get("/api/categories");
46152
- const allCats = catsRes.data.flatMap((c2) => [c2, ...c2.children ?? []]);
46153
- const typeById = new Map(allCats.map((c2) => [c2.id, c2.type]));
47220
+ const catIndex = await loadCategoryIndex(client);
47221
+ const typeById = new Map([...catIndex.byId].map(([id, c2]) => [id, c2.type]));
47222
+ const unknown3 = [
47223
+ ...new Set(links.map((l2) => l2.categoryId))
47224
+ ].filter((id) => !typeById.has(id));
47225
+ if (unknown3.length)
47226
+ throw new Error(`Unknown categoryId(s): ${unknown3.join(", ")}`);
46154
47227
  const groups = new Map;
46155
47228
  for (const { transactionId, categoryId } of links) {
46156
47229
  const arr = groups.get(categoryId) ?? [];
@@ -46201,11 +47274,18 @@ function registerTransactionTools(server, client, crypto3) {
46201
47274
  }).optional().describe("Filter params — deletes all matching transactions (provide one of id / ids / filters)")
46202
47275
  }
46203
47276
  }, async ({ id, ids, filters }) => {
47277
+ const given = [id, ids, filters].filter((v2) => v2 !== undefined).length;
47278
+ if (given !== 1) {
47279
+ throw new Error("Provide exactly one of id, ids, or filters");
47280
+ }
47281
+ if (filters && Object.values(filters).every((v2) => v2 === undefined)) {
47282
+ throw new Error("filters is empty — refusing to delete every transaction");
47283
+ }
46204
47284
  if (id) {
46205
47285
  const data2 = await client.delete(`/api/transactions/${id}`);
46206
47286
  return { content: [{ type: "text", text: JSON.stringify(data2) }] };
46207
47287
  }
46208
- const body = ids ? { ids } : { filters: filters ?? {} };
47288
+ const body = ids ? { ids } : { filters };
46209
47289
  const data = await client.delete("/api/transactions/bulk", body);
46210
47290
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
46211
47291
  });
@@ -46288,6 +47368,7 @@ async function main() {
46288
47368
  version: "0.0.1"
46289
47369
  });
46290
47370
  registerTransactionTools(server, client, crypto3);
47371
+ registerJevTools(server, client, crypto3);
46291
47372
  registerMerchantTools(server, client, crypto3);
46292
47373
  registerPersonTools(server, client, crypto3);
46293
47374
  registerCategoryTools(server, client);
@@ -46321,5 +47402,5 @@ main().catch((err) => {
46321
47402
  process.exit(1);
46322
47403
  });
46323
47404
 
46324
- //# debugId=1EEBC94B29D2067F64756E2164756E21
47405
+ //# debugId=81E220C7BA0CDE2464756E2164756E21
46325
47406
  //# sourceMappingURL=index.js.map