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 +1614 -533
- package/dist/index.js.map +20 -12
- package/package.json +2 -1
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") {}
|
|
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
|
-
}
|
|
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/
|
|
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
|
-
//
|
|
43030
|
-
|
|
43031
|
-
|
|
43032
|
-
|
|
43033
|
-
|
|
43034
|
-
|
|
43035
|
-
|
|
43036
|
-
|
|
43037
|
-
|
|
43038
|
-
|
|
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
|
-
|
|
43042
|
-
|
|
43043
|
-
|
|
43044
|
-
|
|
43045
|
-
|
|
43046
|
-
|
|
43047
|
-
|
|
43048
|
-
|
|
43049
|
-
|
|
43050
|
-
|
|
43051
|
-
|
|
43052
|
-
|
|
43053
|
-
|
|
43054
|
-
|
|
43055
|
-
|
|
43056
|
-
|
|
43057
|
-
|
|
43058
|
-
|
|
43059
|
-
|
|
43060
|
-
|
|
43061
|
-
|
|
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
|
-
|
|
43064
|
-
|
|
43065
|
-
|
|
43066
|
-
|
|
43067
|
-
|
|
43068
|
-
|
|
43069
|
-
|
|
43070
|
-
|
|
43071
|
-
|
|
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
|
-
}
|
|
43074
|
-
|
|
43075
|
-
|
|
43076
|
-
|
|
43077
|
-
|
|
43078
|
-
|
|
43079
|
-
|
|
43080
|
-
|
|
43081
|
-
|
|
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
|
-
|
|
43085
|
-
|
|
43086
|
-
|
|
43087
|
-
|
|
43088
|
-
|
|
43089
|
-
|
|
43090
|
-
|
|
43091
|
-
|
|
43092
|
-
|
|
43093
|
-
|
|
43094
|
-
|
|
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
|
-
}
|
|
43098
|
-
|
|
43099
|
-
|
|
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
|
-
|
|
43102
|
-
return
|
|
43103
|
-
}
|
|
43104
|
-
|
|
43619
|
+
try {
|
|
43620
|
+
return JSON.parse(text);
|
|
43621
|
+
} catch {
|
|
43622
|
+
return text;
|
|
43623
|
+
}
|
|
43624
|
+
};
|
|
43105
43625
|
|
|
43106
|
-
// src/
|
|
43107
|
-
function
|
|
43108
|
-
|
|
43109
|
-
|
|
43110
|
-
|
|
43111
|
-
|
|
43112
|
-
|
|
43113
|
-
|
|
43114
|
-
|
|
43115
|
-
|
|
43116
|
-
|
|
43117
|
-
|
|
43118
|
-
|
|
43119
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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 = ($ =
|
|
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
|
|
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 =
|
|
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
|
|
44287
|
-
|
|
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,
|
|
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 (;
|
|
44661
|
-
for (p2 = t2.charCodeAt(
|
|
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 (;
|
|
44665
|
-
for (p2 = t2.charCodeAt(
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(),
|
|
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,
|
|
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 =
|
|
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:
|
|
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
|
-
//
|
|
45409
|
-
var
|
|
45410
|
-
|
|
45411
|
-
|
|
45412
|
-
|
|
45413
|
-
|
|
45414
|
-
|
|
45415
|
-
|
|
45416
|
-
|
|
45417
|
-
|
|
45418
|
-
|
|
45419
|
-
|
|
45420
|
-
|
|
45421
|
-
|
|
45422
|
-
|
|
45423
|
-
|
|
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 (
|
|
45448
|
-
|
|
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
|
-
|
|
45451
|
-
|
|
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
|
-
|
|
45454
|
-
|
|
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
|
-
|
|
45457
|
-
|
|
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 (
|
|
45460
|
-
|
|
45461
|
-
|
|
45462
|
-
|
|
45463
|
-
|
|
45464
|
-
|
|
45465
|
-
|
|
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 (
|
|
45469
|
-
return
|
|
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
|
|
46016
|
+
return result;
|
|
45472
46017
|
}
|
|
45473
|
-
|
|
45474
|
-
|
|
45475
|
-
|
|
45476
|
-
|
|
45477
|
-
|
|
45478
|
-
|
|
45479
|
-
|
|
45480
|
-
|
|
45481
|
-
|
|
45482
|
-
|
|
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
|
|
45498
|
-
|
|
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
|
|
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
|
-
|
|
45718
|
-
|
|
45719
|
-
groups.set(norm,
|
|
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,
|
|
45725
|
-
const 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:
|
|
46786
|
+
name: g3.displayName,
|
|
45729
46787
|
normalized: norm,
|
|
45730
|
-
txnCount:
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
45798
|
-
|
|
45799
|
-
|
|
45800
|
-
|
|
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
|
-
|
|
45844
|
-
merchantAliases: [],
|
|
45845
|
-
personAliases: [],
|
|
45846
|
-
merchantRules: {},
|
|
45847
|
-
personRules: {}
|
|
45848
|
-
};
|
|
45849
|
-
let bag = EMPTY_BAG2;
|
|
46893
|
+
let bag = structuredClone(EMPTY_BAG);
|
|
45850
46894
|
try {
|
|
45851
|
-
|
|
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
|
|
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
|
-
|
|
45890
|
-
|
|
45891
|
-
|
|
45892
|
-
|
|
45893
|
-
|
|
45894
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
45923
|
-
cursor: exports_external.string().optional().describe("Opaque cursor from a prior page's
|
|
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
|
-
|
|
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
|
-
|
|
45968
|
-
|
|
45969
|
-
|
|
45970
|
-
|
|
45971
|
-
|
|
45972
|
-
|
|
45973
|
-
|
|
45974
|
-
|
|
45975
|
-
|
|
45976
|
-
|
|
45977
|
-
|
|
45978
|
-
|
|
45979
|
-
|
|
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
|
|
46009
|
-
|
|
46010
|
-
|
|
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.
|
|
46054
|
-
await decryptTransactionFields(data2.
|
|
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().
|
|
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
|
-
|
|
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
|
|
46152
|
-
const
|
|
46153
|
-
const
|
|
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
|
|
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=
|
|
47405
|
+
//# debugId=81E220C7BA0CDE2464756E2164756E21
|
|
46325
47406
|
//# sourceMappingURL=index.js.map
|