@opentabs-dev/opentabs-plugin-ynab 0.0.85 → 0.0.87
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/adapter.iife.js +1315 -545
- package/dist/adapter.iife.js.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -16
- package/dist/index.js.map +1 -1
- package/dist/tools/create-category-group.d.ts +11 -0
- package/dist/tools/create-category-group.d.ts.map +1 -0
- package/dist/tools/create-category-group.js +37 -0
- package/dist/tools/create-category-group.js.map +1 -0
- package/dist/tools/create-category.d.ts +59 -0
- package/dist/tools/create-category.d.ts.map +1 -0
- package/dist/tools/create-category.js +63 -0
- package/dist/tools/create-category.js.map +1 -0
- package/dist/tools/create-transaction.d.ts +2 -2
- package/dist/tools/create-transaction.d.ts.map +1 -1
- package/dist/tools/create-transaction.js +63 -22
- package/dist/tools/create-transaction.js.map +1 -1
- package/dist/tools/delete-category-group.d.ts +8 -0
- package/dist/tools/delete-category-group.d.ts.map +1 -0
- package/dist/tools/delete-category-group.js +33 -0
- package/dist/tools/delete-category-group.js.map +1 -0
- package/dist/tools/delete-category.d.ts +7 -0
- package/dist/tools/delete-category.d.ts.map +1 -0
- package/dist/tools/delete-category.js +28 -0
- package/dist/tools/delete-category.js.map +1 -0
- package/dist/tools/delete-transaction.d.ts.map +1 -1
- package/dist/tools/delete-transaction.js +22 -7
- package/dist/tools/delete-transaction.js.map +1 -1
- package/dist/tools/get-account.d.ts.map +1 -1
- package/dist/tools/get-account.js +3 -3
- package/dist/tools/get-account.js.map +1 -1
- package/dist/tools/get-month.d.ts +2 -1
- package/dist/tools/get-month.d.ts.map +1 -1
- package/dist/tools/get-month.js +10 -33
- package/dist/tools/get-month.js.map +1 -1
- package/dist/tools/get-plan.d.ts.map +1 -1
- package/dist/tools/get-plan.js +12 -5
- package/dist/tools/get-plan.js.map +1 -1
- package/dist/tools/get-transaction.d.ts +2 -2
- package/dist/tools/get-transaction.d.ts.map +1 -1
- package/dist/tools/get-transaction.js +5 -4
- package/dist/tools/get-transaction.js.map +1 -1
- package/dist/tools/list-accounts.d.ts.map +1 -1
- package/dist/tools/list-accounts.js +4 -4
- package/dist/tools/list-accounts.js.map +1 -1
- package/dist/tools/list-categories.d.ts.map +1 -1
- package/dist/tools/list-categories.js +9 -29
- package/dist/tools/list-categories.js.map +1 -1
- package/dist/tools/list-months.d.ts +1 -1
- package/dist/tools/list-months.d.ts.map +1 -1
- package/dist/tools/list-months.js +5 -14
- package/dist/tools/list-months.js.map +1 -1
- package/dist/tools/list-payees.d.ts.map +1 -1
- package/dist/tools/list-payees.js +3 -3
- package/dist/tools/list-payees.js.map +1 -1
- package/dist/tools/list-scheduled-transactions.d.ts.map +1 -1
- package/dist/tools/list-scheduled-transactions.js +7 -5
- package/dist/tools/list-scheduled-transactions.js.map +1 -1
- package/dist/tools/list-transactions.d.ts +4 -2
- package/dist/tools/list-transactions.d.ts.map +1 -1
- package/dist/tools/list-transactions.js +40 -8
- package/dist/tools/list-transactions.js.map +1 -1
- package/dist/tools/move-category-budget.d.ts +24 -0
- package/dist/tools/move-category-budget.d.ts.map +1 -0
- package/dist/tools/move-category-budget.js +100 -0
- package/dist/tools/move-category-budget.js.map +1 -0
- package/dist/tools/schemas.d.ts +185 -28
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +411 -28
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/snooze-category-goal.d.ts +10 -0
- package/dist/tools/snooze-category-goal.d.ts.map +1 -0
- package/dist/tools/snooze-category-goal.js +53 -0
- package/dist/tools/snooze-category-goal.js.map +1 -0
- package/dist/tools/update-category-budget.d.ts.map +1 -1
- package/dist/tools/update-category-budget.js +51 -26
- package/dist/tools/update-category-budget.js.map +1 -1
- package/dist/tools/update-category.d.ts +61 -0
- package/dist/tools/update-category.d.ts.map +1 -0
- package/dist/tools/update-category.js +46 -0
- package/dist/tools/update-category.js.map +1 -0
- package/dist/tools/update-transaction.d.ts +3 -2
- package/dist/tools/update-transaction.d.ts.map +1 -1
- package/dist/tools/update-transaction.js +84 -31
- package/dist/tools/update-transaction.js.map +1 -1
- package/dist/tools.json +965 -43
- package/dist/ynab-api.d.ts +4 -1
- package/dist/ynab-api.d.ts.map +1 -1
- package/dist/ynab-api.js +47 -26
- package/dist/ynab-api.js.map +1 -1
- package/package.json +3 -3
package/dist/adapter.iife.js
CHANGED
|
@@ -329,162 +329,6 @@
|
|
|
329
329
|
configSchema;
|
|
330
330
|
};
|
|
331
331
|
|
|
332
|
-
// src/ynab-api.ts
|
|
333
|
-
var generateDeviceId = () => crypto.randomUUID();
|
|
334
|
-
var extractPlanId = () => {
|
|
335
|
-
const url2 = getCurrentUrl();
|
|
336
|
-
const match = url2.match(/app\.ynab\.com\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
|
|
337
|
-
return match?.[1] ?? null;
|
|
338
|
-
};
|
|
339
|
-
var getAuth = () => {
|
|
340
|
-
const cached2 = getAuthCache("ynab");
|
|
341
|
-
if (cached2?.sessionToken && cached2.planId) return cached2;
|
|
342
|
-
const sessionToken = getMetaContent("session-token");
|
|
343
|
-
if (!sessionToken) return null;
|
|
344
|
-
const user = getPageGlobal("YNAB_CLIENT_CONSTANTS.USER");
|
|
345
|
-
if (!user?.id) return null;
|
|
346
|
-
const planId = extractPlanId();
|
|
347
|
-
if (!planId) return null;
|
|
348
|
-
const deviceId = cached2?.deviceId ?? generateDeviceId();
|
|
349
|
-
const auth = {
|
|
350
|
-
sessionToken,
|
|
351
|
-
deviceId,
|
|
352
|
-
userId: user.id,
|
|
353
|
-
planId
|
|
354
|
-
};
|
|
355
|
-
setAuthCache("ynab", auth);
|
|
356
|
-
return auth;
|
|
357
|
-
};
|
|
358
|
-
var isAuthenticated = () => getAuth() !== null;
|
|
359
|
-
var waitForAuth = () => waitUntil(() => isAuthenticated(), { interval: 500, timeout: 5e3 }).then(
|
|
360
|
-
() => true,
|
|
361
|
-
() => false
|
|
362
|
-
);
|
|
363
|
-
var getPlanId = () => {
|
|
364
|
-
const auth = getAuth();
|
|
365
|
-
if (!auth) throw ToolError.auth("Not authenticated \u2014 please log in to YNAB.");
|
|
366
|
-
return auth.planId;
|
|
367
|
-
};
|
|
368
|
-
var getHeaders = () => {
|
|
369
|
-
const auth = getAuth();
|
|
370
|
-
if (!auth) throw ToolError.auth("Not authenticated \u2014 please log in to YNAB.");
|
|
371
|
-
const appVersion = getPageGlobal("YNAB_CLIENT_CONSTANTS.YNAB_APP_VERSION");
|
|
372
|
-
const headers = {
|
|
373
|
-
"X-Session-Token": auth.sessionToken,
|
|
374
|
-
"X-YNAB-Device-Id": auth.deviceId,
|
|
375
|
-
"X-YNAB-Device-OS": "web",
|
|
376
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
377
|
-
Accept: "application/json, text/javascript, */*; q=0.01"
|
|
378
|
-
};
|
|
379
|
-
if (appVersion) headers["X-YNAB-Device-App-Version"] = appVersion;
|
|
380
|
-
return headers;
|
|
381
|
-
};
|
|
382
|
-
var handleApiError = async (response, context) => {
|
|
383
|
-
const errorBody = (await response.text().catch(() => "")).substring(0, 512);
|
|
384
|
-
if (response.status === 426) {
|
|
385
|
-
clearAuthCache("ynab");
|
|
386
|
-
throw ToolError.auth(
|
|
387
|
-
"YNAB requires an app update (426). The session has been cleared \u2014 please reload the YNAB tab and try again."
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
if (response.status === 429) {
|
|
391
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
392
|
-
const retryMs = retryAfter !== null ? parseRetryAfterMs(retryAfter) : void 0;
|
|
393
|
-
throw ToolError.rateLimited(`Rate limited: ${context} \u2014 ${errorBody}`, retryMs);
|
|
394
|
-
}
|
|
395
|
-
if (response.status === 401 || response.status === 403) {
|
|
396
|
-
clearAuthCache("ynab");
|
|
397
|
-
throw ToolError.auth(`Auth error (${response.status}): ${errorBody}`);
|
|
398
|
-
}
|
|
399
|
-
if (response.status === 404) throw ToolError.notFound(`Not found: ${context} \u2014 ${errorBody}`);
|
|
400
|
-
if (response.status === 422) throw ToolError.validation(`Validation error: ${context} \u2014 ${errorBody}`);
|
|
401
|
-
throw ToolError.internal(`API error (${response.status}): ${context} \u2014 ${errorBody}`);
|
|
402
|
-
};
|
|
403
|
-
var catalog = async (operationName, requestData = {}) => {
|
|
404
|
-
const headers = getHeaders();
|
|
405
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
406
|
-
let response;
|
|
407
|
-
try {
|
|
408
|
-
response = await fetch("/api/v1/catalog", {
|
|
409
|
-
method: "POST",
|
|
410
|
-
headers,
|
|
411
|
-
credentials: "include",
|
|
412
|
-
body: `operation_name=${encodeURIComponent(operationName)}&request_data=${encodeURIComponent(JSON.stringify(requestData))}`,
|
|
413
|
-
signal: AbortSignal.timeout(3e4)
|
|
414
|
-
});
|
|
415
|
-
} catch (err2) {
|
|
416
|
-
if (err2 instanceof DOMException && err2.name === "TimeoutError")
|
|
417
|
-
throw ToolError.timeout(`Catalog request timed out: ${operationName}`);
|
|
418
|
-
if (err2 instanceof DOMException && err2.name === "AbortError") throw new ToolError("Request was aborted", "aborted");
|
|
419
|
-
throw new ToolError(`Network error: ${err2 instanceof Error ? err2.message : String(err2)}`, "network_error", {
|
|
420
|
-
category: "internal",
|
|
421
|
-
retryable: true
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
if (!response.ok) return handleApiError(response, operationName);
|
|
425
|
-
const data = await response.json();
|
|
426
|
-
if (data.error) {
|
|
427
|
-
throw ToolError.internal(`Catalog error (${operationName}): ${data.error.message}`);
|
|
428
|
-
}
|
|
429
|
-
return data;
|
|
430
|
-
};
|
|
431
|
-
var BUDGET_SCHEMA_VERSION = 41;
|
|
432
|
-
var syncBudget = async (planId) => catalog("syncBudgetData", {
|
|
433
|
-
budget_version_id: planId,
|
|
434
|
-
sync_type: "delta",
|
|
435
|
-
starting_device_knowledge: 0,
|
|
436
|
-
ending_device_knowledge: 0,
|
|
437
|
-
device_knowledge_of_server: 0,
|
|
438
|
-
calculated_entities_included: false,
|
|
439
|
-
schema_version: BUDGET_SCHEMA_VERSION,
|
|
440
|
-
schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
|
|
441
|
-
changed_entities: {}
|
|
442
|
-
});
|
|
443
|
-
var syncWrite = async (planId, changedEntities) => {
|
|
444
|
-
const readResult = await syncBudget(planId);
|
|
445
|
-
const serverKnowledge = readResult.current_server_knowledge ?? 0;
|
|
446
|
-
return catalog("syncBudgetData", {
|
|
447
|
-
budget_version_id: planId,
|
|
448
|
-
sync_type: "delta",
|
|
449
|
-
starting_device_knowledge: 0,
|
|
450
|
-
ending_device_knowledge: 1,
|
|
451
|
-
device_knowledge_of_server: serverKnowledge,
|
|
452
|
-
calculated_entities_included: false,
|
|
453
|
-
schema_version: BUDGET_SCHEMA_VERSION,
|
|
454
|
-
schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
|
|
455
|
-
changed_entities: changedEntities
|
|
456
|
-
});
|
|
457
|
-
};
|
|
458
|
-
var api = async (endpoint, options = {}) => {
|
|
459
|
-
const headers = getHeaders();
|
|
460
|
-
let fetchBody;
|
|
461
|
-
if (options.body) {
|
|
462
|
-
headers["Content-Type"] = "application/json";
|
|
463
|
-
fetchBody = JSON.stringify(options.body);
|
|
464
|
-
}
|
|
465
|
-
let response;
|
|
466
|
-
try {
|
|
467
|
-
response = await fetch(`/api/v2${endpoint}`, {
|
|
468
|
-
method: options.method ?? "GET",
|
|
469
|
-
headers,
|
|
470
|
-
body: fetchBody,
|
|
471
|
-
credentials: "include",
|
|
472
|
-
signal: AbortSignal.timeout(3e4)
|
|
473
|
-
});
|
|
474
|
-
} catch (err2) {
|
|
475
|
-
if (err2 instanceof DOMException && err2.name === "TimeoutError")
|
|
476
|
-
throw ToolError.timeout(`API request timed out: ${endpoint}`);
|
|
477
|
-
if (err2 instanceof DOMException && err2.name === "AbortError") throw new ToolError("Request was aborted", "aborted");
|
|
478
|
-
throw new ToolError(`Network error: ${err2 instanceof Error ? err2.message : String(err2)}`, "network_error", {
|
|
479
|
-
category: "internal",
|
|
480
|
-
retryable: true
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
if (!response.ok) return handleApiError(response, endpoint);
|
|
484
|
-
if (response.status === 204) return {};
|
|
485
|
-
return await response.json();
|
|
486
|
-
};
|
|
487
|
-
|
|
488
332
|
// node_modules/zod/v4/classic/external.js
|
|
489
333
|
var external_exports = {};
|
|
490
334
|
__export(external_exports, {
|
|
@@ -14253,19 +14097,214 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14253
14097
|
// node_modules/zod/v4/classic/external.js
|
|
14254
14098
|
config(en_default());
|
|
14255
14099
|
|
|
14256
|
-
// src/
|
|
14257
|
-
var
|
|
14258
|
-
|
|
14259
|
-
|
|
14100
|
+
// src/ynab-api.ts
|
|
14101
|
+
var NOT_AUTHENTICATED_MESSAGE = "Not authenticated \u2014 please log in to YNAB.";
|
|
14102
|
+
var generateDeviceId = () => crypto.randomUUID();
|
|
14103
|
+
var extractPlanId = () => {
|
|
14104
|
+
const url2 = getCurrentUrl();
|
|
14105
|
+
const match = url2.match(/app\.ynab\.com\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
|
|
14106
|
+
return match?.[1] ?? null;
|
|
14260
14107
|
};
|
|
14261
|
-
var
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14108
|
+
var getAuth = () => {
|
|
14109
|
+
const cached2 = getAuthCache("ynab");
|
|
14110
|
+
if (cached2?.sessionToken && cached2.planId) return cached2;
|
|
14111
|
+
const sessionToken = getMetaContent("session-token");
|
|
14112
|
+
if (!sessionToken) return null;
|
|
14113
|
+
const user = getPageGlobal("YNAB_CLIENT_CONSTANTS.USER");
|
|
14114
|
+
if (!user?.id) return null;
|
|
14115
|
+
const planId = extractPlanId();
|
|
14116
|
+
if (!planId) return null;
|
|
14117
|
+
const deviceId = cached2?.deviceId ?? generateDeviceId();
|
|
14118
|
+
const auth = {
|
|
14119
|
+
sessionToken,
|
|
14120
|
+
deviceId,
|
|
14121
|
+
userId: user.id,
|
|
14122
|
+
planId
|
|
14123
|
+
};
|
|
14124
|
+
setAuthCache("ynab", auth);
|
|
14125
|
+
return auth;
|
|
14126
|
+
};
|
|
14127
|
+
var isAuthenticated = () => getAuth() !== null;
|
|
14128
|
+
var waitForAuth = () => waitUntil(() => isAuthenticated(), { interval: 500, timeout: 5e3 }).then(
|
|
14129
|
+
() => true,
|
|
14130
|
+
() => false
|
|
14131
|
+
);
|
|
14132
|
+
var getPlanId = () => {
|
|
14133
|
+
const auth = getAuth();
|
|
14134
|
+
if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
|
|
14135
|
+
return auth.planId;
|
|
14136
|
+
};
|
|
14137
|
+
var getDeviceId = () => {
|
|
14138
|
+
const auth = getAuth();
|
|
14139
|
+
if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
|
|
14140
|
+
return auth.deviceId;
|
|
14141
|
+
};
|
|
14142
|
+
var getUserId = () => {
|
|
14143
|
+
const auth = getAuth();
|
|
14144
|
+
if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
|
|
14145
|
+
return auth.userId;
|
|
14146
|
+
};
|
|
14147
|
+
var assertAuthenticated = () => {
|
|
14148
|
+
if (!getAuth()) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
|
|
14149
|
+
};
|
|
14150
|
+
var getHeaders = () => {
|
|
14151
|
+
const auth = getAuth();
|
|
14152
|
+
if (!auth) throw ToolError.auth(NOT_AUTHENTICATED_MESSAGE);
|
|
14153
|
+
const appVersion = getPageGlobal("YNAB_CLIENT_CONSTANTS.YNAB_APP_VERSION");
|
|
14154
|
+
const headers = {
|
|
14155
|
+
"X-Session-Token": auth.sessionToken,
|
|
14156
|
+
"X-YNAB-Device-Id": auth.deviceId,
|
|
14157
|
+
"X-YNAB-Device-OS": "web",
|
|
14158
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
14159
|
+
Accept: "application/json, text/javascript, */*; q=0.01"
|
|
14160
|
+
};
|
|
14161
|
+
if (appVersion) headers["X-YNAB-Device-App-Version"] = appVersion;
|
|
14162
|
+
return headers;
|
|
14163
|
+
};
|
|
14164
|
+
var handleApiError = async (response, context) => {
|
|
14165
|
+
const errorBody = (await response.text().catch(() => "")).substring(0, 512);
|
|
14166
|
+
if (response.status === 426) {
|
|
14167
|
+
clearAuthCache("ynab");
|
|
14168
|
+
throw ToolError.auth(
|
|
14169
|
+
"YNAB requires an app update (426). The session has been cleared \u2014 please reload the YNAB tab and try again."
|
|
14170
|
+
);
|
|
14171
|
+
}
|
|
14172
|
+
if (response.status === 429) {
|
|
14173
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
14174
|
+
const retryMs = retryAfter !== null ? parseRetryAfterMs(retryAfter) : void 0;
|
|
14175
|
+
throw ToolError.rateLimited(`Rate limited: ${context} \u2014 ${errorBody}`, retryMs);
|
|
14176
|
+
}
|
|
14177
|
+
if (response.status === 401 || response.status === 403) {
|
|
14178
|
+
clearAuthCache("ynab");
|
|
14179
|
+
throw ToolError.auth(`Auth error (${response.status}): ${errorBody}`);
|
|
14180
|
+
}
|
|
14181
|
+
if (response.status === 404) throw ToolError.notFound(`Not found: ${context} \u2014 ${errorBody}`);
|
|
14182
|
+
if (response.status === 422) throw ToolError.validation(`Validation error: ${context} \u2014 ${errorBody}`);
|
|
14183
|
+
throw ToolError.internal(`API error (${response.status}): ${context} \u2014 ${errorBody}`);
|
|
14184
|
+
};
|
|
14185
|
+
var handleNetworkError = (err2, context) => {
|
|
14186
|
+
if (err2 instanceof DOMException && err2.name === "TimeoutError")
|
|
14187
|
+
throw ToolError.timeout(`Request timed out: ${context}`);
|
|
14188
|
+
if (err2 instanceof DOMException && err2.name === "AbortError") throw new ToolError("Request was aborted", "aborted");
|
|
14189
|
+
throw new ToolError(`Network error: ${err2 instanceof Error ? err2.message : String(err2)}`, "network_error", {
|
|
14190
|
+
category: "internal",
|
|
14191
|
+
retryable: true
|
|
14192
|
+
});
|
|
14193
|
+
};
|
|
14194
|
+
var catalog = async (operationName, requestData = {}) => {
|
|
14195
|
+
const headers = getHeaders();
|
|
14196
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
14197
|
+
let response;
|
|
14198
|
+
try {
|
|
14199
|
+
response = await fetch("/api/v1/catalog", {
|
|
14200
|
+
method: "POST",
|
|
14201
|
+
headers,
|
|
14202
|
+
credentials: "include",
|
|
14203
|
+
body: `operation_name=${encodeURIComponent(operationName)}&request_data=${encodeURIComponent(JSON.stringify(requestData))}`,
|
|
14204
|
+
signal: AbortSignal.timeout(3e4)
|
|
14205
|
+
});
|
|
14206
|
+
} catch (err2) {
|
|
14207
|
+
return handleNetworkError(err2, operationName);
|
|
14208
|
+
}
|
|
14209
|
+
if (!response.ok) return handleApiError(response, operationName);
|
|
14210
|
+
const data = await response.json();
|
|
14211
|
+
if (data.error) {
|
|
14212
|
+
throw ToolError.internal(`Catalog error (${operationName}): ${data.error.message}`);
|
|
14213
|
+
}
|
|
14214
|
+
return data;
|
|
14215
|
+
};
|
|
14216
|
+
var BUDGET_SCHEMA_VERSION = 41;
|
|
14217
|
+
var syncBudget = async (planId) => catalog("syncBudgetData", {
|
|
14218
|
+
budget_version_id: planId,
|
|
14219
|
+
sync_type: "delta",
|
|
14220
|
+
starting_device_knowledge: 0,
|
|
14221
|
+
ending_device_knowledge: 0,
|
|
14222
|
+
device_knowledge_of_server: 0,
|
|
14223
|
+
calculated_entities_included: false,
|
|
14224
|
+
schema_version: BUDGET_SCHEMA_VERSION,
|
|
14225
|
+
schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
|
|
14226
|
+
changed_entities: {}
|
|
14265
14227
|
});
|
|
14266
|
-
var
|
|
14267
|
-
|
|
14268
|
-
|
|
14228
|
+
var syncWrite = async (planId, changedEntities, serverKnowledge) => {
|
|
14229
|
+
const knowledge = serverKnowledge ?? (await syncBudget(planId)).current_server_knowledge ?? 0;
|
|
14230
|
+
return catalog("syncBudgetData", {
|
|
14231
|
+
budget_version_id: planId,
|
|
14232
|
+
sync_type: "delta",
|
|
14233
|
+
starting_device_knowledge: 0,
|
|
14234
|
+
ending_device_knowledge: 1,
|
|
14235
|
+
device_knowledge_of_server: knowledge,
|
|
14236
|
+
calculated_entities_included: false,
|
|
14237
|
+
schema_version: BUDGET_SCHEMA_VERSION,
|
|
14238
|
+
schema_version_of_knowledge: BUDGET_SCHEMA_VERSION,
|
|
14239
|
+
changed_entities: changedEntities
|
|
14240
|
+
});
|
|
14241
|
+
};
|
|
14242
|
+
var api = async (endpoint, options = {}) => {
|
|
14243
|
+
const headers = getHeaders();
|
|
14244
|
+
let fetchBody;
|
|
14245
|
+
if (options.body) {
|
|
14246
|
+
headers["Content-Type"] = "application/json";
|
|
14247
|
+
fetchBody = JSON.stringify(options.body);
|
|
14248
|
+
}
|
|
14249
|
+
let response;
|
|
14250
|
+
try {
|
|
14251
|
+
response = await fetch(`/api/v2${endpoint}`, {
|
|
14252
|
+
method: options.method ?? "GET",
|
|
14253
|
+
headers,
|
|
14254
|
+
body: fetchBody,
|
|
14255
|
+
credentials: "include",
|
|
14256
|
+
signal: AbortSignal.timeout(3e4)
|
|
14257
|
+
});
|
|
14258
|
+
} catch (err2) {
|
|
14259
|
+
return handleNetworkError(err2, endpoint);
|
|
14260
|
+
}
|
|
14261
|
+
if (!response.ok) return handleApiError(response, endpoint);
|
|
14262
|
+
if (response.status === 204) return {};
|
|
14263
|
+
return await response.json();
|
|
14264
|
+
};
|
|
14265
|
+
|
|
14266
|
+
// src/tools/schemas.ts
|
|
14267
|
+
var formatMilliunits = (milliunits) => {
|
|
14268
|
+
const amount = milliunits / 1e3;
|
|
14269
|
+
return amount.toFixed(2);
|
|
14270
|
+
};
|
|
14271
|
+
var toMilliunits = (amount) => Math.round(amount * 1e3);
|
|
14272
|
+
var notTombstone = (x) => !x.is_tombstone;
|
|
14273
|
+
var findCategory = (entities, id) => {
|
|
14274
|
+
const c = (entities?.be_subcategories ?? []).find((s) => s.id === id && notTombstone(s));
|
|
14275
|
+
if (!c) throw ToolError.notFound(`Category not found: ${id}`);
|
|
14276
|
+
return c;
|
|
14277
|
+
};
|
|
14278
|
+
var findCategoryGroup = (entities, id) => {
|
|
14279
|
+
const g = (entities?.be_master_categories ?? []).find((m) => m.id === id && notTombstone(m));
|
|
14280
|
+
if (!g) throw ToolError.notFound(`Category group not found: ${id}`);
|
|
14281
|
+
return g;
|
|
14282
|
+
};
|
|
14283
|
+
var assertCategoryGroupDeletable = (group) => {
|
|
14284
|
+
if (group.deletable !== true) {
|
|
14285
|
+
throw ToolError.validation(`Category group "${group.name}" is not deletable.`);
|
|
14286
|
+
}
|
|
14287
|
+
};
|
|
14288
|
+
var assertCategoryDeletable = (category) => {
|
|
14289
|
+
if (category.entities_account_id != null || category.type !== CATEGORY_TYPE_DEFAULT) {
|
|
14290
|
+
throw ToolError.validation(`Category "${category.name}" is system-managed and cannot be deleted.`);
|
|
14291
|
+
}
|
|
14292
|
+
};
|
|
14293
|
+
var nextTopSortableIndex = (rows, step = 10) => {
|
|
14294
|
+
let min = 0;
|
|
14295
|
+
for (const r of rows) {
|
|
14296
|
+
if (typeof r.sortable_index === "number" && r.sortable_index < min) min = r.sortable_index;
|
|
14297
|
+
}
|
|
14298
|
+
return min - step;
|
|
14299
|
+
};
|
|
14300
|
+
var userSchema = external_exports.object({
|
|
14301
|
+
id: external_exports.string().describe("User ID"),
|
|
14302
|
+
first_name: external_exports.string().describe("First name"),
|
|
14303
|
+
email: external_exports.string().describe("Email address")
|
|
14304
|
+
});
|
|
14305
|
+
var planSchema = external_exports.object({
|
|
14306
|
+
id: external_exports.string().describe("Plan (budget version) ID used in API calls"),
|
|
14307
|
+
budget_id: external_exports.string().describe("Underlying budget ID"),
|
|
14269
14308
|
name: external_exports.string().describe("Plan name"),
|
|
14270
14309
|
date_format: external_exports.string().describe("Date format string (e.g. MM/DD/YYYY)"),
|
|
14271
14310
|
currency_symbol: external_exports.string().describe("Currency symbol (e.g. $)"),
|
|
@@ -14317,7 +14356,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14317
14356
|
cleared: external_exports.string().describe("Cleared status: cleared, uncleared, or reconciled"),
|
|
14318
14357
|
approved: external_exports.boolean().describe("Whether the transaction is approved"),
|
|
14319
14358
|
flag_color: external_exports.string().describe("Flag color or empty string"),
|
|
14320
|
-
flag_name: external_exports.string().describe("Custom flag name or empty string"),
|
|
14321
14359
|
account_id: external_exports.string().describe("Account ID"),
|
|
14322
14360
|
account_name: external_exports.string().describe("Account name"),
|
|
14323
14361
|
payee_id: external_exports.string().describe("Payee ID"),
|
|
@@ -14325,7 +14363,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14325
14363
|
category_id: external_exports.string().describe("Category ID"),
|
|
14326
14364
|
category_name: external_exports.string().describe("Category name"),
|
|
14327
14365
|
transfer_account_id: external_exports.string().describe("If a transfer, the destination account ID"),
|
|
14328
|
-
|
|
14366
|
+
imported_payee: external_exports.string().describe("Bank-imported payee name after YNAB cleansing (empty if manually entered)"),
|
|
14367
|
+
original_imported_payee: external_exports.string().describe("Raw payee string from the bank feed before any cleansing (empty if manually entered)"),
|
|
14329
14368
|
deleted: external_exports.boolean().describe("Whether the transaction is deleted")
|
|
14330
14369
|
});
|
|
14331
14370
|
var subtransactionSchema = external_exports.object({
|
|
@@ -14351,7 +14390,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14351
14390
|
budgeted_milliunits: external_exports.number().describe("Total budgeted in milliunits"),
|
|
14352
14391
|
activity_milliunits: external_exports.number().describe("Total activity in milliunits"),
|
|
14353
14392
|
to_be_budgeted_milliunits: external_exports.number().describe("Ready to Assign in milliunits"),
|
|
14354
|
-
age_of_money: external_exports.number().describe("Age of money in days")
|
|
14393
|
+
age_of_money: external_exports.number().nullable().describe("Age of money in days, or null if not yet computed")
|
|
14355
14394
|
});
|
|
14356
14395
|
var scheduledTransactionSchema = external_exports.object({
|
|
14357
14396
|
id: external_exports.string().describe("Scheduled transaction ID"),
|
|
@@ -14372,6 +14411,256 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14372
14411
|
category_name: external_exports.string().describe("Category name"),
|
|
14373
14412
|
deleted: external_exports.boolean().describe("Whether the scheduled transaction is deleted")
|
|
14374
14413
|
});
|
|
14414
|
+
var MONEY_MOVEMENT_SOURCE = {
|
|
14415
|
+
/** RTA ↔ category (in either direction). */
|
|
14416
|
+
ASSIGN: "manual_assign",
|
|
14417
|
+
/** Category-to-category transfer. */
|
|
14418
|
+
MOVEMENT: "manual_movement"
|
|
14419
|
+
};
|
|
14420
|
+
var GOAL_TYPE = {
|
|
14421
|
+
/** "Set aside" or "Refill" — `goal_needs_whole_amount` differentiates. */
|
|
14422
|
+
NEED: "NEED",
|
|
14423
|
+
/** "Have a balance of" — no date, no cadence. */
|
|
14424
|
+
TARGET_BALANCE: "TB",
|
|
14425
|
+
/** "Have a balance of by date" — one-shot with `goal_target_date`. */
|
|
14426
|
+
TARGET_BY_DATE: "TBD",
|
|
14427
|
+
/** Debt payment goals on debt-account categories. */
|
|
14428
|
+
DEBT: "DEBT",
|
|
14429
|
+
/** Legacy "Monthly Funding". The modern UI no longer creates these but they
|
|
14430
|
+
still exist on older categories and the API still honors them. */
|
|
14431
|
+
MONTHLY_FUNDING: "MF"
|
|
14432
|
+
};
|
|
14433
|
+
var CATEGORY_TYPE_DEFAULT = "DFT";
|
|
14434
|
+
var SUBCATEGORY_BUDGET_PREFIX = "mcb";
|
|
14435
|
+
var MONTHLY_BUDGET_PREFIX = "mb";
|
|
14436
|
+
var toMonthKey = (month) => month.substring(0, 7);
|
|
14437
|
+
var currentMonthKey = () => {
|
|
14438
|
+
const now = /* @__PURE__ */ new Date();
|
|
14439
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
14440
|
+
};
|
|
14441
|
+
var formatSubcategoryBudgetId = (monthKey, categoryId) => `${SUBCATEGORY_BUDGET_PREFIX}/${monthKey}/${categoryId}`;
|
|
14442
|
+
var formatMonthlyBudgetId = (monthKey, planId) => `${MONTHLY_BUDGET_PREFIX}/${monthKey}/${planId}`;
|
|
14443
|
+
var CLEARED_MAP = {
|
|
14444
|
+
cleared: "Cleared",
|
|
14445
|
+
uncleared: "Uncleared",
|
|
14446
|
+
reconciled: "Reconciled"
|
|
14447
|
+
};
|
|
14448
|
+
var FLAG_MAP = {
|
|
14449
|
+
red: "Red",
|
|
14450
|
+
orange: "Orange",
|
|
14451
|
+
yellow: "Yellow",
|
|
14452
|
+
green: "Green",
|
|
14453
|
+
blue: "Blue",
|
|
14454
|
+
purple: "Purple"
|
|
14455
|
+
};
|
|
14456
|
+
var cadenceWireValue = {
|
|
14457
|
+
weekly: 2,
|
|
14458
|
+
monthly: 1,
|
|
14459
|
+
yearly: 13
|
|
14460
|
+
};
|
|
14461
|
+
var isValidCalendarDate = (s) => {
|
|
14462
|
+
const parts = s.split("-").map(Number);
|
|
14463
|
+
const year = parts[0] ?? 0;
|
|
14464
|
+
const month = parts[1] ?? 0;
|
|
14465
|
+
const day = parts[2] ?? 0;
|
|
14466
|
+
if (month < 1 || month > 12) return false;
|
|
14467
|
+
const maxDay = new Date(year, month, 0).getDate();
|
|
14468
|
+
return day >= 1 && day <= maxDay;
|
|
14469
|
+
};
|
|
14470
|
+
var needGoalShape = {
|
|
14471
|
+
target: external_exports.number().positive().describe("Goal amount in currency units (e.g. 50 for $50)"),
|
|
14472
|
+
cadence: external_exports.enum(["weekly", "monthly", "yearly"]).optional().describe("How often the goal recurs. Defaults to monthly."),
|
|
14473
|
+
every: external_exports.number().int().min(1).optional().describe("Multiplier on cadence (e.g. cadence=monthly + every=5 means every 5 months). Defaults to 1."),
|
|
14474
|
+
day: external_exports.number().int().min(0).max(31).optional().describe("Day-of-week (0=Sunday, 6=Saturday) for weekly cadence, or day-of-month (1-31) for monthly cadence."),
|
|
14475
|
+
start_date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD").refine(isValidCalendarDate, "Date must be a valid calendar date").optional().describe("First occurrence date (YYYY-MM-DD). Required for yearly cadence; optional for others.")
|
|
14476
|
+
};
|
|
14477
|
+
var yearlyNeedRefine = (data, ctx) => {
|
|
14478
|
+
if (data.cadence === "yearly" && !data.start_date) {
|
|
14479
|
+
ctx.addIssue({
|
|
14480
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14481
|
+
message: 'start_date is required when cadence is "yearly"',
|
|
14482
|
+
path: ["start_date"]
|
|
14483
|
+
});
|
|
14484
|
+
}
|
|
14485
|
+
};
|
|
14486
|
+
var needCadenceDayRefine = (data, ctx) => {
|
|
14487
|
+
if (data.day === void 0) return;
|
|
14488
|
+
const cadence = data.cadence ?? "monthly";
|
|
14489
|
+
if (cadence === "weekly" && (data.day < 0 || data.day > 6)) {
|
|
14490
|
+
ctx.addIssue({
|
|
14491
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14492
|
+
message: "For weekly cadence, day must be 0\u20136 (0=Sunday, 6=Saturday)",
|
|
14493
|
+
path: ["day"]
|
|
14494
|
+
});
|
|
14495
|
+
} else if (cadence === "monthly" && (data.day < 1 || data.day > 31)) {
|
|
14496
|
+
ctx.addIssue({
|
|
14497
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14498
|
+
message: "For monthly cadence, day must be 1\u201331",
|
|
14499
|
+
path: ["day"]
|
|
14500
|
+
});
|
|
14501
|
+
} else if (cadence === "yearly") {
|
|
14502
|
+
ctx.addIssue({
|
|
14503
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14504
|
+
message: "For yearly cadence, use start_date to set the recurrence anchor instead of day",
|
|
14505
|
+
path: ["day"]
|
|
14506
|
+
});
|
|
14507
|
+
}
|
|
14508
|
+
};
|
|
14509
|
+
var goalSpecSchema = external_exports.discriminatedUnion("type", [
|
|
14510
|
+
external_exports.object({ type: external_exports.literal("set_aside"), ...needGoalShape }).strict().superRefine(yearlyNeedRefine).superRefine(needCadenceDayRefine),
|
|
14511
|
+
external_exports.object({ type: external_exports.literal("refill"), ...needGoalShape }).strict().superRefine(yearlyNeedRefine).superRefine(needCadenceDayRefine),
|
|
14512
|
+
external_exports.object({
|
|
14513
|
+
type: external_exports.literal("target_balance"),
|
|
14514
|
+
target: external_exports.number().positive().describe("Balance to maintain in currency units")
|
|
14515
|
+
}).strict(),
|
|
14516
|
+
external_exports.object({
|
|
14517
|
+
type: external_exports.literal("target_by_date"),
|
|
14518
|
+
target: external_exports.number().positive().describe("Target balance to have by the given date in currency units"),
|
|
14519
|
+
date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD").refine(isValidCalendarDate, "Date must be a valid calendar date").describe("Target date YYYY-MM-DD")
|
|
14520
|
+
}).strict(),
|
|
14521
|
+
external_exports.object({
|
|
14522
|
+
type: external_exports.literal("debt"),
|
|
14523
|
+
target: external_exports.number().positive().describe("Monthly payment amount in currency units"),
|
|
14524
|
+
day: external_exports.number().int().min(1).max(31).optional().describe("Day of month the payment is due (1-31). Defaults to 1.")
|
|
14525
|
+
}).strict(),
|
|
14526
|
+
external_exports.object({ type: external_exports.literal("none") }).strict()
|
|
14527
|
+
]);
|
|
14528
|
+
var NO_GOAL_FIELDS = {
|
|
14529
|
+
goal_type: null,
|
|
14530
|
+
goal_created_on: null,
|
|
14531
|
+
goal_needs_whole_amount: null,
|
|
14532
|
+
goal_target_amount: 0,
|
|
14533
|
+
goal_target_date: null,
|
|
14534
|
+
goal_cadence: null,
|
|
14535
|
+
goal_cadence_frequency: null,
|
|
14536
|
+
goal_day: null
|
|
14537
|
+
};
|
|
14538
|
+
var buildGoalFields = (goal) => {
|
|
14539
|
+
if (!goal || goal.type === "none") return { ...NO_GOAL_FIELDS };
|
|
14540
|
+
const now = /* @__PURE__ */ new Date();
|
|
14541
|
+
const createdOn = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
14542
|
+
const base = { ...NO_GOAL_FIELDS, goal_created_on: createdOn };
|
|
14543
|
+
switch (goal.type) {
|
|
14544
|
+
case "set_aside":
|
|
14545
|
+
case "refill": {
|
|
14546
|
+
const cadence = goal.cadence ?? "monthly";
|
|
14547
|
+
return {
|
|
14548
|
+
...base,
|
|
14549
|
+
goal_type: GOAL_TYPE.NEED,
|
|
14550
|
+
goal_needs_whole_amount: goal.type === "set_aside",
|
|
14551
|
+
goal_target_amount: toMilliunits(goal.target),
|
|
14552
|
+
goal_cadence: cadenceWireValue[cadence],
|
|
14553
|
+
goal_cadence_frequency: goal.every ?? 1,
|
|
14554
|
+
goal_day: cadence === "yearly" ? null : goal.day ?? null,
|
|
14555
|
+
goal_target_date: goal.start_date ?? null
|
|
14556
|
+
};
|
|
14557
|
+
}
|
|
14558
|
+
case "target_balance":
|
|
14559
|
+
return { ...base, goal_type: GOAL_TYPE.TARGET_BALANCE, goal_target_amount: toMilliunits(goal.target) };
|
|
14560
|
+
case "target_by_date":
|
|
14561
|
+
return {
|
|
14562
|
+
...base,
|
|
14563
|
+
goal_type: GOAL_TYPE.TARGET_BY_DATE,
|
|
14564
|
+
goal_target_amount: toMilliunits(goal.target),
|
|
14565
|
+
goal_target_date: goal.date
|
|
14566
|
+
};
|
|
14567
|
+
case "debt":
|
|
14568
|
+
return {
|
|
14569
|
+
...base,
|
|
14570
|
+
goal_type: GOAL_TYPE.DEBT,
|
|
14571
|
+
goal_target_amount: toMilliunits(goal.target),
|
|
14572
|
+
goal_cadence: cadenceWireValue.monthly,
|
|
14573
|
+
goal_cadence_frequency: 1,
|
|
14574
|
+
goal_day: goal.day ?? 1
|
|
14575
|
+
};
|
|
14576
|
+
}
|
|
14577
|
+
};
|
|
14578
|
+
var resolvePayee = (existingPayees, payeeName) => {
|
|
14579
|
+
const target = payeeName.toLowerCase();
|
|
14580
|
+
const match = existingPayees.find((p) => notTombstone(p) && p.name?.toLowerCase() === target);
|
|
14581
|
+
if (match?.id) return { payeeId: match.id };
|
|
14582
|
+
const payeeId = crypto.randomUUID();
|
|
14583
|
+
const newPayee = {
|
|
14584
|
+
id: payeeId,
|
|
14585
|
+
is_tombstone: false,
|
|
14586
|
+
entities_account_id: null,
|
|
14587
|
+
enabled: true,
|
|
14588
|
+
auto_fill_subcategory_id: null,
|
|
14589
|
+
auto_fill_memo: null,
|
|
14590
|
+
auto_fill_amount: 0,
|
|
14591
|
+
auto_fill_subcategory_enabled: true,
|
|
14592
|
+
auto_fill_memo_enabled: false,
|
|
14593
|
+
auto_fill_amount_enabled: false,
|
|
14594
|
+
rename_on_import_enabled: true,
|
|
14595
|
+
name: payeeName,
|
|
14596
|
+
internal_name: null
|
|
14597
|
+
};
|
|
14598
|
+
return { payeeId, newPayee };
|
|
14599
|
+
};
|
|
14600
|
+
var buildAccountCalcMap = (entities) => new Map(
|
|
14601
|
+
(entities.be_account_calculations ?? []).filter((c) => c.entities_account_id).map((c) => [c.entities_account_id, c])
|
|
14602
|
+
);
|
|
14603
|
+
var buildMonthlyBudgetCalcMap = (calcs) => {
|
|
14604
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
14605
|
+
for (const calc of calcs) {
|
|
14606
|
+
const entityId = calc.entities_monthly_budget_id;
|
|
14607
|
+
if (!entityId) continue;
|
|
14608
|
+
const parts = entityId.split("/");
|
|
14609
|
+
const month = parts[1];
|
|
14610
|
+
if (parts.length >= 2 && month) map2.set(month, calc);
|
|
14611
|
+
}
|
|
14612
|
+
return map2;
|
|
14613
|
+
};
|
|
14614
|
+
var subcategoryCalcKey = (month, categoryId) => `${month}/${categoryId}`;
|
|
14615
|
+
var parseSubcategoryEntityId = (entityId) => {
|
|
14616
|
+
if (!entityId) return null;
|
|
14617
|
+
const parts = entityId.split("/");
|
|
14618
|
+
if (parts.length !== 3) return null;
|
|
14619
|
+
const [, month, categoryId] = parts;
|
|
14620
|
+
if (!month || !categoryId) return null;
|
|
14621
|
+
return { month, categoryId };
|
|
14622
|
+
};
|
|
14623
|
+
var buildSubcategoryCalcMap = (calcs) => {
|
|
14624
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
14625
|
+
for (const calc of calcs) {
|
|
14626
|
+
const parsed = parseSubcategoryEntityId(calc.entities_monthly_subcategory_budget_id);
|
|
14627
|
+
if (parsed) map2.set(subcategoryCalcKey(parsed.month, parsed.categoryId), calc);
|
|
14628
|
+
}
|
|
14629
|
+
return map2;
|
|
14630
|
+
};
|
|
14631
|
+
var buildSubcategoryBudgetMap = (budgets) => {
|
|
14632
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
14633
|
+
for (const budget of budgets) {
|
|
14634
|
+
if (budget.is_tombstone) continue;
|
|
14635
|
+
const parsed = parseSubcategoryEntityId(budget.id);
|
|
14636
|
+
if (parsed) map2.set(subcategoryCalcKey(parsed.month, parsed.categoryId), budget);
|
|
14637
|
+
}
|
|
14638
|
+
return map2;
|
|
14639
|
+
};
|
|
14640
|
+
var mapCategoryForMonth = (c, budgetMap, calcMap, month) => {
|
|
14641
|
+
const key = subcategoryCalcKey(month, c.id ?? "");
|
|
14642
|
+
const budget = budgetMap.get(key);
|
|
14643
|
+
const calc = calcMap.get(key);
|
|
14644
|
+
return mapCategory({
|
|
14645
|
+
...c,
|
|
14646
|
+
budgeted: budget?.budgeted ?? c.budgeted,
|
|
14647
|
+
activity: (calc?.cash_outflows ?? 0) + (calc?.credit_outflows ?? 0),
|
|
14648
|
+
balance: calc?.balance ?? c.balance,
|
|
14649
|
+
goal_percentage_complete: calc?.goal_percentage_complete ?? c.goal_percentage_complete
|
|
14650
|
+
});
|
|
14651
|
+
};
|
|
14652
|
+
var hasId = (x) => !!x.id;
|
|
14653
|
+
var buildLookups = (entities) => ({
|
|
14654
|
+
payees: new Map(
|
|
14655
|
+
(entities.be_payees ?? []).filter(notTombstone).filter(hasId).map((p) => [p.id, p.name ?? ""])
|
|
14656
|
+
),
|
|
14657
|
+
accounts: new Map(
|
|
14658
|
+
(entities.be_accounts ?? []).filter(notTombstone).filter(hasId).map((a) => [a.id, a.account_name ?? ""])
|
|
14659
|
+
),
|
|
14660
|
+
categories: new Map(
|
|
14661
|
+
(entities.be_subcategories ?? []).filter(notTombstone).filter(hasId).map((c) => [c.id, c.name ?? ""])
|
|
14662
|
+
)
|
|
14663
|
+
});
|
|
14375
14664
|
var mapUser = (u) => ({
|
|
14376
14665
|
id: u.id ?? "",
|
|
14377
14666
|
first_name: u.first_name ?? "",
|
|
@@ -14386,8 +14675,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14386
14675
|
const df = JSON.parse(p.date_format);
|
|
14387
14676
|
dateFormat = df.format ?? "";
|
|
14388
14677
|
}
|
|
14389
|
-
} catch {
|
|
14390
|
-
|
|
14678
|
+
} catch (err2) {
|
|
14679
|
+
log.warn("mapPlan: failed to parse date_format", { raw: p.date_format, err: err2 });
|
|
14391
14680
|
}
|
|
14392
14681
|
try {
|
|
14393
14682
|
if (p.currency_format) {
|
|
@@ -14395,7 +14684,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14395
14684
|
currencySymbol = cf.currency_symbol ?? "$";
|
|
14396
14685
|
currencyIsoCode = cf.iso_code ?? "USD";
|
|
14397
14686
|
}
|
|
14398
|
-
} catch {
|
|
14687
|
+
} catch (err2) {
|
|
14688
|
+
log.warn("mapPlan: failed to parse currency_format", { raw: p.currency_format, err: err2 });
|
|
14399
14689
|
}
|
|
14400
14690
|
return {
|
|
14401
14691
|
id: p.id ?? "",
|
|
@@ -14435,7 +14725,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14435
14725
|
activity_milliunits: c.activity ?? 0,
|
|
14436
14726
|
balance_milliunits: c.balance ?? 0,
|
|
14437
14727
|
goal_type: c.goal_type ?? "",
|
|
14438
|
-
|
|
14728
|
+
// The "goal target" the user configured is stored on the category itself:
|
|
14729
|
+
// - MF (legacy Monthly Funding): the static target lives in `monthly_funding`
|
|
14730
|
+
// - All modern goal types (NEED, TB, TBD, DEBT): use `goal_target_amount`
|
|
14731
|
+
// The calc's `goal_target` is YNAB's dynamically-computed "needed this month",
|
|
14732
|
+
// which for refill goals returns max(0, target - current_balance) — surprising
|
|
14733
|
+
// and not what users mean when they ask for the goal target.
|
|
14734
|
+
goal_target: formatMilliunits(
|
|
14735
|
+
(c.goal_type === GOAL_TYPE.MONTHLY_FUNDING ? c.monthly_funding : c.goal_target_amount) ?? 0
|
|
14736
|
+
),
|
|
14439
14737
|
goal_percentage_complete: c.goal_percentage_complete ?? 0
|
|
14440
14738
|
});
|
|
14441
14739
|
var mapPayee = (p) => ({
|
|
@@ -14443,36 +14741,36 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14443
14741
|
name: p.name ?? "",
|
|
14444
14742
|
transfer_account_id: p.entities_account_id ?? ""
|
|
14445
14743
|
});
|
|
14446
|
-
var mapTransaction = (t) => ({
|
|
14744
|
+
var mapTransaction = (t, lookups) => ({
|
|
14447
14745
|
id: t.id ?? "",
|
|
14448
14746
|
date: t.date ?? "",
|
|
14449
14747
|
amount: formatMilliunits(t.amount ?? 0),
|
|
14450
14748
|
amount_milliunits: t.amount ?? 0,
|
|
14451
14749
|
memo: t.memo ?? "",
|
|
14452
|
-
cleared: t.cleared ?? "uncleared",
|
|
14453
|
-
approved: t.
|
|
14454
|
-
flag_color: t.
|
|
14455
|
-
flag_name: t.flag_name ?? "",
|
|
14750
|
+
cleared: t.cleared?.toLowerCase() ?? "uncleared",
|
|
14751
|
+
approved: t.accepted ?? false,
|
|
14752
|
+
flag_color: t.flag?.toLowerCase() ?? "",
|
|
14456
14753
|
account_id: t.entities_account_id ?? "",
|
|
14457
|
-
account_name: t.
|
|
14754
|
+
account_name: lookups?.accounts.get(t.entities_account_id ?? "") ?? "",
|
|
14458
14755
|
payee_id: t.entities_payee_id ?? "",
|
|
14459
|
-
payee_name: t.
|
|
14756
|
+
payee_name: lookups?.payees.get(t.entities_payee_id ?? "") ?? "",
|
|
14460
14757
|
category_id: t.entities_subcategory_id ?? "",
|
|
14461
|
-
category_name: t.
|
|
14758
|
+
category_name: lookups?.categories.get(t.entities_subcategory_id ?? "") ?? "",
|
|
14462
14759
|
transfer_account_id: t.transfer_account_id ?? "",
|
|
14463
|
-
|
|
14760
|
+
imported_payee: t.imported_payee ?? "",
|
|
14761
|
+
original_imported_payee: t.original_imported_payee ?? "",
|
|
14464
14762
|
deleted: t.is_tombstone === true
|
|
14465
14763
|
});
|
|
14466
|
-
var mapSubtransaction = (s) => ({
|
|
14764
|
+
var mapSubtransaction = (s, lookups) => ({
|
|
14467
14765
|
id: s.id ?? "",
|
|
14468
14766
|
transaction_id: s.entities_transaction_id ?? "",
|
|
14469
14767
|
amount: formatMilliunits(s.amount ?? 0),
|
|
14470
14768
|
amount_milliunits: s.amount ?? 0,
|
|
14471
14769
|
memo: s.memo ?? "",
|
|
14472
14770
|
payee_id: s.entities_payee_id ?? "",
|
|
14473
|
-
payee_name: s.
|
|
14771
|
+
payee_name: lookups?.payees.get(s.entities_payee_id ?? "") ?? "",
|
|
14474
14772
|
category_id: s.entities_subcategory_id ?? "",
|
|
14475
|
-
category_name: s.
|
|
14773
|
+
category_name: lookups?.categories.get(s.entities_subcategory_id ?? "") ?? "",
|
|
14476
14774
|
transfer_account_id: s.transfer_account_id ?? "",
|
|
14477
14775
|
deleted: s.is_tombstone === true
|
|
14478
14776
|
});
|
|
@@ -14491,87 +14789,331 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14491
14789
|
budgeted_milliunits: budgeted,
|
|
14492
14790
|
activity_milliunits: activity,
|
|
14493
14791
|
to_be_budgeted_milliunits: toBeBudgeted,
|
|
14494
|
-
age_of_money: calc?.age_of_money ??
|
|
14792
|
+
age_of_money: calc?.age_of_money ?? null
|
|
14495
14793
|
};
|
|
14496
14794
|
};
|
|
14497
|
-
var mapScheduledTransaction = (s) => ({
|
|
14795
|
+
var mapScheduledTransaction = (s, lookups) => ({
|
|
14498
14796
|
id: s.id ?? "",
|
|
14499
|
-
date_first: s.
|
|
14500
|
-
date_next: s.
|
|
14797
|
+
date_first: s.date ?? "",
|
|
14798
|
+
date_next: s.upcoming_instances?.[0] ?? s.date ?? "",
|
|
14501
14799
|
frequency: s.frequency ?? "never",
|
|
14502
14800
|
amount: formatMilliunits(s.amount ?? 0),
|
|
14503
14801
|
amount_milliunits: s.amount ?? 0,
|
|
14504
14802
|
memo: s.memo ?? "",
|
|
14505
|
-
flag_color: s.
|
|
14803
|
+
flag_color: s.flag?.toLowerCase() ?? "",
|
|
14506
14804
|
account_id: s.entities_account_id ?? "",
|
|
14507
|
-
account_name: s.
|
|
14805
|
+
account_name: lookups?.accounts.get(s.entities_account_id ?? "") ?? "",
|
|
14508
14806
|
payee_id: s.entities_payee_id ?? "",
|
|
14509
|
-
payee_name: s.
|
|
14807
|
+
payee_name: lookups?.payees.get(s.entities_payee_id ?? "") ?? "",
|
|
14510
14808
|
category_id: s.entities_subcategory_id ?? "",
|
|
14511
|
-
category_name: s.
|
|
14809
|
+
category_name: lookups?.categories.get(s.entities_subcategory_id ?? "") ?? "",
|
|
14512
14810
|
deleted: s.is_tombstone === true
|
|
14513
14811
|
});
|
|
14514
14812
|
|
|
14515
|
-
// src/tools/
|
|
14516
|
-
var
|
|
14517
|
-
name: "
|
|
14518
|
-
displayName: "
|
|
14519
|
-
description: "
|
|
14520
|
-
summary: "
|
|
14521
|
-
icon: "
|
|
14522
|
-
group: "
|
|
14523
|
-
input: external_exports.object({
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14813
|
+
// src/tools/create-category.ts
|
|
14814
|
+
var createCategory = defineTool({
|
|
14815
|
+
name: "create_category",
|
|
14816
|
+
displayName: "Create Category",
|
|
14817
|
+
description: 'Create a new category in an existing category group. Optionally set an initial goal: "set_aside" (set aside X per cadence), "refill" (refill the balance up to X per cadence), "target_balance" (have a balance of X), or "target_by_date" (have a balance of X by a specific date). NEED-style goals (set_aside, refill) accept weekly/monthly/yearly cadence. Debt goals are not supported for new categories \u2014 they only apply to existing debt-account categories.',
|
|
14818
|
+
summary: "Create a new budget category",
|
|
14819
|
+
icon: "plus",
|
|
14820
|
+
group: "Categories",
|
|
14821
|
+
input: external_exports.object({
|
|
14822
|
+
group_id: external_exports.string().min(1).describe("Category group ID to create the category in"),
|
|
14823
|
+
name: external_exports.string().min(1).describe("Name of the new category"),
|
|
14824
|
+
goal: goalSpecSchema.optional().describe("Optional initial goal for the category"),
|
|
14825
|
+
note: external_exports.string().optional().describe("Optional note for the category")
|
|
14826
|
+
}),
|
|
14827
|
+
output: external_exports.object({
|
|
14828
|
+
category: categorySchema
|
|
14829
|
+
}),
|
|
14830
|
+
handle: async (params) => {
|
|
14831
|
+
if (params.goal?.type === "debt") {
|
|
14832
|
+
throw ToolError.validation("Debt goals can only be set on debt-account categories.");
|
|
14833
|
+
}
|
|
14834
|
+
const planId = getPlanId();
|
|
14835
|
+
const categoryId = crypto.randomUUID();
|
|
14836
|
+
const budget = await syncBudget(planId);
|
|
14837
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
14838
|
+
assertCategoryGroupDeletable(findCategoryGroup(budget.changed_entities, params.group_id));
|
|
14839
|
+
const childCategories = (budget.changed_entities?.be_subcategories ?? []).filter(
|
|
14840
|
+
(c) => c.entities_master_category_id === params.group_id
|
|
14841
|
+
);
|
|
14842
|
+
const monthKey = currentMonthKey();
|
|
14843
|
+
const categoryEntry = {
|
|
14844
|
+
id: categoryId,
|
|
14845
|
+
is_tombstone: false,
|
|
14846
|
+
entities_master_category_id: params.group_id,
|
|
14847
|
+
entities_account_id: null,
|
|
14848
|
+
internal_name: null,
|
|
14849
|
+
sortable_index: nextTopSortableIndex(childCategories, 5),
|
|
14850
|
+
name: params.name,
|
|
14851
|
+
type: CATEGORY_TYPE_DEFAULT,
|
|
14852
|
+
note: params.note ?? null,
|
|
14853
|
+
monthly_funding: 0,
|
|
14854
|
+
is_hidden: false,
|
|
14855
|
+
pinned_index: null,
|
|
14856
|
+
pinned_goal_index: null,
|
|
14857
|
+
...buildGoalFields(params.goal)
|
|
14858
|
+
};
|
|
14859
|
+
const budgetEntry = {
|
|
14860
|
+
id: formatSubcategoryBudgetId(monthKey, categoryId),
|
|
14861
|
+
is_tombstone: false,
|
|
14862
|
+
entities_monthly_budget_id: formatMonthlyBudgetId(monthKey, planId),
|
|
14863
|
+
entities_subcategory_id: categoryId,
|
|
14864
|
+
budgeted: 0
|
|
14865
|
+
};
|
|
14866
|
+
await syncWrite(
|
|
14867
|
+
planId,
|
|
14868
|
+
{
|
|
14869
|
+
be_subcategories: [categoryEntry],
|
|
14870
|
+
be_monthly_subcategory_budgets: [budgetEntry]
|
|
14871
|
+
},
|
|
14872
|
+
serverKnowledge
|
|
14873
|
+
);
|
|
14874
|
+
return { category: mapCategory(categoryEntry) };
|
|
14528
14875
|
}
|
|
14529
14876
|
});
|
|
14530
14877
|
|
|
14531
|
-
// src/tools/
|
|
14532
|
-
var
|
|
14533
|
-
name: "
|
|
14534
|
-
displayName: "
|
|
14535
|
-
description: "
|
|
14536
|
-
summary: "
|
|
14537
|
-
icon: "
|
|
14538
|
-
group: "
|
|
14539
|
-
input: external_exports.object({
|
|
14540
|
-
|
|
14541
|
-
|
|
14878
|
+
// src/tools/create-category-group.ts
|
|
14879
|
+
var createCategoryGroup = defineTool({
|
|
14880
|
+
name: "create_category_group",
|
|
14881
|
+
displayName: "Create Category Group",
|
|
14882
|
+
description: "Create a new category group in the active YNAB plan.",
|
|
14883
|
+
summary: "Create a category group",
|
|
14884
|
+
icon: "folder-plus",
|
|
14885
|
+
group: "Categories",
|
|
14886
|
+
input: external_exports.object({
|
|
14887
|
+
name: external_exports.string().min(1).describe("Name of the new category group")
|
|
14888
|
+
}),
|
|
14889
|
+
output: external_exports.object({
|
|
14890
|
+
group: categoryGroupSchema
|
|
14891
|
+
}),
|
|
14892
|
+
handle: async (params) => {
|
|
14542
14893
|
const planId = getPlanId();
|
|
14543
|
-
const
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14894
|
+
const groupId = crypto.randomUUID();
|
|
14895
|
+
const budget = await syncBudget(planId);
|
|
14896
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
14897
|
+
const groupEntry = {
|
|
14898
|
+
id: groupId,
|
|
14899
|
+
is_tombstone: false,
|
|
14900
|
+
internal_name: "",
|
|
14901
|
+
deletable: true,
|
|
14902
|
+
sortable_index: nextTopSortableIndex(budget.changed_entities?.be_master_categories ?? []),
|
|
14903
|
+
name: params.name,
|
|
14904
|
+
note: "",
|
|
14905
|
+
is_hidden: false
|
|
14906
|
+
};
|
|
14907
|
+
await syncWrite(planId, { be_master_categories: [groupEntry] }, serverKnowledge);
|
|
14908
|
+
return { group: mapCategoryGroup(groupEntry) };
|
|
14547
14909
|
}
|
|
14548
14910
|
});
|
|
14549
14911
|
|
|
14550
|
-
// src/tools/
|
|
14551
|
-
var
|
|
14552
|
-
name: "
|
|
14553
|
-
displayName: "
|
|
14554
|
-
description: "
|
|
14555
|
-
summary: "
|
|
14556
|
-
icon: "
|
|
14557
|
-
group: "
|
|
14912
|
+
// src/tools/create-transaction.ts
|
|
14913
|
+
var createTransaction = defineTool({
|
|
14914
|
+
name: "create_transaction",
|
|
14915
|
+
displayName: "Create Transaction",
|
|
14916
|
+
description: "Create a new transaction in the active YNAB plan. Amount is in currency units (e.g. -42.50 for a $42.50 expense, 1500 for $1500 income). Negative amounts are outflows (expenses), positive amounts are inflows (income).",
|
|
14917
|
+
summary: "Create a new transaction",
|
|
14918
|
+
icon: "plus",
|
|
14919
|
+
group: "Transactions",
|
|
14558
14920
|
input: external_exports.object({
|
|
14559
|
-
|
|
14921
|
+
account_id: external_exports.string().min(1).describe("Account ID to create the transaction in"),
|
|
14922
|
+
date: external_exports.string().min(1).describe("Transaction date in YYYY-MM-DD format"),
|
|
14923
|
+
amount: external_exports.number().describe(
|
|
14924
|
+
"Amount in currency units (negative for expenses, positive for income). E.g. -42.50 for a $42.50 expense."
|
|
14925
|
+
),
|
|
14926
|
+
payee_name: external_exports.string().optional().describe("Payee name (creates new payee if not found)"),
|
|
14927
|
+
payee_id: external_exports.string().optional().describe("Existing payee ID (takes precedence over payee_name)"),
|
|
14928
|
+
category_id: external_exports.string().optional().describe("Category ID to assign"),
|
|
14929
|
+
memo: external_exports.string().optional().describe("Transaction memo"),
|
|
14930
|
+
cleared: external_exports.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status (default uncleared)"),
|
|
14931
|
+
approved: external_exports.boolean().optional().describe("Whether the transaction is approved (default true)"),
|
|
14932
|
+
flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color")
|
|
14560
14933
|
}),
|
|
14561
14934
|
output: external_exports.object({
|
|
14562
|
-
|
|
14935
|
+
transaction: transactionSchema
|
|
14563
14936
|
}),
|
|
14564
14937
|
handle: async (params) => {
|
|
14565
14938
|
const planId = getPlanId();
|
|
14566
|
-
const
|
|
14567
|
-
const
|
|
14568
|
-
const
|
|
14569
|
-
const
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14939
|
+
const milliunits = toMilliunits(params.amount);
|
|
14940
|
+
const txId = crypto.randomUUID();
|
|
14941
|
+
const budget = await syncBudget(planId);
|
|
14942
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
14943
|
+
const lookups = buildLookups(budget.changed_entities ?? {});
|
|
14944
|
+
const changedEntities = {};
|
|
14945
|
+
let payeeId = params.payee_id ?? null;
|
|
14946
|
+
if (!payeeId && params.payee_name) {
|
|
14947
|
+
const resolved = resolvePayee(budget.changed_entities?.be_payees ?? [], params.payee_name);
|
|
14948
|
+
payeeId = resolved.payeeId;
|
|
14949
|
+
if (resolved.newPayee) {
|
|
14950
|
+
changedEntities.be_payees = [resolved.newPayee];
|
|
14951
|
+
lookups.payees.set(resolved.payeeId, params.payee_name);
|
|
14952
|
+
}
|
|
14953
|
+
}
|
|
14954
|
+
changedEntities.be_transaction_groups = [
|
|
14955
|
+
{
|
|
14956
|
+
id: txId,
|
|
14957
|
+
be_transaction: {
|
|
14958
|
+
id: txId,
|
|
14959
|
+
is_tombstone: false,
|
|
14960
|
+
entities_account_id: params.account_id,
|
|
14961
|
+
entities_payee_id: payeeId,
|
|
14962
|
+
entities_subcategory_id: params.category_id ?? null,
|
|
14963
|
+
entities_scheduled_transaction_id: null,
|
|
14964
|
+
date: params.date,
|
|
14965
|
+
date_entered_from_schedule: null,
|
|
14966
|
+
amount: milliunits,
|
|
14967
|
+
// cash_amount and credit_amount are server-computed splits the account
|
|
14968
|
+
// type determines. Captured from a credit card account create where
|
|
14969
|
+
// YNAB's UI sent zeros and the server populated them on response —
|
|
14970
|
+
// not yet verified for cash/checking accounts but likely the same
|
|
14971
|
+
// pattern.
|
|
14972
|
+
cash_amount: 0,
|
|
14973
|
+
credit_amount: 0,
|
|
14974
|
+
credit_amount_adjusted: 0,
|
|
14975
|
+
subcategory_credit_amount_preceding: 0,
|
|
14976
|
+
memo: params.memo ?? null,
|
|
14977
|
+
cleared: CLEARED_MAP[params.cleared ?? "uncleared"],
|
|
14978
|
+
// YNAB's wire format calls this "accepted"; the public tool surface uses "approved".
|
|
14979
|
+
accepted: params.approved ?? true,
|
|
14980
|
+
check_number: null,
|
|
14981
|
+
flag: params.flag_color ? FLAG_MAP[params.flag_color] : null,
|
|
14982
|
+
transfer_account_id: null,
|
|
14983
|
+
transfer_transaction_id: null,
|
|
14984
|
+
transfer_subtransaction_id: null,
|
|
14985
|
+
matched_transaction_id: null,
|
|
14986
|
+
ynab_id: null,
|
|
14987
|
+
// Import-related fields are only populated by bank-feed imports, not manual entry.
|
|
14988
|
+
imported_payee: null,
|
|
14989
|
+
imported_date: null,
|
|
14990
|
+
original_imported_payee: null,
|
|
14991
|
+
provider_cleansed_payee: null,
|
|
14992
|
+
source: null,
|
|
14993
|
+
debt_transaction_type: null
|
|
14994
|
+
},
|
|
14995
|
+
be_subtransactions: null
|
|
14996
|
+
}
|
|
14997
|
+
];
|
|
14998
|
+
const result = await syncWrite(planId, changedEntities, serverKnowledge);
|
|
14999
|
+
const saved = result.changed_entities?.be_transactions?.find((t) => t.id === txId);
|
|
15000
|
+
if (!saved) {
|
|
15001
|
+
throw ToolError.internal("Transaction was created but no data was returned");
|
|
14573
15002
|
}
|
|
14574
|
-
return {
|
|
15003
|
+
return { transaction: mapTransaction(saved, lookups) };
|
|
15004
|
+
}
|
|
15005
|
+
});
|
|
15006
|
+
|
|
15007
|
+
// src/tools/delete-category.ts
|
|
15008
|
+
var deleteCategory = defineTool({
|
|
15009
|
+
name: "delete_category",
|
|
15010
|
+
displayName: "Delete Category",
|
|
15011
|
+
description: "Delete a category from the active YNAB plan. This is a soft delete (tombstone). Existing transactions assigned to this category remain in place but the category will no longer appear in budget views.",
|
|
15012
|
+
summary: "Delete a category",
|
|
15013
|
+
icon: "trash-2",
|
|
15014
|
+
group: "Categories",
|
|
15015
|
+
input: external_exports.object({
|
|
15016
|
+
category_id: external_exports.string().min(1).describe("Category ID to delete")
|
|
15017
|
+
}),
|
|
15018
|
+
output: external_exports.object({
|
|
15019
|
+
success: external_exports.boolean()
|
|
15020
|
+
}),
|
|
15021
|
+
handle: async (params) => {
|
|
15022
|
+
const planId = getPlanId();
|
|
15023
|
+
const budget = await syncBudget(planId);
|
|
15024
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15025
|
+
const existing2 = findCategory(budget.changed_entities, params.category_id);
|
|
15026
|
+
assertCategoryDeletable(existing2);
|
|
15027
|
+
await syncWrite(
|
|
15028
|
+
planId,
|
|
15029
|
+
{ be_subcategories: [{ ...existing2, is_tombstone: true }] },
|
|
15030
|
+
serverKnowledge
|
|
15031
|
+
);
|
|
15032
|
+
return { success: true };
|
|
15033
|
+
}
|
|
15034
|
+
});
|
|
15035
|
+
|
|
15036
|
+
// src/tools/delete-category-group.ts
|
|
15037
|
+
var deleteCategoryGroup = defineTool({
|
|
15038
|
+
name: "delete_category_group",
|
|
15039
|
+
displayName: "Delete Category Group",
|
|
15040
|
+
description: "Delete a category group and all of its child categories from the active YNAB plan. This is a soft delete (tombstone). Internal/non-deletable groups (Credit Card Payments, Hidden Categories, Internal Master Category) cannot be deleted.",
|
|
15041
|
+
summary: "Delete a category group and its children",
|
|
15042
|
+
icon: "folder-x",
|
|
15043
|
+
group: "Categories",
|
|
15044
|
+
input: external_exports.object({
|
|
15045
|
+
group_id: external_exports.string().min(1).describe("Category group ID to delete")
|
|
15046
|
+
}),
|
|
15047
|
+
output: external_exports.object({
|
|
15048
|
+
success: external_exports.boolean(),
|
|
15049
|
+
deleted_category_count: external_exports.number().describe("Number of child categories that were also tombstoned")
|
|
15050
|
+
}),
|
|
15051
|
+
handle: async (params) => {
|
|
15052
|
+
const planId = getPlanId();
|
|
15053
|
+
const budget = await syncBudget(planId);
|
|
15054
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15055
|
+
const group = findCategoryGroup(budget.changed_entities, params.group_id);
|
|
15056
|
+
assertCategoryGroupDeletable(group);
|
|
15057
|
+
const childCategories = (budget.changed_entities?.be_subcategories ?? []).filter(
|
|
15058
|
+
(c) => c.entities_master_category_id === params.group_id && notTombstone(c)
|
|
15059
|
+
);
|
|
15060
|
+
await syncWrite(
|
|
15061
|
+
planId,
|
|
15062
|
+
{
|
|
15063
|
+
be_master_categories: [{ ...group, is_tombstone: true }],
|
|
15064
|
+
be_subcategories: childCategories.map((c) => ({ ...c, is_tombstone: true }))
|
|
15065
|
+
},
|
|
15066
|
+
serverKnowledge
|
|
15067
|
+
);
|
|
15068
|
+
return { success: true, deleted_category_count: childCategories.length };
|
|
15069
|
+
}
|
|
15070
|
+
});
|
|
15071
|
+
|
|
15072
|
+
// src/tools/delete-transaction.ts
|
|
15073
|
+
var deleteTransaction = defineTool({
|
|
15074
|
+
name: "delete_transaction",
|
|
15075
|
+
displayName: "Delete Transaction",
|
|
15076
|
+
description: "Delete a transaction from the active YNAB plan. This marks the transaction as deleted (soft delete). Transfer transactions cannot be deleted through this tool \u2014 delete them directly in YNAB.",
|
|
15077
|
+
summary: "Delete a transaction",
|
|
15078
|
+
icon: "trash-2",
|
|
15079
|
+
group: "Transactions",
|
|
15080
|
+
input: external_exports.object({
|
|
15081
|
+
transaction_id: external_exports.string().min(1).describe("Transaction ID to delete"),
|
|
15082
|
+
account_id: external_exports.string().min(1).describe("Account ID the transaction belongs to")
|
|
15083
|
+
}),
|
|
15084
|
+
output: external_exports.object({
|
|
15085
|
+
success: external_exports.boolean().describe("Whether the operation succeeded")
|
|
15086
|
+
}),
|
|
15087
|
+
handle: async (params) => {
|
|
15088
|
+
const planId = getPlanId();
|
|
15089
|
+
const budget = await syncBudget(planId);
|
|
15090
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15091
|
+
const existing2 = budget.changed_entities?.be_transactions?.find(
|
|
15092
|
+
(t) => t.id === params.transaction_id && !t.is_tombstone
|
|
15093
|
+
);
|
|
15094
|
+
if (!existing2) {
|
|
15095
|
+
throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
|
|
15096
|
+
}
|
|
15097
|
+
if (existing2.transfer_account_id) {
|
|
15098
|
+
throw ToolError.validation("Cannot delete transfer transactions \u2014 delete them in YNAB directly.");
|
|
15099
|
+
}
|
|
15100
|
+
await syncWrite(
|
|
15101
|
+
planId,
|
|
15102
|
+
{
|
|
15103
|
+
be_transaction_groups: [
|
|
15104
|
+
{
|
|
15105
|
+
id: params.transaction_id,
|
|
15106
|
+
be_transaction: {
|
|
15107
|
+
...existing2,
|
|
15108
|
+
is_tombstone: true
|
|
15109
|
+
},
|
|
15110
|
+
be_subtransactions: null
|
|
15111
|
+
}
|
|
15112
|
+
]
|
|
15113
|
+
},
|
|
15114
|
+
serverKnowledge
|
|
15115
|
+
);
|
|
15116
|
+
return { success: true };
|
|
14575
15117
|
}
|
|
14576
15118
|
});
|
|
14577
15119
|
|
|
@@ -14594,7 +15136,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14594
15136
|
const result = await syncBudget(planId);
|
|
14595
15137
|
const entities = result.changed_entities;
|
|
14596
15138
|
const raw = entities?.be_accounts ?? [];
|
|
14597
|
-
const calcMap =
|
|
15139
|
+
const calcMap = buildAccountCalcMap(entities ?? {});
|
|
14598
15140
|
const account = raw.find((a) => a.id === params.account_id && !a.is_tombstone);
|
|
14599
15141
|
if (!account) {
|
|
14600
15142
|
throw ToolError.notFound(`Account not found: ${params.account_id}`);
|
|
@@ -14603,103 +15145,205 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14603
15145
|
}
|
|
14604
15146
|
});
|
|
14605
15147
|
|
|
14606
|
-
// src/tools/
|
|
14607
|
-
var
|
|
14608
|
-
name: "
|
|
14609
|
-
displayName: "
|
|
14610
|
-
description: "
|
|
14611
|
-
summary: "
|
|
14612
|
-
icon: "
|
|
14613
|
-
group: "
|
|
15148
|
+
// src/tools/get-current-user.ts
|
|
15149
|
+
var getCurrentUser = defineTool({
|
|
15150
|
+
name: "get_current_user",
|
|
15151
|
+
displayName: "Get Current User",
|
|
15152
|
+
description: "Get the profile of the currently authenticated YNAB user including name and email.",
|
|
15153
|
+
summary: "Get your YNAB user profile",
|
|
15154
|
+
icon: "user",
|
|
15155
|
+
group: "Account",
|
|
15156
|
+
input: external_exports.object({}),
|
|
15157
|
+
output: external_exports.object({ user: userSchema }),
|
|
15158
|
+
handle: async () => {
|
|
15159
|
+
const data = await api("/user");
|
|
15160
|
+
return { user: mapUser(data) };
|
|
15161
|
+
}
|
|
15162
|
+
});
|
|
15163
|
+
|
|
15164
|
+
// src/tools/get-month.ts
|
|
15165
|
+
var getMonth = defineTool({
|
|
15166
|
+
name: "get_month",
|
|
15167
|
+
displayName: "Get Month",
|
|
15168
|
+
description: "Get budget summary and category details for a specific month. Returns the month overview (income, budgeted, activity, Ready to Assign) plus per-category breakdowns. Month format is YYYY-MM-DD using the first of the month (e.g. 2026-03-01).",
|
|
15169
|
+
summary: "Get budget details for a month",
|
|
15170
|
+
icon: "calendar",
|
|
15171
|
+
group: "Months",
|
|
14614
15172
|
input: external_exports.object({
|
|
15173
|
+
month: external_exports.string().min(1).describe("Month in YYYY-MM-DD format (first of month, e.g. 2026-03-01)"),
|
|
14615
15174
|
include_hidden: external_exports.boolean().optional().describe("Include hidden categories (default false)")
|
|
14616
15175
|
}),
|
|
14617
15176
|
output: external_exports.object({
|
|
14618
|
-
|
|
14619
|
-
categories: external_exports.array(categorySchema).describe("
|
|
15177
|
+
month: monthSchema,
|
|
15178
|
+
categories: external_exports.array(categorySchema).describe("Category budgets for this month")
|
|
14620
15179
|
}),
|
|
14621
15180
|
handle: async (params) => {
|
|
14622
15181
|
const planId = getPlanId();
|
|
14623
15182
|
const result = await syncBudget(planId);
|
|
14624
15183
|
const entities = result.changed_entities;
|
|
14625
|
-
const
|
|
14626
|
-
const
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
for (const calc of calcs) {
|
|
14630
|
-
const entityId = calc.entities_monthly_subcategory_budget_id;
|
|
14631
|
-
if (entityId) {
|
|
14632
|
-
const parts = entityId.split("/");
|
|
14633
|
-
const categoryId = parts.length >= 3 ? parts.slice(2).join("/") : entityId;
|
|
14634
|
-
calcMap.set(categoryId, calc);
|
|
14635
|
-
}
|
|
15184
|
+
const rawMonths = entities?.be_monthly_budgets ?? [];
|
|
15185
|
+
const monthData = rawMonths.find((m) => m.month === params.month && !m.is_tombstone);
|
|
15186
|
+
if (!monthData) {
|
|
15187
|
+
throw ToolError.notFound(`Month not found: ${params.month}`);
|
|
14636
15188
|
}
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
15189
|
+
const monthKey = toMonthKey(params.month);
|
|
15190
|
+
const monthCalcMap = buildMonthlyBudgetCalcMap(entities?.be_monthly_budget_calculations ?? []);
|
|
15191
|
+
const monthCalc = monthCalcMap.get(monthKey);
|
|
15192
|
+
const rawCategories = (entities?.be_subcategories ?? []).filter(
|
|
15193
|
+
(c) => notTombstone(c) && (params.include_hidden || c.is_hidden !== true)
|
|
15194
|
+
);
|
|
15195
|
+
const budgetMap = buildSubcategoryBudgetMap(entities?.be_monthly_subcategory_budgets ?? []);
|
|
15196
|
+
const calcMap = buildSubcategoryCalcMap(entities?.be_monthly_subcategory_budget_calculations ?? []);
|
|
15197
|
+
const categories = rawCategories.map((c) => mapCategoryForMonth(c, budgetMap, calcMap, monthKey));
|
|
15198
|
+
return {
|
|
15199
|
+
month: mapMonth(monthData, monthCalc),
|
|
15200
|
+
categories
|
|
15201
|
+
};
|
|
15202
|
+
}
|
|
15203
|
+
});
|
|
15204
|
+
|
|
15205
|
+
// src/tools/get-plan.ts
|
|
15206
|
+
var getPlan = defineTool({
|
|
15207
|
+
name: "get_plan",
|
|
15208
|
+
displayName: "Get Plan",
|
|
15209
|
+
description: "Get details about the currently active YNAB plan (budget), including name, currency, and date format. The plan ID is extracted from the current URL.",
|
|
15210
|
+
summary: "Get the active plan details",
|
|
15211
|
+
icon: "wallet",
|
|
15212
|
+
group: "Plans",
|
|
15213
|
+
input: external_exports.object({}),
|
|
15214
|
+
output: external_exports.object({ plan: planSchema }),
|
|
15215
|
+
handle: async () => {
|
|
15216
|
+
assertAuthenticated();
|
|
15217
|
+
const result = await catalog("getInitialUserData", {
|
|
15218
|
+
device_info: { id: getDeviceId(), device_os: "web" }
|
|
14649
15219
|
});
|
|
14650
|
-
|
|
14651
|
-
|
|
14652
|
-
|
|
15220
|
+
const budgetVersion = result.budget_version;
|
|
15221
|
+
if (!budgetVersion) {
|
|
15222
|
+
throw ToolError.notFound("No active plan found");
|
|
14653
15223
|
}
|
|
14654
|
-
return {
|
|
15224
|
+
return { plan: mapPlan(budgetVersion) };
|
|
14655
15225
|
}
|
|
14656
15226
|
});
|
|
14657
15227
|
|
|
14658
|
-
// src/tools/
|
|
14659
|
-
var
|
|
14660
|
-
name: "
|
|
14661
|
-
displayName: "
|
|
14662
|
-
description: "
|
|
14663
|
-
summary: "
|
|
14664
|
-
icon: "
|
|
14665
|
-
group: "
|
|
15228
|
+
// src/tools/get-transaction.ts
|
|
15229
|
+
var getTransaction = defineTool({
|
|
15230
|
+
name: "get_transaction",
|
|
15231
|
+
displayName: "Get Transaction",
|
|
15232
|
+
description: "Get details for a specific transaction by its ID. Returns full transaction data including any split subtransactions.",
|
|
15233
|
+
summary: "Get transaction details by ID",
|
|
15234
|
+
icon: "receipt",
|
|
15235
|
+
group: "Transactions",
|
|
14666
15236
|
input: external_exports.object({
|
|
14667
|
-
|
|
14668
|
-
month: external_exports.string().min(1).describe("Month in YYYY-MM format (e.g. 2026-03)"),
|
|
14669
|
-
budgeted: external_exports.number().describe("Amount to budget in currency units (e.g. 500 for $500)")
|
|
15237
|
+
transaction_id: external_exports.string().min(1).describe("Transaction ID to retrieve")
|
|
14670
15238
|
}),
|
|
14671
15239
|
output: external_exports.object({
|
|
14672
|
-
|
|
15240
|
+
transaction: transactionSchema,
|
|
15241
|
+
subtransactions: external_exports.array(subtransactionSchema).describe("Split subtransactions (empty if not a split)")
|
|
14673
15242
|
}),
|
|
14674
15243
|
handle: async (params) => {
|
|
14675
15244
|
const planId = getPlanId();
|
|
14676
|
-
const
|
|
14677
|
-
const
|
|
14678
|
-
const
|
|
14679
|
-
const
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
budgeted: milliunits,
|
|
14687
|
-
overspending_handling: "AffectsBuffer",
|
|
14688
|
-
is_tombstone: false
|
|
14689
|
-
}
|
|
14690
|
-
]
|
|
14691
|
-
});
|
|
14692
|
-
const budgets = result.changed_entities?.be_monthly_subcategory_budgets;
|
|
14693
|
-
const updatedBudget = budgets?.[0]?.budgeted ?? milliunits;
|
|
15245
|
+
const result = await syncBudget(planId);
|
|
15246
|
+
const entities = result.changed_entities;
|
|
15247
|
+
const raw = entities?.be_transactions ?? [];
|
|
15248
|
+
const tx = raw.find((t) => t.id === params.transaction_id && !t.is_tombstone);
|
|
15249
|
+
if (!tx) {
|
|
15250
|
+
throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
|
|
15251
|
+
}
|
|
15252
|
+
const lookups = buildLookups(entities ?? {});
|
|
15253
|
+
const allSubs = entities?.be_subtransactions ?? [];
|
|
15254
|
+
const subtransactions = allSubs.filter((s) => s.entities_transaction_id === params.transaction_id && !s.is_tombstone).map((s) => mapSubtransaction(s, lookups));
|
|
14694
15255
|
return {
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
budgeted: updatedBudget
|
|
14698
|
-
})
|
|
15256
|
+
transaction: mapTransaction(tx, lookups),
|
|
15257
|
+
subtransactions
|
|
14699
15258
|
};
|
|
14700
15259
|
}
|
|
14701
15260
|
});
|
|
14702
15261
|
|
|
15262
|
+
// src/tools/list-accounts.ts
|
|
15263
|
+
var listAccounts = defineTool({
|
|
15264
|
+
name: "list_accounts",
|
|
15265
|
+
displayName: "List Accounts",
|
|
15266
|
+
description: "List all accounts in the active YNAB plan. Returns account name, type, balances, and on-budget status. Includes checking, savings, credit cards, and tracking accounts.",
|
|
15267
|
+
summary: "List all budget accounts",
|
|
15268
|
+
icon: "landmark",
|
|
15269
|
+
group: "Accounts",
|
|
15270
|
+
input: external_exports.object({
|
|
15271
|
+
include_closed: external_exports.boolean().optional().describe("Include closed accounts (default false)")
|
|
15272
|
+
}),
|
|
15273
|
+
output: external_exports.object({
|
|
15274
|
+
accounts: external_exports.array(accountSchema).describe("List of accounts")
|
|
15275
|
+
}),
|
|
15276
|
+
handle: async (params) => {
|
|
15277
|
+
const planId = getPlanId();
|
|
15278
|
+
const result = await syncBudget(planId);
|
|
15279
|
+
const entities = result.changed_entities;
|
|
15280
|
+
const raw = entities?.be_accounts ?? [];
|
|
15281
|
+
const calcMap = buildAccountCalcMap(entities ?? {});
|
|
15282
|
+
let accounts = raw.filter(notTombstone).map((a) => mapAccount(a, calcMap.get(a.id)));
|
|
15283
|
+
if (!params.include_closed) {
|
|
15284
|
+
accounts = accounts.filter((a) => !a.closed);
|
|
15285
|
+
}
|
|
15286
|
+
return { accounts };
|
|
15287
|
+
}
|
|
15288
|
+
});
|
|
15289
|
+
|
|
15290
|
+
// src/tools/list-categories.ts
|
|
15291
|
+
var listCategories = defineTool({
|
|
15292
|
+
name: "list_categories",
|
|
15293
|
+
displayName: "List Categories",
|
|
15294
|
+
description: "List category groups and categories in the active YNAB plan. Returns budgeted amounts, activity, and available balances for the current month. Hidden and deleted categories are excluded by default \u2014 pass include_hidden=true to also see hidden categories (useful for editing budgets on hidden categories).",
|
|
15295
|
+
summary: "List budget categories with balances",
|
|
15296
|
+
icon: "tags",
|
|
15297
|
+
group: "Categories",
|
|
15298
|
+
input: external_exports.object({
|
|
15299
|
+
include_hidden: external_exports.boolean().optional().describe("Include hidden categories (default false)")
|
|
15300
|
+
}),
|
|
15301
|
+
output: external_exports.object({
|
|
15302
|
+
groups: external_exports.array(categoryGroupSchema).describe("Category groups"),
|
|
15303
|
+
categories: external_exports.array(categorySchema).describe("Categories with budget data")
|
|
15304
|
+
}),
|
|
15305
|
+
handle: async (params) => {
|
|
15306
|
+
const planId = getPlanId();
|
|
15307
|
+
const result = await syncBudget(planId);
|
|
15308
|
+
const entities = result.changed_entities;
|
|
15309
|
+
const rawGroups = (entities?.be_master_categories ?? []).filter(notTombstone);
|
|
15310
|
+
const rawCategories = (entities?.be_subcategories ?? []).filter(notTombstone);
|
|
15311
|
+
const budgetMap = buildSubcategoryBudgetMap(entities?.be_monthly_subcategory_budgets ?? []);
|
|
15312
|
+
const calcMap = buildSubcategoryCalcMap(entities?.be_monthly_subcategory_budget_calculations ?? []);
|
|
15313
|
+
const currentMonth = currentMonthKey();
|
|
15314
|
+
let groups = rawGroups.map(mapCategoryGroup);
|
|
15315
|
+
let categories = rawCategories.map((c) => mapCategoryForMonth(c, budgetMap, calcMap, currentMonth));
|
|
15316
|
+
if (!params.include_hidden) {
|
|
15317
|
+
groups = groups.filter((g) => !g.hidden);
|
|
15318
|
+
categories = categories.filter((c) => !c.hidden);
|
|
15319
|
+
}
|
|
15320
|
+
return { groups, categories };
|
|
15321
|
+
}
|
|
15322
|
+
});
|
|
15323
|
+
|
|
15324
|
+
// src/tools/list-months.ts
|
|
15325
|
+
var listMonths = defineTool({
|
|
15326
|
+
name: "list_months",
|
|
15327
|
+
displayName: "List Months",
|
|
15328
|
+
description: "List all budget months in the active YNAB plan. Returns income, budgeted, activity, and Ready to Assign amounts for each month. Sorted from most recent to oldest.",
|
|
15329
|
+
summary: "List budget months with summaries",
|
|
15330
|
+
icon: "calendar",
|
|
15331
|
+
group: "Months",
|
|
15332
|
+
input: external_exports.object({}),
|
|
15333
|
+
output: external_exports.object({
|
|
15334
|
+
months: external_exports.array(monthSchema).describe("List of budget months")
|
|
15335
|
+
}),
|
|
15336
|
+
handle: async () => {
|
|
15337
|
+
const planId = getPlanId();
|
|
15338
|
+
const result = await syncBudget(planId);
|
|
15339
|
+
const entities = result.changed_entities;
|
|
15340
|
+
const rawMonths = entities?.be_monthly_budgets ?? [];
|
|
15341
|
+
const calcMap = buildMonthlyBudgetCalcMap(entities?.be_monthly_budget_calculations ?? []);
|
|
15342
|
+
const months = rawMonths.filter(notTombstone).map((m) => mapMonth(m, calcMap.get((m.month ?? "").substring(0, 7)))).sort((a, b) => b.month.localeCompare(a.month));
|
|
15343
|
+
return { months };
|
|
15344
|
+
}
|
|
15345
|
+
});
|
|
15346
|
+
|
|
14703
15347
|
// src/tools/list-payees.ts
|
|
14704
15348
|
var listPayees = defineTool({
|
|
14705
15349
|
name: "list_payees",
|
|
@@ -14716,11 +15360,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14716
15360
|
const planId = getPlanId();
|
|
14717
15361
|
const result = await syncBudget(planId);
|
|
14718
15362
|
const raw = result.changed_entities?.be_payees ?? [];
|
|
14719
|
-
const payees = raw.filter(
|
|
15363
|
+
const payees = raw.filter(notTombstone).map(mapPayee);
|
|
14720
15364
|
return { payees };
|
|
14721
15365
|
}
|
|
14722
15366
|
});
|
|
14723
15367
|
|
|
15368
|
+
// src/tools/list-scheduled-transactions.ts
|
|
15369
|
+
var listScheduledTransactions = defineTool({
|
|
15370
|
+
name: "list_scheduled_transactions",
|
|
15371
|
+
displayName: "List Scheduled Transactions",
|
|
15372
|
+
description: "List all scheduled (recurring) transactions in the active YNAB plan. Returns frequency, next occurrence date, amount, payee, and category for each.",
|
|
15373
|
+
summary: "List scheduled/recurring transactions",
|
|
15374
|
+
icon: "clock",
|
|
15375
|
+
group: "Transactions",
|
|
15376
|
+
input: external_exports.object({}),
|
|
15377
|
+
output: external_exports.object({
|
|
15378
|
+
scheduled_transactions: external_exports.array(scheduledTransactionSchema).describe("List of scheduled transactions")
|
|
15379
|
+
}),
|
|
15380
|
+
handle: async () => {
|
|
15381
|
+
const planId = getPlanId();
|
|
15382
|
+
const result = await syncBudget(planId);
|
|
15383
|
+
const entities = result.changed_entities;
|
|
15384
|
+
const raw = entities?.be_scheduled_transactions ?? [];
|
|
15385
|
+
const lookups = buildLookups(entities ?? {});
|
|
15386
|
+
const scheduledTransactions = raw.filter(notTombstone).map((s) => mapScheduledTransaction(s, lookups)).sort((a, b) => a.date_next.localeCompare(b.date_next));
|
|
15387
|
+
return { scheduled_transactions: scheduledTransactions };
|
|
15388
|
+
}
|
|
15389
|
+
});
|
|
15390
|
+
|
|
14724
15391
|
// src/tools/list-transactions.ts
|
|
14725
15392
|
var listTransactions = defineTool({
|
|
14726
15393
|
name: "list_transactions",
|
|
@@ -14731,7 +15398,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14731
15398
|
group: "Transactions",
|
|
14732
15399
|
input: external_exports.object({
|
|
14733
15400
|
account_id: external_exports.string().optional().describe("Filter by account ID. Omit to list all transactions."),
|
|
14734
|
-
since_date: external_exports.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD). Omit for all transactions.")
|
|
15401
|
+
since_date: external_exports.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD). Omit for all transactions."),
|
|
15402
|
+
until_date: external_exports.string().optional().describe(
|
|
15403
|
+
"Only return transactions on or before this date (YYYY-MM-DD). Combine with since_date for a date range."
|
|
15404
|
+
),
|
|
15405
|
+
payee_search: external_exports.string().optional().describe(
|
|
15406
|
+
"Case-insensitive substring match against payee_name, imported_payee, and original_imported_payee. Useful for finding all transactions for a merchant without knowing the exact payee ID."
|
|
15407
|
+
)
|
|
14735
15408
|
}),
|
|
14736
15409
|
output: external_exports.object({
|
|
14737
15410
|
transactions: external_exports.array(transactionSchema).describe("List of transactions")
|
|
@@ -14739,112 +15412,292 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14739
15412
|
handle: async (params) => {
|
|
14740
15413
|
const planId = getPlanId();
|
|
14741
15414
|
const result = await syncBudget(planId);
|
|
14742
|
-
const
|
|
14743
|
-
|
|
15415
|
+
const entities = result.changed_entities;
|
|
15416
|
+
const raw = entities?.be_transactions ?? [];
|
|
15417
|
+
const lookups = buildLookups(entities ?? {});
|
|
15418
|
+
let filtered = raw.filter(notTombstone);
|
|
14744
15419
|
if (params.account_id) {
|
|
14745
|
-
|
|
15420
|
+
filtered = filtered.filter((t) => t.entities_account_id === params.account_id);
|
|
14746
15421
|
}
|
|
14747
15422
|
if (params.since_date) {
|
|
14748
15423
|
const sinceDate = params.since_date;
|
|
14749
|
-
|
|
15424
|
+
filtered = filtered.filter((t) => (t.date ?? "") >= sinceDate);
|
|
15425
|
+
}
|
|
15426
|
+
if (params.until_date) {
|
|
15427
|
+
const untilDate = params.until_date;
|
|
15428
|
+
filtered = filtered.filter((t) => (t.date ?? "") <= untilDate);
|
|
15429
|
+
}
|
|
15430
|
+
if (params.payee_search) {
|
|
15431
|
+
const needle = params.payee_search.toLowerCase();
|
|
15432
|
+
const matchingPayeeIds = /* @__PURE__ */ new Set();
|
|
15433
|
+
for (const [id, name] of lookups.payees) {
|
|
15434
|
+
if (name.toLowerCase().includes(needle)) matchingPayeeIds.add(id);
|
|
15435
|
+
}
|
|
15436
|
+
filtered = filtered.filter((t) => {
|
|
15437
|
+
if (t.entities_payee_id && matchingPayeeIds.has(t.entities_payee_id)) return true;
|
|
15438
|
+
if (t.imported_payee?.toLowerCase().includes(needle)) return true;
|
|
15439
|
+
if (t.original_imported_payee?.toLowerCase().includes(needle)) return true;
|
|
15440
|
+
return false;
|
|
15441
|
+
});
|
|
14750
15442
|
}
|
|
14751
|
-
transactions.sort((a, b) => b.date.localeCompare(a.date));
|
|
15443
|
+
const transactions = filtered.map((t) => mapTransaction(t, lookups)).sort((a, b) => b.date.localeCompare(a.date));
|
|
14752
15444
|
return { transactions };
|
|
14753
15445
|
}
|
|
14754
15446
|
});
|
|
14755
15447
|
|
|
14756
|
-
// src/tools/
|
|
14757
|
-
var
|
|
14758
|
-
name: "
|
|
14759
|
-
displayName: "
|
|
14760
|
-
description: "
|
|
14761
|
-
summary: "
|
|
14762
|
-
icon: "
|
|
14763
|
-
group: "
|
|
15448
|
+
// src/tools/move-category-budget.ts
|
|
15449
|
+
var moveCategoryBudget = defineTool({
|
|
15450
|
+
name: "move_category_budget",
|
|
15451
|
+
displayName: "Move Category Budget",
|
|
15452
|
+
description: "Move budgeted money between categories or to/from Ready to Assign for a specific month. Omit from_category_id to move money out of Ready to Assign; omit to_category_id to move money back to Ready to Assign. Both null is invalid.",
|
|
15453
|
+
summary: "Move money between budget categories",
|
|
15454
|
+
icon: "arrow-left-right",
|
|
15455
|
+
group: "Categories",
|
|
14764
15456
|
input: external_exports.object({
|
|
14765
|
-
|
|
15457
|
+
month: external_exports.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, "Month must be YYYY-MM or YYYY-MM-DD").describe("Month in YYYY-MM format (e.g. 2026-03)"),
|
|
15458
|
+
amount: external_exports.number().positive().describe("Amount to move in currency units (e.g. 50 for $50)"),
|
|
15459
|
+
from_category_id: external_exports.string().optional().describe("Source category ID. Omit to move from Ready to Assign."),
|
|
15460
|
+
to_category_id: external_exports.string().optional().describe("Destination category ID. Omit to move to Ready to Assign.")
|
|
15461
|
+
}).refine((p) => p.from_category_id || p.to_category_id, {
|
|
15462
|
+
message: "At least one of from_category_id or to_category_id must be provided"
|
|
15463
|
+
}).refine((p) => !p.from_category_id || !p.to_category_id || p.from_category_id !== p.to_category_id, {
|
|
15464
|
+
message: "from_category_id and to_category_id must differ"
|
|
14766
15465
|
}),
|
|
14767
15466
|
output: external_exports.object({
|
|
14768
|
-
|
|
14769
|
-
subtransactions: external_exports.array(subtransactionSchema).describe("Split subtransactions (empty if not a split)")
|
|
15467
|
+
categories: external_exports.array(categorySchema).describe("Updated categories (1 if RTA is involved, 2 for category-to-category)")
|
|
14770
15468
|
}),
|
|
14771
15469
|
handle: async (params) => {
|
|
14772
15470
|
const planId = getPlanId();
|
|
14773
|
-
const
|
|
14774
|
-
const
|
|
14775
|
-
const
|
|
14776
|
-
const
|
|
14777
|
-
|
|
14778
|
-
|
|
15471
|
+
const userId = getUserId();
|
|
15472
|
+
const milliunits = toMilliunits(params.amount);
|
|
15473
|
+
const monthKey = toMonthKey(params.month);
|
|
15474
|
+
const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
|
|
15475
|
+
const budget = await syncBudget(planId);
|
|
15476
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15477
|
+
const existingBudgets = budget.changed_entities?.be_monthly_subcategory_budgets ?? [];
|
|
15478
|
+
const fromCategoryId = params.from_category_id;
|
|
15479
|
+
const toCategoryId = params.to_category_id;
|
|
15480
|
+
const fromCategory = fromCategoryId ? findCategory(budget.changed_entities, fromCategoryId) : null;
|
|
15481
|
+
const toCategory = toCategoryId ? findCategory(budget.changed_entities, toCategoryId) : null;
|
|
15482
|
+
const fromBudgetId = fromCategoryId ? formatSubcategoryBudgetId(monthKey, fromCategoryId) : null;
|
|
15483
|
+
const toBudgetId = toCategoryId ? formatSubcategoryBudgetId(monthKey, toCategoryId) : null;
|
|
15484
|
+
const buildEntry = (categoryId, budgetId, signedDelta) => {
|
|
15485
|
+
const current = existingBudgets.find((b) => b.id === budgetId && notTombstone(b))?.budgeted ?? 0;
|
|
15486
|
+
return {
|
|
15487
|
+
id: budgetId,
|
|
15488
|
+
is_tombstone: false,
|
|
15489
|
+
entities_monthly_budget_id: monthlyBudgetId,
|
|
15490
|
+
entities_subcategory_id: categoryId,
|
|
15491
|
+
budgeted: current + signedDelta
|
|
15492
|
+
};
|
|
15493
|
+
};
|
|
15494
|
+
const budgetEntries = [];
|
|
15495
|
+
if (fromCategoryId && fromBudgetId) budgetEntries.push(buildEntry(fromCategoryId, fromBudgetId, -milliunits));
|
|
15496
|
+
if (toCategoryId && toBudgetId) budgetEntries.push(buildEntry(toCategoryId, toBudgetId, milliunits));
|
|
15497
|
+
const source = fromCategoryId && toCategoryId ? MONEY_MOVEMENT_SOURCE.MOVEMENT : MONEY_MOVEMENT_SOURCE.ASSIGN;
|
|
15498
|
+
const result = await syncWrite(
|
|
15499
|
+
planId,
|
|
15500
|
+
{
|
|
15501
|
+
be_monthly_subcategory_budgets: budgetEntries,
|
|
15502
|
+
be_money_movements: [
|
|
15503
|
+
{
|
|
15504
|
+
id: crypto.randomUUID(),
|
|
15505
|
+
is_tombstone: false,
|
|
15506
|
+
from_entities_monthly_subcategory_budget_id: fromBudgetId,
|
|
15507
|
+
to_entities_monthly_subcategory_budget_id: toBudgetId,
|
|
15508
|
+
entities_money_movement_group_id: null,
|
|
15509
|
+
amount: milliunits,
|
|
15510
|
+
performed_by_user_id: userId,
|
|
15511
|
+
note: null,
|
|
15512
|
+
source,
|
|
15513
|
+
move_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15514
|
+
move_accepted_at: null
|
|
15515
|
+
}
|
|
15516
|
+
]
|
|
15517
|
+
},
|
|
15518
|
+
serverKnowledge
|
|
15519
|
+
);
|
|
15520
|
+
const calcMap = buildSubcategoryCalcMap(result.changed_entities?.be_monthly_subcategory_budget_calculations ?? []);
|
|
15521
|
+
const budgetMap = buildSubcategoryBudgetMap(result.changed_entities?.be_monthly_subcategory_budgets ?? []);
|
|
15522
|
+
for (const e of budgetEntries) {
|
|
15523
|
+
const key = `${monthKey}/${e.entities_subcategory_id}`;
|
|
15524
|
+
if (!budgetMap.has(key)) budgetMap.set(key, e);
|
|
14779
15525
|
}
|
|
14780
|
-
const
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
15526
|
+
const categories = [];
|
|
15527
|
+
if (fromCategory) categories.push(mapCategoryForMonth(fromCategory, budgetMap, calcMap, monthKey));
|
|
15528
|
+
if (toCategory) categories.push(mapCategoryForMonth(toCategory, budgetMap, calcMap, monthKey));
|
|
15529
|
+
return { categories };
|
|
15530
|
+
}
|
|
15531
|
+
});
|
|
15532
|
+
|
|
15533
|
+
// src/tools/snooze-category-goal.ts
|
|
15534
|
+
var snoozeCategoryGoal = defineTool({
|
|
15535
|
+
name: "snooze_category_goal",
|
|
15536
|
+
displayName: "Snooze Category Goal",
|
|
15537
|
+
description: "Snooze a category goal for a specific month so it does not appear as needing funding for that month. Pass snooze=false to un-snooze.",
|
|
15538
|
+
summary: "Snooze a category goal for a month",
|
|
15539
|
+
icon: "bell-off",
|
|
15540
|
+
group: "Categories",
|
|
15541
|
+
input: external_exports.object({
|
|
15542
|
+
category_id: external_exports.string().min(1).describe("Category ID whose goal to snooze"),
|
|
15543
|
+
month: external_exports.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, "Month must be YYYY-MM or YYYY-MM-DD").describe("Month in YYYY-MM format (e.g. 2026-04)"),
|
|
15544
|
+
snooze: external_exports.boolean().optional().describe("true to snooze (default), false to un-snooze")
|
|
15545
|
+
}),
|
|
15546
|
+
output: external_exports.object({
|
|
15547
|
+
success: external_exports.boolean(),
|
|
15548
|
+
snoozed_at: external_exports.string().nullable().describe("ISO timestamp the goal was snoozed at, or null if un-snoozed")
|
|
15549
|
+
}),
|
|
15550
|
+
handle: async (params) => {
|
|
15551
|
+
const planId = getPlanId();
|
|
15552
|
+
const monthKey = toMonthKey(params.month);
|
|
15553
|
+
const budgetId = formatSubcategoryBudgetId(monthKey, params.category_id);
|
|
15554
|
+
const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
|
|
15555
|
+
const shouldSnooze = params.snooze ?? true;
|
|
15556
|
+
const budget = await syncBudget(planId);
|
|
15557
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15558
|
+
const category = findCategory(budget.changed_entities, params.category_id);
|
|
15559
|
+
if (!category.goal_type) {
|
|
15560
|
+
throw ToolError.validation(`Category "${category.name}" has no goal to snooze.`);
|
|
15561
|
+
}
|
|
15562
|
+
const existing2 = (budget.changed_entities?.be_monthly_subcategory_budgets ?? []).find(
|
|
15563
|
+
(b) => b.id === budgetId && notTombstone(b)
|
|
15564
|
+
);
|
|
15565
|
+
const snoozedAt = shouldSnooze ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
15566
|
+
const budgetEntry = {
|
|
15567
|
+
...existing2 ?? {},
|
|
15568
|
+
id: budgetId,
|
|
15569
|
+
is_tombstone: false,
|
|
15570
|
+
entities_monthly_budget_id: monthlyBudgetId,
|
|
15571
|
+
entities_subcategory_id: params.category_id,
|
|
15572
|
+
budgeted: existing2?.budgeted ?? 0,
|
|
15573
|
+
goal_snoozed_at: snoozedAt
|
|
14785
15574
|
};
|
|
15575
|
+
await syncWrite(planId, { be_monthly_subcategory_budgets: [budgetEntry] }, serverKnowledge);
|
|
15576
|
+
return { success: true, snoozed_at: snoozedAt };
|
|
14786
15577
|
}
|
|
14787
15578
|
});
|
|
14788
15579
|
|
|
14789
|
-
// src/tools/
|
|
14790
|
-
var
|
|
14791
|
-
name: "
|
|
14792
|
-
displayName: "
|
|
14793
|
-
description:
|
|
14794
|
-
summary: "
|
|
14795
|
-
icon: "
|
|
14796
|
-
group: "
|
|
15580
|
+
// src/tools/update-category.ts
|
|
15581
|
+
var updateCategory = defineTool({
|
|
15582
|
+
name: "update_category",
|
|
15583
|
+
displayName: "Update Category",
|
|
15584
|
+
description: 'Rename a category, change its group, set/clear its goal, or hide/unhide it. Only specified fields change; omitted fields remain unchanged. Goal types: "set_aside" / "refill" (recurring NEED with optional cadence), "target_balance" (have $X), "target_by_date" (have $X by date), "debt" (recurring debt payment), or "none" to clear an existing goal.',
|
|
15585
|
+
summary: "Update a category",
|
|
15586
|
+
icon: "pencil",
|
|
15587
|
+
group: "Categories",
|
|
14797
15588
|
input: external_exports.object({
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
),
|
|
14803
|
-
|
|
14804
|
-
payee_id: external_exports.string().optional().describe("Existing payee ID (takes precedence over payee_name)"),
|
|
14805
|
-
category_id: external_exports.string().optional().describe("Category ID to assign"),
|
|
14806
|
-
memo: external_exports.string().optional().describe("Transaction memo"),
|
|
14807
|
-
cleared: external_exports.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status (default uncleared)"),
|
|
14808
|
-
approved: external_exports.boolean().optional().describe("Whether the transaction is approved (default true)"),
|
|
14809
|
-
flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color")
|
|
15589
|
+
category_id: external_exports.string().min(1).describe("Category ID to update"),
|
|
15590
|
+
name: external_exports.string().min(1).optional().describe("New name"),
|
|
15591
|
+
group_id: external_exports.string().min(1).optional().describe("New parent category group ID (to move the category)"),
|
|
15592
|
+
goal: goalSpecSchema.optional().describe('New goal definition. Pass { type: "none" } to clear the goal.'),
|
|
15593
|
+
hidden: external_exports.boolean().optional().describe("Hide or unhide the category"),
|
|
15594
|
+
note: external_exports.string().optional().describe("New note (pass empty string to clear)")
|
|
14810
15595
|
}),
|
|
14811
15596
|
output: external_exports.object({
|
|
14812
|
-
|
|
15597
|
+
category: categorySchema
|
|
14813
15598
|
}),
|
|
14814
15599
|
handle: async (params) => {
|
|
14815
15600
|
const planId = getPlanId();
|
|
14816
|
-
const
|
|
14817
|
-
const
|
|
14818
|
-
const
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
15601
|
+
const budget = await syncBudget(planId);
|
|
15602
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15603
|
+
const existing2 = findCategory(budget.changed_entities, params.category_id);
|
|
15604
|
+
if (params.group_id) {
|
|
15605
|
+
assertCategoryGroupDeletable(findCategoryGroup(budget.changed_entities, params.group_id));
|
|
15606
|
+
}
|
|
15607
|
+
if (params.goal?.type === "debt" && existing2.entities_account_id == null) {
|
|
15608
|
+
throw ToolError.validation("Debt goals can only be set on debt-account categories.");
|
|
15609
|
+
}
|
|
15610
|
+
const updated = {
|
|
15611
|
+
...existing2,
|
|
15612
|
+
name: params.name ?? existing2.name,
|
|
15613
|
+
entities_master_category_id: params.group_id ?? existing2.entities_master_category_id,
|
|
15614
|
+
is_hidden: params.hidden ?? existing2.is_hidden,
|
|
15615
|
+
note: params.note ?? existing2.note,
|
|
15616
|
+
...params.goal !== void 0 ? buildGoalFields(params.goal) : {}
|
|
14831
15617
|
};
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
15618
|
+
await syncWrite(planId, { be_subcategories: [updated] }, serverKnowledge);
|
|
15619
|
+
return { category: mapCategory(updated) };
|
|
15620
|
+
}
|
|
15621
|
+
});
|
|
15622
|
+
|
|
15623
|
+
// src/tools/update-category-budget.ts
|
|
15624
|
+
var updateCategoryBudget = defineTool({
|
|
15625
|
+
name: "update_category_budget",
|
|
15626
|
+
displayName: "Update Category Budget",
|
|
15627
|
+
description: "Set the budgeted amount for a category in a specific month. Amount is in currency units (e.g. 500 to budget $500). The month should be in YYYY-MM format (e.g. 2026-03 for March 2026).",
|
|
15628
|
+
summary: "Set budgeted amount for a category",
|
|
15629
|
+
icon: "pencil",
|
|
15630
|
+
group: "Categories",
|
|
15631
|
+
input: external_exports.object({
|
|
15632
|
+
category_id: external_exports.string().min(1).describe("Category ID to budget"),
|
|
15633
|
+
month: external_exports.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, "Month must be YYYY-MM or YYYY-MM-DD").describe("Month in YYYY-MM format (e.g. 2026-03)"),
|
|
15634
|
+
budgeted: external_exports.number().describe("Amount to budget in currency units (e.g. 500 for $500)")
|
|
15635
|
+
}),
|
|
15636
|
+
output: external_exports.object({
|
|
15637
|
+
category: categorySchema
|
|
15638
|
+
}),
|
|
15639
|
+
handle: async (params) => {
|
|
15640
|
+
const planId = getPlanId();
|
|
15641
|
+
const userId = getUserId();
|
|
15642
|
+
const milliunits = toMilliunits(params.budgeted);
|
|
15643
|
+
const monthKey = toMonthKey(params.month);
|
|
15644
|
+
const budgetId = formatSubcategoryBudgetId(monthKey, params.category_id);
|
|
15645
|
+
const monthlyBudgetId = formatMonthlyBudgetId(monthKey, planId);
|
|
15646
|
+
const budget = await syncBudget(planId);
|
|
15647
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15648
|
+
const category = findCategory(budget.changed_entities, params.category_id);
|
|
15649
|
+
const existingBudget = (budget.changed_entities?.be_monthly_subcategory_budgets ?? []).find(
|
|
15650
|
+
(b) => b.id === budgetId && notTombstone(b)
|
|
15651
|
+
);
|
|
15652
|
+
const delta = milliunits - (existingBudget?.budgeted ?? 0);
|
|
15653
|
+
const budgetEntry = {
|
|
15654
|
+
id: budgetId,
|
|
15655
|
+
is_tombstone: false,
|
|
15656
|
+
entities_monthly_budget_id: monthlyBudgetId,
|
|
15657
|
+
entities_subcategory_id: params.category_id,
|
|
15658
|
+
budgeted: milliunits
|
|
15659
|
+
};
|
|
15660
|
+
const changedEntities = { be_monthly_subcategory_budgets: [budgetEntry] };
|
|
15661
|
+
if (delta !== 0) {
|
|
15662
|
+
changedEntities.be_money_movements = [
|
|
15663
|
+
{
|
|
15664
|
+
id: crypto.randomUUID(),
|
|
15665
|
+
is_tombstone: false,
|
|
15666
|
+
to_entities_monthly_subcategory_budget_id: delta > 0 ? budgetId : null,
|
|
15667
|
+
from_entities_monthly_subcategory_budget_id: delta < 0 ? budgetId : null,
|
|
15668
|
+
entities_money_movement_group_id: null,
|
|
15669
|
+
amount: Math.abs(delta),
|
|
15670
|
+
performed_by_user_id: userId,
|
|
15671
|
+
note: null,
|
|
15672
|
+
source: MONEY_MOVEMENT_SOURCE.ASSIGN,
|
|
15673
|
+
move_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15674
|
+
move_accepted_at: null
|
|
15675
|
+
}
|
|
15676
|
+
];
|
|
14838
15677
|
}
|
|
14839
|
-
|
|
15678
|
+
const result = await syncWrite(planId, changedEntities, serverKnowledge);
|
|
15679
|
+
const calcMap = buildSubcategoryCalcMap(result.changed_entities?.be_monthly_subcategory_budget_calculations ?? []);
|
|
15680
|
+
const budgetMap = buildSubcategoryBudgetMap(result.changed_entities?.be_monthly_subcategory_budgets ?? []);
|
|
15681
|
+
const key = `${monthKey}/${params.category_id}`;
|
|
15682
|
+
if (!budgetMap.has(key)) budgetMap.set(key, budgetEntry);
|
|
15683
|
+
return { category: mapCategoryForMonth(category, budgetMap, calcMap, monthKey) };
|
|
14840
15684
|
}
|
|
14841
15685
|
});
|
|
14842
15686
|
|
|
14843
15687
|
// src/tools/update-transaction.ts
|
|
15688
|
+
var resolveFlag = (requested, existing2) => {
|
|
15689
|
+
if (requested === "none") return null;
|
|
15690
|
+
if (requested) return FLAG_MAP[requested];
|
|
15691
|
+
return existing2 ?? null;
|
|
15692
|
+
};
|
|
15693
|
+
var resolveCleared = (requested, existing2) => {
|
|
15694
|
+
if (requested) return CLEARED_MAP[requested];
|
|
15695
|
+
return existing2 ?? "Uncleared";
|
|
15696
|
+
};
|
|
14844
15697
|
var updateTransaction = defineTool({
|
|
14845
15698
|
name: "update_transaction",
|
|
14846
15699
|
displayName: "Update Transaction",
|
|
14847
|
-
description: "Update an existing transaction in the active YNAB plan. Only specified fields are changed; omitted fields remain unchanged. Amount is in currency units (negative for expenses, positive for income).",
|
|
15700
|
+
description: "Update an existing transaction in the active YNAB plan. Only specified fields are changed; omitted fields remain unchanged. Amount is in currency units (negative for expenses, positive for income). Transfers and split transactions cannot be updated through this tool \u2014 edit them directly in YNAB.",
|
|
14848
15701
|
summary: "Update a transaction",
|
|
14849
15702
|
icon: "pencil",
|
|
14850
15703
|
group: "Transactions",
|
|
@@ -14859,175 +15712,85 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
14859
15712
|
memo: external_exports.string().optional().describe("New transaction memo"),
|
|
14860
15713
|
cleared: external_exports.enum(["cleared", "uncleared", "reconciled"]).optional().describe("New cleared status"),
|
|
14861
15714
|
approved: external_exports.boolean().optional().describe("New approval status"),
|
|
14862
|
-
flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe(
|
|
15715
|
+
flag_color: external_exports.enum(["red", "orange", "yellow", "green", "blue", "purple", "none"]).optional().describe('New flag color (pass "none" to clear)')
|
|
14863
15716
|
}),
|
|
14864
15717
|
output: external_exports.object({
|
|
14865
15718
|
transaction: transactionSchema
|
|
14866
15719
|
}),
|
|
14867
15720
|
handle: async (params) => {
|
|
14868
15721
|
const planId = getPlanId();
|
|
14869
|
-
const
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
if (
|
|
14876
|
-
|
|
14877
|
-
if (params.category_id !== void 0) transaction.entities_subcategory_id = params.category_id;
|
|
14878
|
-
if (params.memo !== void 0) transaction.memo = params.memo;
|
|
14879
|
-
if (params.cleared !== void 0) transaction.cleared = params.cleared;
|
|
14880
|
-
if (params.approved !== void 0) transaction.approved = params.approved;
|
|
14881
|
-
if (params.flag_color !== void 0) transaction.flag_color = params.flag_color;
|
|
14882
|
-
const result = await syncWrite(planId, {
|
|
14883
|
-
be_transactions: [transaction]
|
|
14884
|
-
});
|
|
14885
|
-
const saved = result.changed_entities?.be_transactions?.[0];
|
|
14886
|
-
if (!saved) {
|
|
14887
|
-
throw ToolError.internal("Transaction was updated but no data was returned");
|
|
15722
|
+
const budget = await syncBudget(planId);
|
|
15723
|
+
const serverKnowledge = budget.current_server_knowledge ?? 0;
|
|
15724
|
+
const lookups = buildLookups(budget.changed_entities ?? {});
|
|
15725
|
+
const existing2 = budget.changed_entities?.be_transactions?.find(
|
|
15726
|
+
(t) => t.id === params.transaction_id && !t.is_tombstone
|
|
15727
|
+
);
|
|
15728
|
+
if (!existing2) {
|
|
15729
|
+
throw ToolError.notFound(`Transaction not found: ${params.transaction_id}`);
|
|
14888
15730
|
}
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
{
|
|
15731
|
+
if (existing2.transfer_account_id) {
|
|
15732
|
+
throw ToolError.validation("Cannot update transfer transactions \u2014 edit them in YNAB directly.");
|
|
15733
|
+
}
|
|
15734
|
+
const hasSubtransactions = (budget.changed_entities?.be_subtransactions ?? []).some(
|
|
15735
|
+
(s) => s.entities_transaction_id === params.transaction_id && !s.is_tombstone
|
|
15736
|
+
);
|
|
15737
|
+
if (hasSubtransactions) {
|
|
15738
|
+
throw ToolError.validation("Cannot update split transactions \u2014 edit them in YNAB directly.");
|
|
15739
|
+
}
|
|
15740
|
+
const changedEntities = {};
|
|
15741
|
+
let payeeId = params.payee_id ?? existing2.entities_payee_id ?? null;
|
|
15742
|
+
if (params.payee_name && !params.payee_id) {
|
|
15743
|
+
const resolved = resolvePayee(budget.changed_entities?.be_payees ?? [], params.payee_name);
|
|
15744
|
+
payeeId = resolved.payeeId;
|
|
15745
|
+
if (resolved.newPayee) {
|
|
15746
|
+
changedEntities.be_payees = [resolved.newPayee];
|
|
15747
|
+
lookups.payees.set(resolved.payeeId, params.payee_name);
|
|
15748
|
+
}
|
|
15749
|
+
}
|
|
15750
|
+
changedEntities.be_transaction_groups = [
|
|
15751
|
+
{
|
|
15752
|
+
id: params.transaction_id,
|
|
15753
|
+
be_transaction: {
|
|
14913
15754
|
id: params.transaction_id,
|
|
15755
|
+
is_tombstone: false,
|
|
14914
15756
|
entities_account_id: params.account_id,
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
// src/tools/list-months.ts
|
|
14945
|
-
var listMonths = defineTool({
|
|
14946
|
-
name: "list_months",
|
|
14947
|
-
displayName: "List Months",
|
|
14948
|
-
description: "List all budget months in the active YNAB plan. Returns income, budgeted, activity, and Ready to Assign amounts for each month. Sorted from most recent to oldest.",
|
|
14949
|
-
summary: "List budget months with summaries",
|
|
14950
|
-
icon: "calendar",
|
|
14951
|
-
group: "Months",
|
|
14952
|
-
input: external_exports.object({}),
|
|
14953
|
-
output: external_exports.object({
|
|
14954
|
-
months: external_exports.array(monthSchema).describe("List of budget months")
|
|
14955
|
-
}),
|
|
14956
|
-
handle: async () => {
|
|
14957
|
-
const planId = getPlanId();
|
|
14958
|
-
const result = await syncBudget(planId);
|
|
14959
|
-
const entities = result.changed_entities;
|
|
14960
|
-
const rawMonths = entities?.be_monthly_budgets ?? [];
|
|
14961
|
-
const rawCalcs = entities?.be_monthly_budget_calculations ?? [];
|
|
14962
|
-
const calcMap = /* @__PURE__ */ new Map();
|
|
14963
|
-
for (const calc of rawCalcs) {
|
|
14964
|
-
const budgetId = calc.entities_monthly_budget_id;
|
|
14965
|
-
if (budgetId) {
|
|
14966
|
-
const month = budgetId.replace("mb/", "");
|
|
14967
|
-
calcMap.set(month, calc);
|
|
15757
|
+
entities_payee_id: payeeId,
|
|
15758
|
+
entities_subcategory_id: params.category_id ?? existing2.entities_subcategory_id ?? null,
|
|
15759
|
+
entities_scheduled_transaction_id: existing2.entities_scheduled_transaction_id ?? null,
|
|
15760
|
+
date: params.date ?? existing2.date ?? "",
|
|
15761
|
+
date_entered_from_schedule: null,
|
|
15762
|
+
amount: params.amount !== void 0 ? toMilliunits(params.amount) : existing2.amount ?? 0,
|
|
15763
|
+
cash_amount: 0,
|
|
15764
|
+
credit_amount: 0,
|
|
15765
|
+
credit_amount_adjusted: 0,
|
|
15766
|
+
subcategory_credit_amount_preceding: 0,
|
|
15767
|
+
memo: params.memo ?? existing2.memo ?? null,
|
|
15768
|
+
cleared: resolveCleared(params.cleared, existing2.cleared),
|
|
15769
|
+
// YNAB's wire format calls this "accepted"; the public tool surface uses "approved".
|
|
15770
|
+
accepted: params.approved ?? existing2.accepted ?? false,
|
|
15771
|
+
check_number: null,
|
|
15772
|
+
flag: resolveFlag(params.flag_color, existing2.flag),
|
|
15773
|
+
transfer_account_id: existing2.transfer_account_id ?? null,
|
|
15774
|
+
transfer_transaction_id: null,
|
|
15775
|
+
transfer_subtransaction_id: null,
|
|
15776
|
+
matched_transaction_id: null,
|
|
15777
|
+
ynab_id: existing2.ynab_id ?? null,
|
|
15778
|
+
imported_payee: existing2.imported_payee ?? null,
|
|
15779
|
+
imported_date: null,
|
|
15780
|
+
original_imported_payee: existing2.original_imported_payee ?? null,
|
|
15781
|
+
provider_cleansed_payee: null,
|
|
15782
|
+
source: existing2.source ?? null,
|
|
15783
|
+
debt_transaction_type: null
|
|
15784
|
+
},
|
|
15785
|
+
be_subtransactions: null
|
|
14968
15786
|
}
|
|
15787
|
+
];
|
|
15788
|
+
const result = await syncWrite(planId, changedEntities, serverKnowledge);
|
|
15789
|
+
const saved = result.changed_entities?.be_transactions?.find((t) => t.id === params.transaction_id);
|
|
15790
|
+
if (!saved) {
|
|
15791
|
+
throw ToolError.internal("Transaction was updated but no data was returned");
|
|
14969
15792
|
}
|
|
14970
|
-
|
|
14971
|
-
return { months };
|
|
14972
|
-
}
|
|
14973
|
-
});
|
|
14974
|
-
|
|
14975
|
-
// src/tools/get-month.ts
|
|
14976
|
-
var getMonth = defineTool({
|
|
14977
|
-
name: "get_month",
|
|
14978
|
-
displayName: "Get Month",
|
|
14979
|
-
description: "Get budget summary and category details for a specific month. Returns the month overview (income, budgeted, activity, Ready to Assign) plus per-category breakdowns. Month format is YYYY-MM-DD using the first of the month (e.g. 2026-03-01).",
|
|
14980
|
-
summary: "Get budget details for a month",
|
|
14981
|
-
icon: "calendar",
|
|
14982
|
-
group: "Months",
|
|
14983
|
-
input: external_exports.object({
|
|
14984
|
-
month: external_exports.string().min(1).describe("Month in YYYY-MM-DD format (first of month, e.g. 2026-03-01)")
|
|
14985
|
-
}),
|
|
14986
|
-
output: external_exports.object({
|
|
14987
|
-
month: monthSchema,
|
|
14988
|
-
categories: external_exports.array(categorySchema).describe("Category budgets for this month")
|
|
14989
|
-
}),
|
|
14990
|
-
handle: async (params) => {
|
|
14991
|
-
const planId = getPlanId();
|
|
14992
|
-
const result = await syncBudget(planId);
|
|
14993
|
-
const entities = result.changed_entities;
|
|
14994
|
-
const rawMonths = entities?.be_monthly_budgets ?? [];
|
|
14995
|
-
const monthData = rawMonths.find((m) => m.month === params.month && !m.is_tombstone);
|
|
14996
|
-
if (!monthData) {
|
|
14997
|
-
throw ToolError.notFound(`Month not found: ${params.month}`);
|
|
14998
|
-
}
|
|
14999
|
-
const monthlyCalcs = entities?.be_monthly_budget_calculations ?? [];
|
|
15000
|
-
const monthCalc = monthlyCalcs.find((c) => {
|
|
15001
|
-
const budgetId = c.entities_monthly_budget_id;
|
|
15002
|
-
return budgetId && budgetId.replace("mb/", "") === params.month;
|
|
15003
|
-
});
|
|
15004
|
-
const rawCategories = (entities?.be_subcategories ?? []).filter((c) => !c.is_tombstone && c.is_hidden !== true);
|
|
15005
|
-
const subcatCalcs = entities?.be_monthly_subcategory_budget_calculations ?? [];
|
|
15006
|
-
const calcMap = /* @__PURE__ */ new Map();
|
|
15007
|
-
for (const calc of subcatCalcs) {
|
|
15008
|
-
const entityId = calc.entities_monthly_subcategory_budget_id;
|
|
15009
|
-
if (entityId) {
|
|
15010
|
-
const parts = entityId.split("/");
|
|
15011
|
-
const categoryId = parts.length >= 3 ? parts.slice(2).join("/") : entityId;
|
|
15012
|
-
calcMap.set(categoryId, calc);
|
|
15013
|
-
}
|
|
15014
|
-
}
|
|
15015
|
-
const categories = rawCategories.map((c) => {
|
|
15016
|
-
const calc = calcMap.get(c.id ?? "");
|
|
15017
|
-
return mapCategory({
|
|
15018
|
-
...c,
|
|
15019
|
-
budgeted: calc?.budgeted ?? c.budgeted,
|
|
15020
|
-
activity: calc?.activity ?? c.activity,
|
|
15021
|
-
balance: calc?.balance ?? c.balance,
|
|
15022
|
-
goal_type: calc?.goal_type ?? c.goal_type,
|
|
15023
|
-
goal_target: calc?.goal_target ?? c.goal_target,
|
|
15024
|
-
goal_percentage_complete: calc?.goal_percentage_complete ?? c.goal_percentage_complete
|
|
15025
|
-
});
|
|
15026
|
-
});
|
|
15027
|
-
return {
|
|
15028
|
-
month: mapMonth(monthData, monthCalc),
|
|
15029
|
-
categories
|
|
15030
|
-
};
|
|
15793
|
+
return { transaction: mapTransaction(saved, lookups) };
|
|
15031
15794
|
}
|
|
15032
15795
|
});
|
|
15033
15796
|
|
|
@@ -15048,7 +15811,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
15048
15811
|
getAccount,
|
|
15049
15812
|
// Categories
|
|
15050
15813
|
listCategories,
|
|
15814
|
+
createCategory,
|
|
15815
|
+
updateCategory,
|
|
15816
|
+
deleteCategory,
|
|
15817
|
+
createCategoryGroup,
|
|
15818
|
+
deleteCategoryGroup,
|
|
15051
15819
|
updateCategoryBudget,
|
|
15820
|
+
moveCategoryBudget,
|
|
15821
|
+
snoozeCategoryGoal,
|
|
15052
15822
|
// Payees
|
|
15053
15823
|
listPayees,
|
|
15054
15824
|
// Transactions
|
|
@@ -15069,7 +15839,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
15069
15839
|
};
|
|
15070
15840
|
var src_default = new YnabPlugin();
|
|
15071
15841
|
|
|
15072
|
-
// dist/
|
|
15842
|
+
// dist/_adapter_entry_0fc84288-9dca-44bb-92da-639aeea53a88.ts
|
|
15073
15843
|
if (!globalThis.__openTabs) {
|
|
15074
15844
|
globalThis.__openTabs = {};
|
|
15075
15845
|
} else {
|
|
@@ -15285,5 +16055,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
15285
16055
|
};
|
|
15286
16056
|
delete src_default.onDeactivate;
|
|
15287
16057
|
}
|
|
15288
|
-
})();(function(){var o=(globalThis).__openTabs;if(o&&o.adapters&&o.adapters["ynab"]){var a=o.adapters["ynab"];a.__adapterHash="
|
|
16058
|
+
})();(function(){var o=(globalThis).__openTabs;if(o&&o.adapters&&o.adapters["ynab"]){var a=o.adapters["ynab"];a.__adapterHash="3a2d11d4883f6b89713b9ed3995d0c919e24e6ae5699ae84dc071e130223def6";if(a.tools&&Array.isArray(a.tools)){for(var i=0;i<a.tools.length;i++){Object.freeze(a.tools[i]);}Object.freeze(a.tools);}Object.freeze(a);Object.defineProperty(o.adapters,"ynab",{value:a,writable:false,configurable:false,enumerable:true});Object.defineProperty(o,"adapters",{value:o.adapters,writable:false,configurable:false});}})();
|
|
15289
16059
|
//# sourceMappingURL=adapter.iife.js.map
|