paisa-mcp 0.0.19 → 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 +1615 -524
- package/dist/index.js.map +21 -13
- 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");
|
|
@@ -42554,18 +42554,28 @@ function registerDebtTools(server, client) {
|
|
|
42554
42554
|
dueDate: exports_external.string().optional().describe("YYYY-MM-DD"),
|
|
42555
42555
|
notes: exports_external.string().optional()
|
|
42556
42556
|
}
|
|
42557
|
-
}, async (
|
|
42558
|
-
const
|
|
42557
|
+
}, async ({ name, amount, type, dueDate, notes }) => {
|
|
42558
|
+
const notesWithDue = dueDate ? [notes, `Due: ${dueDate}`].filter(Boolean).join(" — ") : notes;
|
|
42559
|
+
const data = await client.post("/api/debts", {
|
|
42560
|
+
name,
|
|
42561
|
+
direction: type,
|
|
42562
|
+
type: "other",
|
|
42563
|
+
outstandingBalance: amount,
|
|
42564
|
+
...notesWithDue ? { notes: notesWithDue } : {}
|
|
42565
|
+
});
|
|
42559
42566
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
42560
42567
|
});
|
|
42561
42568
|
server.registerTool("settle_debt", {
|
|
42562
|
-
description: "Mark a debt as settled.",
|
|
42569
|
+
description: "Mark a debt as settled (soft-deactivates it; zeroes the balance).",
|
|
42563
42570
|
inputSchema: {
|
|
42564
42571
|
id: exports_external.string(),
|
|
42565
42572
|
settledAmount: exports_external.string().optional().describe("Actual settled amount if different")
|
|
42566
42573
|
}
|
|
42567
|
-
}, async ({ id,
|
|
42568
|
-
const data = await client.patch(`/api/debts/${id}`, {
|
|
42574
|
+
}, async ({ id, settledAmount }) => {
|
|
42575
|
+
const data = await client.patch(`/api/debts/${id}`, {
|
|
42576
|
+
isActive: false,
|
|
42577
|
+
outstandingBalance: settledAmount ?? "0"
|
|
42578
|
+
});
|
|
42569
42579
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
42570
42580
|
});
|
|
42571
42581
|
}
|
|
@@ -42763,7 +42773,7 @@ function registerHeatmapTools(server, client) {
|
|
|
42763
42773
|
});
|
|
42764
42774
|
}
|
|
42765
42775
|
|
|
42766
|
-
// src/
|
|
42776
|
+
// src/lib/learning-bag.ts
|
|
42767
42777
|
var EMPTY_BAG = {
|
|
42768
42778
|
merchantAliases: [],
|
|
42769
42779
|
personAliases: [],
|
|
@@ -42777,12 +42787,11 @@ async function loadBag(client) {
|
|
|
42777
42787
|
return { bag: res.data.bag, version: res.data.version };
|
|
42778
42788
|
}
|
|
42779
42789
|
async function saveBag(client, bag, expectedVersion) {
|
|
42780
|
-
const res = await client.put("/api/household-learning", {
|
|
42781
|
-
bag,
|
|
42782
|
-
expectedVersion
|
|
42783
|
-
});
|
|
42790
|
+
const res = await client.put("/api/household-learning", { bag, expectedVersion });
|
|
42784
42791
|
return { version: res.data.version };
|
|
42785
42792
|
}
|
|
42793
|
+
|
|
42794
|
+
// src/tools/household-learning.ts
|
|
42786
42795
|
function registerHouseholdLearningTools(server, client, _crypto) {
|
|
42787
42796
|
server.registerTool("list_learning", {
|
|
42788
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.",
|
|
@@ -43016,309 +43025,630 @@ function registerInvestmentTools(server, client) {
|
|
|
43016
43025
|
});
|
|
43017
43026
|
}
|
|
43018
43027
|
|
|
43019
|
-
//
|
|
43020
|
-
|
|
43021
|
-
|
|
43022
|
-
|
|
43023
|
-
|
|
43024
|
-
|
|
43025
|
-
|
|
43026
|
-
|
|
43027
|
-
|
|
43028
|
-
|
|
43029
|
-
}
|
|
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;
|
|
43030
43038
|
}
|
|
43031
|
-
|
|
43032
|
-
|
|
43033
|
-
|
|
43034
|
-
|
|
43035
|
-
|
|
43036
|
-
|
|
43037
|
-
|
|
43038
|
-
|
|
43039
|
-
|
|
43040
|
-
|
|
43041
|
-
|
|
43042
|
-
|
|
43043
|
-
|
|
43044
|
-
|
|
43045
|
-
|
|
43046
|
-
|
|
43047
|
-
|
|
43048
|
-
|
|
43049
|
-
|
|
43050
|
-
|
|
43051
|
-
|
|
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;
|
|
43052
43547
|
}
|
|
43053
|
-
|
|
43054
|
-
|
|
43055
|
-
|
|
43056
|
-
|
|
43057
|
-
|
|
43058
|
-
|
|
43059
|
-
|
|
43060
|
-
|
|
43061
|
-
|
|
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);
|
|
43062
43558
|
}
|
|
43063
|
-
}
|
|
43064
|
-
|
|
43065
|
-
|
|
43066
|
-
|
|
43067
|
-
|
|
43068
|
-
|
|
43069
|
-
|
|
43070
|
-
|
|
43071
|
-
|
|
43072
|
-
|
|
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);
|
|
43073
43594
|
}
|
|
43074
|
-
|
|
43075
|
-
|
|
43076
|
-
|
|
43077
|
-
|
|
43078
|
-
|
|
43079
|
-
|
|
43080
|
-
|
|
43081
|
-
|
|
43082
|
-
|
|
43083
|
-
|
|
43084
|
-
|
|
43085
|
-
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 });
|
|
43086
43606
|
}
|
|
43087
|
-
}
|
|
43088
|
-
|
|
43089
|
-
|
|
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;
|
|
43090
43618
|
}
|
|
43091
|
-
|
|
43092
|
-
return
|
|
43093
|
-
}
|
|
43094
|
-
|
|
43619
|
+
try {
|
|
43620
|
+
return JSON.parse(text);
|
|
43621
|
+
} catch {
|
|
43622
|
+
return text;
|
|
43623
|
+
}
|
|
43624
|
+
};
|
|
43095
43625
|
|
|
43096
|
-
// src/
|
|
43097
|
-
function
|
|
43098
|
-
|
|
43099
|
-
|
|
43100
|
-
|
|
43101
|
-
|
|
43102
|
-
|
|
43103
|
-
|
|
43104
|
-
|
|
43105
|
-
|
|
43106
|
-
|
|
43107
|
-
|
|
43108
|
-
|
|
43109
|
-
|
|
43110
|
-
year: exports_external.number().min(2020).max(2100),
|
|
43111
|
-
liquidCash: exports_external.string().optional(),
|
|
43112
|
-
investments: exports_external.string().optional(),
|
|
43113
|
-
emergencyFund: exports_external.string().optional(),
|
|
43114
|
-
liabilities: exports_external.string().optional(),
|
|
43115
|
-
breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
43116
|
-
}
|
|
43117
|
-
}, async ({ mode, ...body }) => {
|
|
43118
|
-
let data;
|
|
43119
|
-
if (mode === "auto") {
|
|
43120
|
-
data = await client.post("/api/net-worth/auto", { month: body.month, year: body.year });
|
|
43121
|
-
} else {
|
|
43122
|
-
const liquidCash = parseFloat(body.liquidCash ?? "0");
|
|
43123
|
-
const investments = parseFloat(body.investments ?? "0");
|
|
43124
|
-
const emergencyFund = parseFloat(body.emergencyFund ?? "0");
|
|
43125
|
-
const liabilities = parseFloat(body.liabilities ?? "0");
|
|
43126
|
-
const totalNetWorth = (liquidCash + investments + emergencyFund - liabilities).toFixed(2);
|
|
43127
|
-
data = await client.post("/api/net-worth", {
|
|
43128
|
-
...body,
|
|
43129
|
-
emergencyFund: body.emergencyFund ?? "0",
|
|
43130
|
-
totalNetWorth
|
|
43131
|
-
});
|
|
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 });
|
|
43132
43640
|
}
|
|
43133
|
-
|
|
43134
|
-
}
|
|
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 };
|
|
43135
43650
|
}
|
|
43136
43651
|
|
|
43137
|
-
// src/tools/persons.ts
|
|
43138
|
-
function registerPersonTools(server, client, crypto3) {
|
|
43139
|
-
server.registerTool("upsert_person", {
|
|
43140
|
-
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.",
|
|
43141
|
-
inputSchema: {
|
|
43142
|
-
id: exports_external.string().optional().describe("Person UUID — pass to update, omit to create"),
|
|
43143
|
-
name: exports_external.string().optional().describe('Display name (e.g. "Priya") — required when creating'),
|
|
43144
|
-
upiHandles: exports_external.array(exports_external.string()).optional().describe("UPI ids the person uses"),
|
|
43145
|
-
phoneNumbers: exports_external.array(exports_external.string()).optional(),
|
|
43146
|
-
relationship: exports_external.string().optional().describe('e.g. "sister", "friend", "landlord"'),
|
|
43147
|
-
notes: exports_external.string().optional()
|
|
43148
|
-
}
|
|
43149
|
-
}, async ({ id, ...updates }) => {
|
|
43150
|
-
if (crypto3) {
|
|
43151
|
-
if (id) {
|
|
43152
|
-
const existing = await client.get(`/api/persons/${id}`);
|
|
43153
|
-
const personData = existing.data;
|
|
43154
|
-
let current;
|
|
43155
|
-
if (personData.cipher) {
|
|
43156
|
-
current = await decryptPersonCipher(personData.cipher, crypto3.key);
|
|
43157
|
-
} else {
|
|
43158
|
-
current = {
|
|
43159
|
-
name: personData.name,
|
|
43160
|
-
upiHandles: personData.upiHandles,
|
|
43161
|
-
phoneNumbers: personData.phoneNumbers,
|
|
43162
|
-
relationship: personData.relationship,
|
|
43163
|
-
notes: personData.notes
|
|
43164
|
-
};
|
|
43165
|
-
}
|
|
43166
|
-
const merged = {
|
|
43167
|
-
name: updates.name ?? current.name,
|
|
43168
|
-
upiHandles: updates.upiHandles ?? current.upiHandles,
|
|
43169
|
-
phoneNumbers: updates.phoneNumbers ?? current.phoneNumbers,
|
|
43170
|
-
relationship: updates.relationship ?? current.relationship ?? null,
|
|
43171
|
-
notes: updates.notes ?? current.notes ?? null
|
|
43172
|
-
};
|
|
43173
|
-
const payload2 = await encryptPersonCipher(merged, crypto3.key);
|
|
43174
|
-
const data3 = await client.patch(`/api/persons/${id}`, payload2);
|
|
43175
|
-
return { content: [{ type: "text", text: JSON.stringify(data3) }] };
|
|
43176
|
-
}
|
|
43177
|
-
if (!updates.name)
|
|
43178
|
-
throw new Error("name is required when creating a person");
|
|
43179
|
-
const fields = {
|
|
43180
|
-
name: updates.name,
|
|
43181
|
-
upiHandles: updates.upiHandles ?? [],
|
|
43182
|
-
phoneNumbers: updates.phoneNumbers ?? [],
|
|
43183
|
-
relationship: updates.relationship ?? null,
|
|
43184
|
-
notes: updates.notes ?? null
|
|
43185
|
-
};
|
|
43186
|
-
const payload = await encryptPersonCipher(fields, crypto3.key);
|
|
43187
|
-
const data2 = await client.post("/api/persons", payload);
|
|
43188
|
-
return { content: [{ type: "text", text: JSON.stringify(data2) }] };
|
|
43189
|
-
}
|
|
43190
|
-
const data = id ? await client.patch(`/api/persons/${id}`, updates) : await client.post("/api/persons", updates);
|
|
43191
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43192
|
-
});
|
|
43193
|
-
server.registerTool("list_persons", {
|
|
43194
|
-
description: "List all persons in the current household. Pass `id` to get a single person by id instead of listing all.",
|
|
43195
|
-
inputSchema: {
|
|
43196
|
-
id: exports_external.string().optional().describe("Person UUID to fetch a single person; omit to list all")
|
|
43197
|
-
}
|
|
43198
|
-
}, async ({ id }) => {
|
|
43199
|
-
if (id) {
|
|
43200
|
-
const data2 = await client.get(`/api/persons/${id}`);
|
|
43201
|
-
if (crypto3 && data2.data.cipher) {
|
|
43202
|
-
const fields = await decryptPersonCipher(data2.data.cipher, crypto3.key);
|
|
43203
|
-
Object.assign(data2.data, fields, { cipher: null });
|
|
43204
|
-
}
|
|
43205
|
-
return { content: [{ type: "text", text: JSON.stringify(data2) }] };
|
|
43206
|
-
}
|
|
43207
|
-
const data = await client.get("/api/persons");
|
|
43208
|
-
if (crypto3 && data.data) {
|
|
43209
|
-
for (const person of data.data) {
|
|
43210
|
-
const cipher = person.cipher;
|
|
43211
|
-
if (cipher) {
|
|
43212
|
-
try {
|
|
43213
|
-
const fields = await decryptPersonCipher(cipher, crypto3.key);
|
|
43214
|
-
Object.assign(person, fields, { cipher: null });
|
|
43215
|
-
} catch {}
|
|
43216
|
-
}
|
|
43217
|
-
}
|
|
43218
|
-
}
|
|
43219
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43220
|
-
});
|
|
43221
|
-
server.registerTool("delete_person", {
|
|
43222
|
-
description: "Delete a person. Returns 409 if any transactions still reference this person.",
|
|
43223
|
-
inputSchema: { id: exports_external.string().describe("Person UUID") }
|
|
43224
|
-
}, async ({ id }) => {
|
|
43225
|
-
const data = await client.delete(`/api/persons/${id}`);
|
|
43226
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43227
|
-
});
|
|
43228
|
-
}
|
|
43229
|
-
|
|
43230
|
-
// src/tools/profile.ts
|
|
43231
|
-
function registerProfileTools(server, client) {
|
|
43232
|
-
server.registerTool("get_profile", {
|
|
43233
|
-
description: "Get the current user's profile (name, email, owner).",
|
|
43234
|
-
inputSchema: {}
|
|
43235
|
-
}, async () => {
|
|
43236
|
-
const data = await client.get("/api/profile");
|
|
43237
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43238
|
-
});
|
|
43239
|
-
server.registerTool("update_profile", {
|
|
43240
|
-
description: "Update your display name.",
|
|
43241
|
-
inputSchema: {
|
|
43242
|
-
name: exports_external.string().min(1).describe("Display name")
|
|
43243
|
-
}
|
|
43244
|
-
}, async (body) => {
|
|
43245
|
-
const data = await client.patch("/api/profile", body);
|
|
43246
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43247
|
-
});
|
|
43248
|
-
}
|
|
43249
|
-
|
|
43250
|
-
// src/tools/push.ts
|
|
43251
|
-
function registerPushTools(server, client) {
|
|
43252
|
-
server.registerTool("list_push_subscriptions", {
|
|
43253
|
-
description: "List the household's registered web-push subscriptions (browser/device endpoints that receive alert notifications).",
|
|
43254
|
-
inputSchema: {}
|
|
43255
|
-
}, async () => {
|
|
43256
|
-
const data = await client.get("/api/push/subscriptions");
|
|
43257
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43258
|
-
});
|
|
43259
|
-
server.registerTool("send_test_push", {
|
|
43260
|
-
description: "Send a test web-push notification to all of the household's subscriptions. Use to verify push delivery is working.",
|
|
43261
|
-
inputSchema: {}
|
|
43262
|
-
}, async () => {
|
|
43263
|
-
const data = await client.post("/api/push/test");
|
|
43264
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43265
|
-
});
|
|
43266
|
-
}
|
|
43267
|
-
|
|
43268
|
-
// src/tools/recurring.ts
|
|
43269
|
-
function registerRecurringTools(server, client) {
|
|
43270
|
-
server.registerTool("list_recurring", {
|
|
43271
|
-
description: "List all recurring transactions (subscriptions, rent, salaries).",
|
|
43272
|
-
inputSchema: {}
|
|
43273
|
-
}, async () => {
|
|
43274
|
-
const data = await client.get("/api/recurring");
|
|
43275
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43276
|
-
});
|
|
43277
|
-
server.registerTool("add_recurring", {
|
|
43278
|
-
description: "Add a recurring transaction to track subscriptions or regular income.",
|
|
43279
|
-
inputSchema: {
|
|
43280
|
-
name: exports_external.string(),
|
|
43281
|
-
amount: exports_external.string(),
|
|
43282
|
-
type: exports_external.enum(["debit", "credit"]),
|
|
43283
|
-
frequency: exports_external.enum(["daily", "weekly", "monthly", "yearly"]),
|
|
43284
|
-
dayOfMonth: exports_external.number().optional().describe("For monthly — day of month (1-31)"),
|
|
43285
|
-
categorySlug: exports_external.string().optional(),
|
|
43286
|
-
owner: exports_external.string(),
|
|
43287
|
-
startDate: exports_external.string().optional().describe("YYYY-MM-DD"),
|
|
43288
|
-
notes: exports_external.string().optional()
|
|
43289
|
-
}
|
|
43290
|
-
}, async (body) => {
|
|
43291
|
-
const data = await client.post("/api/recurring", body);
|
|
43292
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43293
|
-
});
|
|
43294
|
-
}
|
|
43295
|
-
|
|
43296
|
-
// src/tools/reports.ts
|
|
43297
|
-
function registerReportTools(server, client) {
|
|
43298
|
-
server.registerTool("generate_monthly_report", {
|
|
43299
|
-
description: "Generate a detailed monthly financial report. Computes income, expenses, savings rate, top categories, and insights. Persists to DB.",
|
|
43300
|
-
inputSchema: {
|
|
43301
|
-
month: exports_external.number().min(1).max(12),
|
|
43302
|
-
year: exports_external.number().min(2020).max(2100)
|
|
43303
|
-
}
|
|
43304
|
-
}, async (body) => {
|
|
43305
|
-
const data = await client.post("/api/reports", body);
|
|
43306
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43307
|
-
});
|
|
43308
|
-
server.registerTool("get_reports", {
|
|
43309
|
-
description: "Monthly financial reports for the last N months — income, expenses, savings rate, net worth.",
|
|
43310
|
-
inputSchema: {
|
|
43311
|
-
months: exports_external.number().optional().describe("Number of months (default: 6)")
|
|
43312
|
-
}
|
|
43313
|
-
}, async (params) => {
|
|
43314
|
-
const data = await client.get("/api/reports", params.months ? { months: params.months } : undefined);
|
|
43315
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43316
|
-
});
|
|
43317
|
-
}
|
|
43318
|
-
|
|
43319
|
-
// src/tools/transactions.ts
|
|
43320
|
-
import { readFile } from "node:fs/promises";
|
|
43321
|
-
|
|
43322
43652
|
// ../reconciliation/src/aliases.ts
|
|
43323
43653
|
var MERCHANT_ALIASES = [
|
|
43324
43654
|
["Swiggy", ["swiggy", "bundl technologies", "swiggy bundl"]],
|
|
@@ -43510,9 +43840,9 @@ var i = function() {
|
|
|
43510
43840
|
return _has(u, e3);
|
|
43511
43841
|
};
|
|
43512
43842
|
}, SequenceMatcher.prototype.findLongestMatch = function(e2, t2, r2, n2) {
|
|
43513
|
-
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;
|
|
43514
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) {
|
|
43515
|
-
for (_ = {}, y = 0,
|
|
43845
|
+
for (_ = {}, y = 0, g2 = (k = _has(i2, o2[l]) ? i2[o2[l]] : []).length;y < g2; y++)
|
|
43516
43846
|
if (!((p = k[y]) < r2)) {
|
|
43517
43847
|
if (p >= n2)
|
|
43518
43848
|
break;
|
|
@@ -43530,11 +43860,11 @@ var i = function() {
|
|
|
43530
43860
|
u++;
|
|
43531
43861
|
return [s, c, u];
|
|
43532
43862
|
}, SequenceMatcher.prototype.getMatchingBlocks = function() {
|
|
43533
|
-
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;
|
|
43534
43864
|
if (this.matchingBlocks)
|
|
43535
43865
|
return this.matchingBlocks;
|
|
43536
43866
|
for (y = [[0, h = (k = [this.a.length, this.b.length])[0], 0, v = k[1]]], _ = [];y.length; )
|
|
43537
|
-
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]));
|
|
43538
43868
|
for (_.sort(_arrayCmp), a2 = c = f = 0, d = [], m = 0, b = _.length;m < b; m++)
|
|
43539
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]);
|
|
43540
43870
|
return f && d.push([a2, c, f]), d.push([h, v, 0]), this.matchingBlocks = d;
|
|
@@ -43682,11 +44012,11 @@ var v = p || h || Function("return this")();
|
|
|
43682
44012
|
var _ = v.Symbol;
|
|
43683
44013
|
var d = _;
|
|
43684
44014
|
var y = Object.prototype;
|
|
43685
|
-
var
|
|
44015
|
+
var g2 = y.hasOwnProperty;
|
|
43686
44016
|
var m = y.toString;
|
|
43687
44017
|
var b = d ? d.toStringTag : undefined;
|
|
43688
44018
|
var k = function getRawTag$1(e2) {
|
|
43689
|
-
var t2 =
|
|
44019
|
+
var t2 = g2.call(e2, b), r2 = e2[b];
|
|
43690
44020
|
try {
|
|
43691
44021
|
e2[b] = undefined;
|
|
43692
44022
|
var n2 = true;
|
|
@@ -44273,8 +44603,8 @@ var Xr = function equalObjects$1(e2, t2, r2, n2, o2, a2) {
|
|
|
44273
44603
|
v2 || (v2 = l2 == "constructor");
|
|
44274
44604
|
}
|
|
44275
44605
|
if (h2 && !v2) {
|
|
44276
|
-
var
|
|
44277
|
-
|
|
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);
|
|
44278
44608
|
}
|
|
44279
44609
|
return a2.delete(e2), a2.delete(t2), h2;
|
|
44280
44610
|
};
|
|
@@ -44644,15 +44974,15 @@ var qo = function leven2(e2, t2, r2, n2) {
|
|
|
44644
44974
|
return 0;
|
|
44645
44975
|
} else
|
|
44646
44976
|
f2 = (l2 = r2.wildcards[0].toLowerCase()).charCodeAt(0);
|
|
44647
|
-
for (var y2 = 0,
|
|
44977
|
+
for (var y2 = 0, g3 = 0;y2 < c2; )
|
|
44648
44978
|
a2[y2] = e2.charCodeAt(y2), o2[y2] = ++y2;
|
|
44649
44979
|
if (i2)
|
|
44650
|
-
for (;
|
|
44651
|
-
for (p2 = t2.charCodeAt(
|
|
44980
|
+
for (;g3 < u2; )
|
|
44981
|
+
for (p2 = t2.charCodeAt(g3), v2 = g3++, h2 = g3, y2 = 0;y2 < c2; y2++)
|
|
44652
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;
|
|
44653
44983
|
else
|
|
44654
|
-
for (;
|
|
44655
|
-
for (p2 = t2.charCodeAt(
|
|
44984
|
+
for (;g3 < u2; )
|
|
44985
|
+
for (p2 = t2.charCodeAt(g3), v2 = g3++, h2 = g3, y2 = 0;y2 < c2; y2++)
|
|
44656
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;
|
|
44657
44987
|
return h2;
|
|
44658
44988
|
}
|
|
@@ -44857,7 +45187,7 @@ var Lo = { exports: {} };
|
|
|
44857
45187
|
}, n3.isCustomFunc = function(e4) {
|
|
44858
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";
|
|
44859
45189
|
}, n3;
|
|
44860
|
-
}(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;
|
|
44861
45191
|
function QRatio(e3, t3, r2) {
|
|
44862
45192
|
var n3 = k2(r2);
|
|
44863
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;
|
|
@@ -44872,11 +45202,11 @@ var Lo = { exports: {} };
|
|
|
44872
45202
|
}
|
|
44873
45203
|
function token_sort_ratio(e3, t3, r2) {
|
|
44874
45204
|
var n3 = k2(r2);
|
|
44875
|
-
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;
|
|
44876
45206
|
}
|
|
44877
45207
|
function partial_token_sort_ratio(e3, t3, r2) {
|
|
44878
45208
|
var n3 = k2(r2);
|
|
44879
|
-
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;
|
|
44880
45210
|
}
|
|
44881
45211
|
function extract(e3, r2, n3) {
|
|
44882
45212
|
var o3, a3 = k2(n3);
|
|
@@ -44906,7 +45236,7 @@ var Lo = { exports: {} };
|
|
|
44906
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"));
|
|
44907
45237
|
var c2, u3, l3, h3, v3 = [], _3 = false, d3 = false, y3 = false;
|
|
44908
45238
|
if (a3.scorer.name === "token_sort_ratio" || a3.scorer.name === "partial_token_sort_ratio") {
|
|
44909
|
-
var $3 =
|
|
45239
|
+
var $3 = g3(e3);
|
|
44910
45240
|
d3 = true;
|
|
44911
45241
|
} else if (a3.scorer.name === "token_set_ratio" || a3.scorer.name === "partial_token_set_ratio") {
|
|
44912
45242
|
var w3 = m2(e3, a3);
|
|
@@ -44921,7 +45251,7 @@ var Lo = { exports: {} };
|
|
|
44921
45251
|
}, h3 = function(e4, t3) {
|
|
44922
45252
|
return t3[1] - e4[1];
|
|
44923
45253
|
}), f2(r2, function(t3, r3) {
|
|
44924
|
-
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]));
|
|
44925
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;
|
|
44926
45256
|
}
|
|
44927
45257
|
function extractAsync(e3, r2, n3, o3) {
|
|
@@ -44957,7 +45287,7 @@ var Lo = { exports: {} };
|
|
|
44957
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"));
|
|
44958
45288
|
var v3, _3, d3, y3, $3, w3 = [], O3 = false, S3 = false, C2 = false;
|
|
44959
45289
|
if (s3.scorer.name === "token_sort_ratio" || s3.scorer.name === "partial_token_sort_ratio") {
|
|
44960
|
-
var z2 =
|
|
45290
|
+
var z2 = g3(e3);
|
|
44961
45291
|
S3 = true;
|
|
44962
45292
|
} else if (s3.scorer.name === "token_set_ratio" || s3.scorer.name === "partial_token_set_ratio") {
|
|
44963
45293
|
var A2 = m2(e3, s3);
|
|
@@ -44976,7 +45306,7 @@ var Lo = { exports: {} };
|
|
|
44976
45306
|
l3 ? searchLoop(0) : searchLoop(x2[0], 0);
|
|
44977
45307
|
}
|
|
44978
45308
|
function searchLoop(n4, p3) {
|
|
44979
|
-
(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() {
|
|
44980
45310
|
searchLoop(n4 + 1);
|
|
44981
45311
|
}) : searchLoop(n4 + 1) : p3 < x2.length - 1 ? p3 % c2 == 0 ? setImmediate(function() {
|
|
44982
45312
|
searchLoop(x2[p3 + 1], p3 + 1);
|
|
@@ -45062,11 +45392,11 @@ var Lo = { exports: {} };
|
|
|
45062
45392
|
}, f3 = a2(n3, i2, wildCompare), p3 = u2(n3, i2, wildCompare), h3 = u2(i2, n3, wildCompare);
|
|
45063
45393
|
else
|
|
45064
45394
|
f3 = o2(n3, i2), p3 = s2(n3, i2), h3 = s2(i2, n3);
|
|
45065
|
-
var d3 = f3.sort().join(" "), y3 = p3.sort(),
|
|
45395
|
+
var d3 = f3.sort().join(" "), y3 = p3.sort(), g4 = h3.sort();
|
|
45066
45396
|
if (r2.sortBySimilarity)
|
|
45067
|
-
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(" ");
|
|
45068
45398
|
else
|
|
45069
|
-
$3 = y3.join(" "), w3 =
|
|
45399
|
+
$3 = y3.join(" "), w3 = g4.join(" ");
|
|
45070
45400
|
var O3 = d3 + " " + $3, S3 = d3 + " " + w3;
|
|
45071
45401
|
d3 = d3.trim(), O3 = O3.trim(), S3 = S3.trim();
|
|
45072
45402
|
var C2 = _ratio;
|
|
@@ -45151,7 +45481,7 @@ var Lo = { exports: {} };
|
|
|
45151
45481
|
}
|
|
45152
45482
|
var p3 = token_sort_ratio(e3, t3, n3) * a3, h3 = token_set_ratio(e3, t3, n3) * a3;
|
|
45153
45483
|
return Math.round(Math.max(s3, p3, h3));
|
|
45154
|
-
}, 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 };
|
|
45155
45485
|
r.exports = S2;
|
|
45156
45486
|
}();
|
|
45157
45487
|
var Ro = r.exports;
|
|
@@ -45395,82 +45725,820 @@ function settlementEnrichedData(description, type, source) {
|
|
|
45395
45725
|
out.settlementCardLast4 = last4;
|
|
45396
45726
|
return out;
|
|
45397
45727
|
}
|
|
45398
|
-
//
|
|
45399
|
-
var
|
|
45400
|
-
|
|
45401
|
-
|
|
45402
|
-
|
|
45403
|
-
|
|
45404
|
-
|
|
45405
|
-
|
|
45406
|
-
|
|
45407
|
-
|
|
45408
|
-
|
|
45409
|
-
|
|
45410
|
-
|
|
45411
|
-
|
|
45412
|
-
|
|
45413
|
-
|
|
45414
|
-
|
|
45415
|
-
|
|
45416
|
-
|
|
45417
|
-
|
|
45418
|
-
const upper = (extractedName ?? "").toUpperCase();
|
|
45419
|
-
if (upper && CORPORATE_REGEX.test(upper))
|
|
45420
|
-
return "merchant";
|
|
45421
|
-
if (paymentMethod === "imps")
|
|
45422
|
-
return "person";
|
|
45423
|
-
if (paymentMethod === "upi")
|
|
45424
|
-
return "person";
|
|
45425
|
-
if (paymentMethod === "neft" || paymentMethod === "rtgs")
|
|
45426
|
-
return "merchant";
|
|
45427
|
-
if (paymentMethod === "ach")
|
|
45428
|
-
return "merchant";
|
|
45429
|
-
return "unknown";
|
|
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);
|
|
45744
|
+
}
|
|
45745
|
+
if (cur.length)
|
|
45746
|
+
batches.push(cur);
|
|
45747
|
+
return batches;
|
|
45430
45748
|
}
|
|
45431
|
-
|
|
45432
|
-
|
|
45433
|
-
|
|
45434
|
-
if (trimmed.startsWith("UPI-")) {
|
|
45435
|
-
return parseUpi(trimmed);
|
|
45749
|
+
async function postLinks(client, links) {
|
|
45750
|
+
for (const batch of chunkLinks(links)) {
|
|
45751
|
+
await client.post("/api/transactions/bulk-link-entity", { links: batch });
|
|
45436
45752
|
}
|
|
45437
|
-
|
|
45438
|
-
|
|
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;
|
|
45439
45786
|
}
|
|
45440
|
-
|
|
45441
|
-
|
|
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;
|
|
45442
45897
|
}
|
|
45443
|
-
|
|
45444
|
-
|
|
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
|
+
}
|
|
45445
45942
|
}
|
|
45446
|
-
if (
|
|
45447
|
-
|
|
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;
|
|
45448
45951
|
}
|
|
45449
|
-
if (
|
|
45450
|
-
return
|
|
45451
|
-
|
|
45452
|
-
|
|
45453
|
-
|
|
45454
|
-
|
|
45455
|
-
|
|
45456
|
-
|
|
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
|
+
}
|
|
45457
46015
|
}
|
|
45458
|
-
|
|
45459
|
-
|
|
46016
|
+
return result;
|
|
46017
|
+
}
|
|
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.");
|
|
45460
46028
|
}
|
|
45461
|
-
return parseFallback(trimmed);
|
|
45462
46029
|
}
|
|
45463
|
-
function
|
|
45464
|
-
|
|
45465
|
-
const
|
|
45466
|
-
if (
|
|
45467
|
-
|
|
45468
|
-
|
|
45469
|
-
|
|
45470
|
-
|
|
45471
|
-
|
|
45472
|
-
|
|
45473
|
-
}
|
|
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
|
+
};
|
|
45474
46542
|
}
|
|
45475
46543
|
const name = parts[0].trim();
|
|
45476
46544
|
const upiId = parts[1]?.trim() || null;
|
|
@@ -45698,33 +46766,33 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
45698
46766
|
const norm = normalizePersonName(name);
|
|
45699
46767
|
if (!norm)
|
|
45700
46768
|
continue;
|
|
45701
|
-
const
|
|
46769
|
+
const g3 = groups.get(norm) ?? {
|
|
45702
46770
|
displayName: name.trim(),
|
|
45703
46771
|
upiHandles: new Set,
|
|
45704
46772
|
txnIds: []
|
|
45705
46773
|
};
|
|
45706
46774
|
if (upiId)
|
|
45707
|
-
|
|
45708
|
-
|
|
45709
|
-
groups.set(norm,
|
|
46775
|
+
g3.upiHandles.add(upiId);
|
|
46776
|
+
g3.txnIds.push(t2.id);
|
|
46777
|
+
groups.set(norm, g3);
|
|
45710
46778
|
}
|
|
45711
46779
|
const plan = [];
|
|
45712
46780
|
const newGroups = [];
|
|
45713
46781
|
let reused = 0;
|
|
45714
|
-
for (const [norm,
|
|
45715
|
-
const upiHandles = [...
|
|
46782
|
+
for (const [norm, g3] of groups) {
|
|
46783
|
+
const upiHandles = [...g3.upiHandles];
|
|
45716
46784
|
const isReuse = personIdByNorm.has(norm);
|
|
45717
46785
|
plan.push({
|
|
45718
|
-
name:
|
|
46786
|
+
name: g3.displayName,
|
|
45719
46787
|
normalized: norm,
|
|
45720
|
-
txnCount:
|
|
46788
|
+
txnCount: g3.txnIds.length,
|
|
45721
46789
|
upiHandles,
|
|
45722
46790
|
action: isReuse ? "reuse" : "create"
|
|
45723
46791
|
});
|
|
45724
46792
|
if (isReuse)
|
|
45725
46793
|
reused++;
|
|
45726
46794
|
else
|
|
45727
|
-
newGroups.push({ norm, displayName:
|
|
46795
|
+
newGroups.push({ norm, displayName: g3.displayName, upiHandles });
|
|
45728
46796
|
}
|
|
45729
46797
|
if (!opts.dryRun && newGroups.length > 0) {
|
|
45730
46798
|
const bodies = await Promise.all(newGroups.map(async ({ displayName, upiHandles }) => {
|
|
@@ -45747,11 +46815,11 @@ async function backfillPersons(client, crypto3, opts = {}) {
|
|
|
45747
46815
|
const created = newGroups.length;
|
|
45748
46816
|
const links = [];
|
|
45749
46817
|
if (!opts.dryRun) {
|
|
45750
|
-
for (const [norm,
|
|
46818
|
+
for (const [norm, g3] of groups) {
|
|
45751
46819
|
const personId = personIdByNorm.get(norm);
|
|
45752
46820
|
if (!personId)
|
|
45753
46821
|
continue;
|
|
45754
|
-
for (const txnId of
|
|
46822
|
+
for (const txnId of g3.txnIds)
|
|
45755
46823
|
links.push({ transactionId: txnId, personId });
|
|
45756
46824
|
}
|
|
45757
46825
|
}
|
|
@@ -45778,31 +46846,23 @@ function flagBatchDuplicates(txns) {
|
|
|
45778
46846
|
return false;
|
|
45779
46847
|
});
|
|
45780
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
|
+
}));
|
|
45781
46856
|
async function importTransactions(client, crypto3, opts) {
|
|
45782
46857
|
let txns = opts.txns;
|
|
45783
46858
|
let duplicatesSkipped = 0;
|
|
45784
46859
|
if (txns.length > 0) {
|
|
45785
46860
|
const dates = txns.map((t2) => t2.date).sort();
|
|
45786
|
-
const existing =
|
|
45787
|
-
|
|
45788
|
-
|
|
45789
|
-
|
|
45790
|
-
|
|
45791
|
-
startDate: dates[0],
|
|
45792
|
-
endDate: dates[dates.length - 1]
|
|
45793
|
-
};
|
|
45794
|
-
if (cursor)
|
|
45795
|
-
params.cursor = cursor;
|
|
45796
|
-
const res = await client.get("/api/transactions", params);
|
|
45797
|
-
const batch = res.data ?? [];
|
|
45798
|
-
if (crypto3)
|
|
45799
|
-
await decryptTransactionFields(batch, crypto3.key);
|
|
45800
|
-
existing.push(...batch);
|
|
45801
|
-
if (!res.hasMore || !res.nextCursor || batch.length === 0)
|
|
45802
|
-
break;
|
|
45803
|
-
cursor = res.nextCursor;
|
|
45804
|
-
}
|
|
45805
|
-
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)));
|
|
45806
46866
|
const before = txns.length;
|
|
45807
46867
|
txns = txns.filter((t2) => !seen.has(dedupeKey(t2.date, t2.description ?? "", t2.amount, t2.type)));
|
|
45808
46868
|
duplicatesSkipped = before - txns.length;
|
|
@@ -45830,16 +46890,9 @@ async function importTransactions(client, crypto3, opts) {
|
|
|
45830
46890
|
return { imported, duplicatesSkipped, duplicatesFlagged };
|
|
45831
46891
|
}
|
|
45832
46892
|
async function runRescan(client, crypto3) {
|
|
45833
|
-
|
|
45834
|
-
merchantAliases: [],
|
|
45835
|
-
personAliases: [],
|
|
45836
|
-
merchantRules: {},
|
|
45837
|
-
personRules: {}
|
|
45838
|
-
};
|
|
45839
|
-
let bag = EMPTY_BAG2;
|
|
46893
|
+
let bag = structuredClone(EMPTY_BAG);
|
|
45840
46894
|
try {
|
|
45841
|
-
|
|
45842
|
-
bag = learningRes.data?.bag ?? EMPTY_BAG2;
|
|
46895
|
+
bag = (await loadBag(client)).bag;
|
|
45843
46896
|
} catch {}
|
|
45844
46897
|
const merchantsRes = await client.get("/api/merchants");
|
|
45845
46898
|
const allMerchants = merchantsRes.data ?? [];
|
|
@@ -45849,23 +46902,8 @@ async function runRescan(client, crypto3) {
|
|
|
45849
46902
|
const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45850
46903
|
const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
|
|
45851
46904
|
const persons = await loadDecryptedPersons(client, crypto3);
|
|
45852
|
-
const allRows =
|
|
45853
|
-
const
|
|
45854
|
-
let cursor;
|
|
45855
|
-
const MAX_PAGES = 100;
|
|
45856
|
-
for (let page = 0;page < MAX_PAGES; page++) {
|
|
45857
|
-
const params = { limit };
|
|
45858
|
-
if (cursor)
|
|
45859
|
-
params.cursor = cursor;
|
|
45860
|
-
const res = await client.get("/api/transactions", params);
|
|
45861
|
-
const rows = res.data ?? [];
|
|
45862
|
-
if (crypto3)
|
|
45863
|
-
await decryptTransactionFields(rows, crypto3.key);
|
|
45864
|
-
allRows.push(...rows);
|
|
45865
|
-
if (!res.hasMore || !res.nextCursor || rows.length === 0)
|
|
45866
|
-
break;
|
|
45867
|
-
cursor = res.nextCursor;
|
|
45868
|
-
}
|
|
46905
|
+
const allRows = await fetchAllRows(client, crypto3, { unresolved: "true" });
|
|
46906
|
+
const categoryByTxn = new Map(allRows.map((r2) => [r2.id, r2.categoryId]));
|
|
45869
46907
|
const plan = autoCategorizeFromDescriptions({
|
|
45870
46908
|
rows: allRows,
|
|
45871
46909
|
bag,
|
|
@@ -45876,25 +46914,23 @@ async function runRescan(client, crypto3) {
|
|
|
45876
46914
|
let categorized = 0;
|
|
45877
46915
|
const links = plan.suggestions.flatMap((s2) => {
|
|
45878
46916
|
const categoryId = s2.categoryId ?? (s2.entity.type === "merchant" ? merchantCategoryById.get(s2.entity.merchantId) ?? null : null);
|
|
45879
|
-
|
|
45880
|
-
|
|
45881
|
-
|
|
45882
|
-
|
|
45883
|
-
|
|
45884
|
-
|
|
45885
|
-
|
|
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
|
+
});
|
|
45886
46927
|
});
|
|
45887
|
-
|
|
45888
|
-
const CHUNK = 500;
|
|
45889
|
-
for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
|
|
45890
|
-
await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
|
|
45891
|
-
}
|
|
45892
|
-
}
|
|
46928
|
+
await postLinks(client, links);
|
|
45893
46929
|
return { linked: links.length, categorized, unresolved: plan.unresolvedCount };
|
|
45894
46930
|
}
|
|
45895
46931
|
function registerTransactionTools(server, client, crypto3) {
|
|
45896
46932
|
server.registerTool("get_transactions", {
|
|
45897
|
-
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).",
|
|
45898
46934
|
inputSchema: {
|
|
45899
46935
|
search: exports_external.string().optional().describe("Search merchant name (NOT encrypted descriptions)"),
|
|
45900
46936
|
personId: exports_external.string().optional().describe("Filter to one person (exact, indexed)"),
|
|
@@ -45909,64 +46945,106 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45909
46945
|
amountMin: exports_external.number().optional(),
|
|
45910
46946
|
amountMax: exports_external.number().optional(),
|
|
45911
46947
|
uncategorized: exports_external.boolean().optional().describe("Only transactions with no category (server-side filter)"),
|
|
45912
|
-
limit: exports_external.number().optional().describe("Max results (default
|
|
45913
|
-
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")
|
|
45914
46951
|
}
|
|
45915
46952
|
}, async (params) => {
|
|
45916
|
-
const { personName, categorySlug, ...rest } = params;
|
|
46953
|
+
const { personName, categorySlug, full, limit, ...rest } = params;
|
|
45917
46954
|
const apiParams = { ...rest };
|
|
45918
46955
|
if (apiParams.uncategorized !== true)
|
|
45919
46956
|
delete apiParams.uncategorized;
|
|
45920
46957
|
if (categorySlug) {
|
|
45921
|
-
|
|
45922
|
-
const all2 = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
|
|
45923
|
-
const match = all2.find((c2) => c2.slug === categorySlug);
|
|
45924
|
-
if (match)
|
|
45925
|
-
apiParams.categoryId = match.id;
|
|
46958
|
+
apiParams.categoryId = requireCategory(await loadCategoryIndex(client), categorySlug).id;
|
|
45926
46959
|
}
|
|
45927
46960
|
if (personName && !apiParams.personId) {
|
|
45928
46961
|
const persons = await loadDecryptedPersons(client, crypto3);
|
|
45929
46962
|
const match = resolvePersonByName(persons, personName);
|
|
46963
|
+
const err = (error2) => ({
|
|
46964
|
+
content: [{ type: "text", text: JSON.stringify({ error: error2 }) }]
|
|
46965
|
+
});
|
|
45930
46966
|
if (!match) {
|
|
45931
|
-
return {
|
|
45932
|
-
content: [
|
|
45933
|
-
{
|
|
45934
|
-
type: "text",
|
|
45935
|
-
text: JSON.stringify({
|
|
45936
|
-
error: `No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`
|
|
45937
|
-
})
|
|
45938
|
-
}
|
|
45939
|
-
]
|
|
45940
|
-
};
|
|
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.`);
|
|
45941
46968
|
}
|
|
45942
46969
|
if ("candidates" in match) {
|
|
45943
|
-
return {
|
|
45944
|
-
content: [
|
|
45945
|
-
{
|
|
45946
|
-
type: "text",
|
|
45947
|
-
text: JSON.stringify({
|
|
45948
|
-
error: `Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`
|
|
45949
|
-
})
|
|
45950
|
-
}
|
|
45951
|
-
]
|
|
45952
|
-
};
|
|
46970
|
+
return err(`Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`);
|
|
45953
46971
|
}
|
|
45954
46972
|
apiParams.personId = match.personId;
|
|
45955
46973
|
}
|
|
46974
|
+
apiParams.limit = Math.min(Math.max(limit ?? 25, 1), full ? 200 : 100);
|
|
46975
|
+
if (!full)
|
|
46976
|
+
apiParams.view = "lean";
|
|
45956
46977
|
const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
|
|
45957
|
-
|
|
45958
|
-
|
|
45959
|
-
|
|
45960
|
-
|
|
45961
|
-
|
|
45962
|
-
|
|
45963
|
-
|
|
45964
|
-
|
|
45965
|
-
|
|
45966
|
-
|
|
45967
|
-
|
|
45968
|
-
|
|
45969
|
-
|
|
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) }] };
|
|
45970
47048
|
});
|
|
45971
47049
|
server.registerTool("backfill_persons", {
|
|
45972
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.",
|
|
@@ -45995,13 +47073,9 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45995
47073
|
if (crypto3 && body.notes)
|
|
45996
47074
|
body.notes = await encryptField(body.notes, crypto3.key);
|
|
45997
47075
|
if (body.categorySlug) {
|
|
45998
|
-
const
|
|
45999
|
-
|
|
46000
|
-
|
|
46001
|
-
if (cat) {
|
|
46002
|
-
body.categoryId = cat.id;
|
|
46003
|
-
body.categoryType = cat.type;
|
|
46004
|
-
}
|
|
47076
|
+
const cat = requireCategory(await loadCategoryIndex(client), body.categorySlug);
|
|
47077
|
+
body.categoryId = cat.id;
|
|
47078
|
+
body.categoryType = cat.type;
|
|
46005
47079
|
delete body.categorySlug;
|
|
46006
47080
|
}
|
|
46007
47081
|
const data = await client.patch(`/api/transactions/${id}`, body);
|
|
@@ -46040,8 +47114,8 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
46040
47114
|
}
|
|
46041
47115
|
if (action === "list") {
|
|
46042
47116
|
const data2 = await client.get("/api/transactions/duplicates");
|
|
46043
|
-
if (crypto3 && data2.
|
|
46044
|
-
await decryptTransactionFields(data2.
|
|
47117
|
+
if (crypto3 && data2.data)
|
|
47118
|
+
await decryptTransactionFields(data2.data, crypto3.key);
|
|
46045
47119
|
return { content: [{ type: "text", text: JSON.stringify(data2) }] };
|
|
46046
47120
|
}
|
|
46047
47121
|
const data = await client.post("/api/transactions/duplicates/resolve", {
|
|
@@ -46070,7 +47144,7 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
46070
47144
|
source: exports_external.enum(["bank_statement", "cc_statement", "manual", "telegram", "wisprflow", "ai_import"]).describe("Origin of the statement"),
|
|
46071
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"),
|
|
46072
47146
|
transactions: exports_external.array(exports_external.object({
|
|
46073
|
-
date: exports_external.string().
|
|
47147
|
+
date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD"),
|
|
46074
47148
|
amount: exports_external.number().positive(),
|
|
46075
47149
|
type: exports_external.enum(["debit", "credit"]),
|
|
46076
47150
|
description: exports_external.string(),
|
|
@@ -46109,7 +47183,12 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
46109
47183
|
const parsed = JSON.parse(raw);
|
|
46110
47184
|
if (!Array.isArray(parsed))
|
|
46111
47185
|
throw new Error("File must contain a JSON array");
|
|
46112
|
-
|
|
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;
|
|
46113
47192
|
} catch (e2) {
|
|
46114
47193
|
throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
46115
47194
|
}
|
|
@@ -46138,9 +47217,13 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
46138
47217
|
})).min(1).max(500)
|
|
46139
47218
|
}
|
|
46140
47219
|
}, async ({ links }) => {
|
|
46141
|
-
const
|
|
46142
|
-
const
|
|
46143
|
-
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(", ")}`);
|
|
46144
47227
|
const groups = new Map;
|
|
46145
47228
|
for (const { transactionId, categoryId } of links) {
|
|
46146
47229
|
const arr = groups.get(categoryId) ?? [];
|
|
@@ -46191,11 +47274,18 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
46191
47274
|
}).optional().describe("Filter params — deletes all matching transactions (provide one of id / ids / filters)")
|
|
46192
47275
|
}
|
|
46193
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
|
+
}
|
|
46194
47284
|
if (id) {
|
|
46195
47285
|
const data2 = await client.delete(`/api/transactions/${id}`);
|
|
46196
47286
|
return { content: [{ type: "text", text: JSON.stringify(data2) }] };
|
|
46197
47287
|
}
|
|
46198
|
-
const body = ids ? { ids } : { filters
|
|
47288
|
+
const body = ids ? { ids } : { filters };
|
|
46199
47289
|
const data = await client.delete("/api/transactions/bulk", body);
|
|
46200
47290
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
46201
47291
|
});
|
|
@@ -46278,6 +47368,7 @@ async function main() {
|
|
|
46278
47368
|
version: "0.0.1"
|
|
46279
47369
|
});
|
|
46280
47370
|
registerTransactionTools(server, client, crypto3);
|
|
47371
|
+
registerJevTools(server, client, crypto3);
|
|
46281
47372
|
registerMerchantTools(server, client, crypto3);
|
|
46282
47373
|
registerPersonTools(server, client, crypto3);
|
|
46283
47374
|
registerCategoryTools(server, client);
|
|
@@ -46311,5 +47402,5 @@ main().catch((err) => {
|
|
|
46311
47402
|
process.exit(1);
|
|
46312
47403
|
});
|
|
46313
47404
|
|
|
46314
|
-
//# debugId=
|
|
47405
|
+
//# debugId=81E220C7BA0CDE2464756E2164756E21
|
|
46315
47406
|
//# sourceMappingURL=index.js.map
|