copilot-money-mcp 1.7.1 → 2.0.0
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/cli.js +1038 -1219
- package/dist/server.js +1038 -1219
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -28537,7 +28537,6 @@ function getAccountDisplayName(account) {
|
|
|
28537
28537
|
}
|
|
28538
28538
|
|
|
28539
28539
|
// src/models/recurring.ts
|
|
28540
|
-
var RECURRING_STATES = ["active", "paused", "archived"];
|
|
28541
28540
|
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
28542
28541
|
var RecurringSchema = exports_external.object({
|
|
28543
28542
|
recurring_id: exports_external.string(),
|
|
@@ -28571,7 +28570,6 @@ function getRecurringDisplayName(recurring) {
|
|
|
28571
28570
|
}
|
|
28572
28571
|
|
|
28573
28572
|
// src/models/budget.ts
|
|
28574
|
-
var KNOWN_PERIODS = ["monthly", "yearly", "weekly", "daily"];
|
|
28575
28573
|
var DATE_REGEX2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
28576
28574
|
var BudgetSchema = exports_external.object({
|
|
28577
28575
|
budget_id: exports_external.string(),
|
|
@@ -32208,33 +32206,661 @@ class CopilotDatabase {
|
|
|
32208
32206
|
}
|
|
32209
32207
|
}
|
|
32210
32208
|
|
|
32211
|
-
// src/core/
|
|
32212
|
-
|
|
32213
|
-
|
|
32214
|
-
|
|
32215
|
-
|
|
32216
|
-
|
|
32217
|
-
|
|
32218
|
-
|
|
32219
|
-
|
|
32220
|
-
|
|
32209
|
+
// src/core/graphql/client.ts
|
|
32210
|
+
var ENDPOINT = "https://app.copilot.money/api/graphql";
|
|
32211
|
+
|
|
32212
|
+
class GraphQLError extends Error {
|
|
32213
|
+
code;
|
|
32214
|
+
operationName;
|
|
32215
|
+
httpStatus;
|
|
32216
|
+
serverErrors;
|
|
32217
|
+
constructor(code, message, operationName, httpStatus, serverErrors) {
|
|
32218
|
+
super(message);
|
|
32219
|
+
this.code = code;
|
|
32220
|
+
this.operationName = operationName;
|
|
32221
|
+
this.httpStatus = httpStatus;
|
|
32222
|
+
this.serverErrors = serverErrors;
|
|
32223
|
+
this.name = "GraphQLError";
|
|
32221
32224
|
}
|
|
32222
|
-
|
|
32223
|
-
|
|
32225
|
+
}
|
|
32226
|
+
|
|
32227
|
+
class GraphQLClient {
|
|
32228
|
+
auth;
|
|
32229
|
+
constructor(auth) {
|
|
32230
|
+
this.auth = auth;
|
|
32224
32231
|
}
|
|
32225
|
-
|
|
32226
|
-
|
|
32232
|
+
async mutate(operationName, query, variables) {
|
|
32233
|
+
const idToken = await this.auth.getIdToken();
|
|
32234
|
+
let response;
|
|
32235
|
+
try {
|
|
32236
|
+
response = await fetch(ENDPOINT, {
|
|
32237
|
+
method: "POST",
|
|
32238
|
+
headers: {
|
|
32239
|
+
Authorization: `Bearer ${idToken}`,
|
|
32240
|
+
"Content-Type": "application/json"
|
|
32241
|
+
},
|
|
32242
|
+
body: JSON.stringify({ operationName, query, variables })
|
|
32243
|
+
});
|
|
32244
|
+
} catch (e) {
|
|
32245
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
32246
|
+
this.logError(operationName, "NETWORK", undefined);
|
|
32247
|
+
throw new GraphQLError("NETWORK", msg, operationName);
|
|
32248
|
+
}
|
|
32249
|
+
if (response.status === 401) {
|
|
32250
|
+
const text = await response.text().catch(() => "");
|
|
32251
|
+
this.logError(operationName, "AUTH_FAILED", 401);
|
|
32252
|
+
throw new GraphQLError("AUTH_FAILED", `401 Unauthorized: ${text || "no body"}`, operationName, 401);
|
|
32253
|
+
}
|
|
32254
|
+
if (response.status === 400 || response.status === 500) {
|
|
32255
|
+
const text = await response.text().catch(() => "");
|
|
32256
|
+
this.logError(operationName, "SCHEMA_ERROR", response.status);
|
|
32257
|
+
throw new GraphQLError("SCHEMA_ERROR", `${response.status} Schema Error: ${text || "no body"}`, operationName, response.status);
|
|
32258
|
+
}
|
|
32259
|
+
if (!response.ok) {
|
|
32260
|
+
const text = await response.text().catch(() => "");
|
|
32261
|
+
this.logError(operationName, "UNKNOWN", response.status);
|
|
32262
|
+
throw new GraphQLError("UNKNOWN", `${response.status}: ${text || "no body"}`, operationName, response.status);
|
|
32263
|
+
}
|
|
32264
|
+
let body;
|
|
32265
|
+
try {
|
|
32266
|
+
body = await response.json();
|
|
32267
|
+
} catch (e) {
|
|
32268
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
32269
|
+
this.logError(operationName, "UNKNOWN", response.status);
|
|
32270
|
+
throw new GraphQLError("UNKNOWN", `Invalid JSON response: ${msg}`, operationName, response.status);
|
|
32271
|
+
}
|
|
32272
|
+
if (body.errors && body.errors.length > 0) {
|
|
32273
|
+
const firstMessage = body.errors[0]?.message ?? "GraphQL error (no message)";
|
|
32274
|
+
this.logError(operationName, "USER_ACTION_REQUIRED", response.status);
|
|
32275
|
+
throw new GraphQLError("USER_ACTION_REQUIRED", firstMessage, operationName, response.status, body.errors);
|
|
32276
|
+
}
|
|
32277
|
+
if (!body.data) {
|
|
32278
|
+
this.logError(operationName, "UNKNOWN", response.status);
|
|
32279
|
+
throw new GraphQLError("UNKNOWN", "Response missing data field", operationName, response.status);
|
|
32280
|
+
}
|
|
32281
|
+
return body.data;
|
|
32282
|
+
}
|
|
32283
|
+
logError(operationName, code, httpStatus) {
|
|
32284
|
+
const statusPart = httpStatus !== undefined ? ` status=${httpStatus}` : "";
|
|
32285
|
+
console.error(`[graphql] ${operationName} failed: code=${code}${statusPart}`);
|
|
32227
32286
|
}
|
|
32228
|
-
throw new Error(`Unsupported value type: ${typeof value}`);
|
|
32229
32287
|
}
|
|
32230
|
-
|
|
32231
|
-
|
|
32232
|
-
|
|
32233
|
-
|
|
32234
|
-
|
|
32235
|
-
|
|
32288
|
+
|
|
32289
|
+
// src/core/graphql/operations.generated.ts
|
|
32290
|
+
var EDIT_TRANSACTION = `mutation EditTransaction($itemId: ID!, $accountId: ID!, $id: ID!, $input: EditTransactionInput) {
|
|
32291
|
+
editTransaction(itemId: $itemId, accountId: $accountId, id: $id, input: $input) {
|
|
32292
|
+
__typename
|
|
32293
|
+
transaction {
|
|
32294
|
+
__typename
|
|
32295
|
+
...TransactionFields
|
|
32296
|
+
}
|
|
32297
|
+
}
|
|
32298
|
+
}
|
|
32299
|
+
|
|
32300
|
+
fragment TagFields on Tag {
|
|
32301
|
+
__typename
|
|
32302
|
+
colorName
|
|
32303
|
+
name
|
|
32304
|
+
id
|
|
32305
|
+
}
|
|
32306
|
+
|
|
32307
|
+
fragment GoalFields on Goal {
|
|
32308
|
+
__typename
|
|
32309
|
+
name
|
|
32310
|
+
icon {
|
|
32311
|
+
__typename
|
|
32312
|
+
... on EmojiUnicode {
|
|
32313
|
+
__typename
|
|
32314
|
+
unicode
|
|
32315
|
+
}
|
|
32316
|
+
... on Genmoji {
|
|
32317
|
+
__typename
|
|
32318
|
+
id
|
|
32319
|
+
src
|
|
32320
|
+
}
|
|
32321
|
+
}
|
|
32322
|
+
id
|
|
32323
|
+
}
|
|
32324
|
+
|
|
32325
|
+
fragment TransactionFields on Transaction {
|
|
32326
|
+
__typename
|
|
32327
|
+
suggestedCategoryIds
|
|
32328
|
+
recurringId
|
|
32329
|
+
categoryId
|
|
32330
|
+
isReviewed
|
|
32331
|
+
accountId
|
|
32332
|
+
createdAt
|
|
32333
|
+
isPending
|
|
32334
|
+
tipAmount
|
|
32335
|
+
userNotes
|
|
32336
|
+
itemId
|
|
32337
|
+
amount
|
|
32338
|
+
date
|
|
32339
|
+
name
|
|
32340
|
+
type
|
|
32341
|
+
id
|
|
32342
|
+
tags {
|
|
32343
|
+
__typename
|
|
32344
|
+
...TagFields
|
|
32345
|
+
}
|
|
32346
|
+
goal {
|
|
32347
|
+
__typename
|
|
32348
|
+
...GoalFields
|
|
32349
|
+
}
|
|
32350
|
+
}`;
|
|
32351
|
+
var CREATE_CATEGORY = `mutation CreateCategory($input: CreateCategoryInput!, $spend: Boolean = false, $budget: Boolean = false) {
|
|
32352
|
+
createCategory(input: $input) {
|
|
32353
|
+
__typename
|
|
32354
|
+
...CategoryFields
|
|
32355
|
+
spend @include(if: $spend) {
|
|
32356
|
+
__typename
|
|
32357
|
+
...SpendFields
|
|
32358
|
+
}
|
|
32359
|
+
budget @include(if: $budget) {
|
|
32360
|
+
__typename
|
|
32361
|
+
...BudgetFields
|
|
32362
|
+
}
|
|
32363
|
+
childCategories {
|
|
32364
|
+
__typename
|
|
32365
|
+
...CategoryFields
|
|
32366
|
+
spend @include(if: $spend) {
|
|
32367
|
+
__typename
|
|
32368
|
+
...SpendFields
|
|
32369
|
+
}
|
|
32370
|
+
budget @include(if: $budget) {
|
|
32371
|
+
__typename
|
|
32372
|
+
...BudgetFields
|
|
32373
|
+
}
|
|
32374
|
+
}
|
|
32375
|
+
}
|
|
32376
|
+
}
|
|
32377
|
+
|
|
32378
|
+
fragment SpendMonthlyFields on CategoryMonthlySpent {
|
|
32379
|
+
__typename
|
|
32380
|
+
unpaidRecurringAmount
|
|
32381
|
+
paidRecurringAmount
|
|
32382
|
+
comparisonAmount
|
|
32383
|
+
amount
|
|
32384
|
+
month
|
|
32385
|
+
id
|
|
32386
|
+
}
|
|
32387
|
+
|
|
32388
|
+
fragment BudgetMonthlyFields on CategoryMonthlyBudget {
|
|
32389
|
+
__typename
|
|
32390
|
+
unassignedRolloverAmount
|
|
32391
|
+
childRolloverAmount
|
|
32392
|
+
unassignedAmount
|
|
32393
|
+
resolvedAmount
|
|
32394
|
+
rolloverAmount
|
|
32395
|
+
childAmount
|
|
32396
|
+
goalAmount
|
|
32397
|
+
amount
|
|
32398
|
+
month
|
|
32399
|
+
id
|
|
32400
|
+
}
|
|
32401
|
+
|
|
32402
|
+
fragment CategoryFields on Category {
|
|
32403
|
+
__typename
|
|
32404
|
+
isRolloverDisabled
|
|
32405
|
+
canBeDeleted
|
|
32406
|
+
isExcluded
|
|
32407
|
+
templateId
|
|
32408
|
+
colorName
|
|
32409
|
+
icon {
|
|
32410
|
+
__typename
|
|
32411
|
+
... on EmojiUnicode {
|
|
32412
|
+
__typename
|
|
32413
|
+
unicode
|
|
32414
|
+
}
|
|
32415
|
+
... on Genmoji {
|
|
32416
|
+
__typename
|
|
32417
|
+
id
|
|
32418
|
+
src
|
|
32419
|
+
}
|
|
32420
|
+
}
|
|
32421
|
+
name
|
|
32422
|
+
id
|
|
32423
|
+
}
|
|
32424
|
+
|
|
32425
|
+
fragment SpendFields on CategorySpend {
|
|
32426
|
+
__typename
|
|
32427
|
+
current {
|
|
32428
|
+
__typename
|
|
32429
|
+
...SpendMonthlyFields
|
|
32430
|
+
}
|
|
32431
|
+
histories {
|
|
32432
|
+
__typename
|
|
32433
|
+
...SpendMonthlyFields
|
|
32434
|
+
}
|
|
32435
|
+
}
|
|
32436
|
+
|
|
32437
|
+
fragment BudgetFields on CategoryBudget {
|
|
32438
|
+
__typename
|
|
32439
|
+
current {
|
|
32440
|
+
__typename
|
|
32441
|
+
...BudgetMonthlyFields
|
|
32442
|
+
}
|
|
32443
|
+
histories {
|
|
32444
|
+
__typename
|
|
32445
|
+
...BudgetMonthlyFields
|
|
32446
|
+
}
|
|
32447
|
+
}`;
|
|
32448
|
+
var EDIT_CATEGORY = `mutation EditCategory($id: ID!, $input: EditCategoryInput!, $spend: Boolean = false, $budget: Boolean = false, $rollovers: Boolean = false) {
|
|
32449
|
+
editCategory(id: $id, input: $input) {
|
|
32450
|
+
__typename
|
|
32451
|
+
category {
|
|
32452
|
+
__typename
|
|
32453
|
+
...CategoryFields
|
|
32454
|
+
spend @include(if: $spend) {
|
|
32455
|
+
__typename
|
|
32456
|
+
...SpendFields
|
|
32457
|
+
}
|
|
32458
|
+
budget(isRolloverEnabled: $rollovers) @include(if: $budget) {
|
|
32459
|
+
__typename
|
|
32460
|
+
...BudgetFields
|
|
32461
|
+
}
|
|
32462
|
+
}
|
|
32463
|
+
}
|
|
32464
|
+
}
|
|
32465
|
+
|
|
32466
|
+
fragment SpendMonthlyFields on CategoryMonthlySpent {
|
|
32467
|
+
__typename
|
|
32468
|
+
unpaidRecurringAmount
|
|
32469
|
+
paidRecurringAmount
|
|
32470
|
+
comparisonAmount
|
|
32471
|
+
amount
|
|
32472
|
+
month
|
|
32473
|
+
id
|
|
32474
|
+
}
|
|
32475
|
+
|
|
32476
|
+
fragment BudgetMonthlyFields on CategoryMonthlyBudget {
|
|
32477
|
+
__typename
|
|
32478
|
+
unassignedRolloverAmount
|
|
32479
|
+
childRolloverAmount
|
|
32480
|
+
unassignedAmount
|
|
32481
|
+
resolvedAmount
|
|
32482
|
+
rolloverAmount
|
|
32483
|
+
childAmount
|
|
32484
|
+
goalAmount
|
|
32485
|
+
amount
|
|
32486
|
+
month
|
|
32487
|
+
id
|
|
32488
|
+
}
|
|
32489
|
+
|
|
32490
|
+
fragment CategoryFields on Category {
|
|
32491
|
+
__typename
|
|
32492
|
+
isRolloverDisabled
|
|
32493
|
+
canBeDeleted
|
|
32494
|
+
isExcluded
|
|
32495
|
+
templateId
|
|
32496
|
+
colorName
|
|
32497
|
+
icon {
|
|
32498
|
+
__typename
|
|
32499
|
+
... on EmojiUnicode {
|
|
32500
|
+
__typename
|
|
32501
|
+
unicode
|
|
32502
|
+
}
|
|
32503
|
+
... on Genmoji {
|
|
32504
|
+
__typename
|
|
32505
|
+
id
|
|
32506
|
+
src
|
|
32507
|
+
}
|
|
32508
|
+
}
|
|
32509
|
+
name
|
|
32510
|
+
id
|
|
32511
|
+
}
|
|
32512
|
+
|
|
32513
|
+
fragment SpendFields on CategorySpend {
|
|
32514
|
+
__typename
|
|
32515
|
+
current {
|
|
32516
|
+
__typename
|
|
32517
|
+
...SpendMonthlyFields
|
|
32518
|
+
}
|
|
32519
|
+
histories {
|
|
32520
|
+
__typename
|
|
32521
|
+
...SpendMonthlyFields
|
|
32522
|
+
}
|
|
32523
|
+
}
|
|
32524
|
+
|
|
32525
|
+
fragment BudgetFields on CategoryBudget {
|
|
32526
|
+
__typename
|
|
32527
|
+
current {
|
|
32528
|
+
__typename
|
|
32529
|
+
...BudgetMonthlyFields
|
|
32530
|
+
}
|
|
32531
|
+
histories {
|
|
32532
|
+
__typename
|
|
32533
|
+
...BudgetMonthlyFields
|
|
32534
|
+
}
|
|
32535
|
+
}`;
|
|
32536
|
+
var DELETE_CATEGORY = `mutation DeleteCategory($id: ID!) {
|
|
32537
|
+
deleteCategory(id: $id)
|
|
32538
|
+
}`;
|
|
32539
|
+
var CREATE_TAG = `mutation CreateTag($input: CreateTagInput!) {
|
|
32540
|
+
createTag(input: $input) {
|
|
32541
|
+
__typename
|
|
32542
|
+
...TagFields
|
|
32543
|
+
}
|
|
32544
|
+
}
|
|
32545
|
+
|
|
32546
|
+
fragment TagFields on Tag {
|
|
32547
|
+
__typename
|
|
32548
|
+
colorName
|
|
32549
|
+
name
|
|
32550
|
+
id
|
|
32551
|
+
}`;
|
|
32552
|
+
var EDIT_TAG = `mutation EditTag($id: ID!, $input: EditTagInput!) {
|
|
32553
|
+
editTag(id: $id, input: $input) {
|
|
32554
|
+
__typename
|
|
32555
|
+
...TagFields
|
|
32556
|
+
}
|
|
32557
|
+
}
|
|
32558
|
+
|
|
32559
|
+
fragment TagFields on Tag {
|
|
32560
|
+
__typename
|
|
32561
|
+
colorName
|
|
32562
|
+
name
|
|
32563
|
+
id
|
|
32564
|
+
}`;
|
|
32565
|
+
var DELETE_TAG = `mutation DeleteTag($id: ID!) {
|
|
32566
|
+
deleteTag(id: $id)
|
|
32567
|
+
}`;
|
|
32568
|
+
var CREATE_RECURRING = `mutation CreateRecurring($input: CreateRecurringInput!) {
|
|
32569
|
+
createRecurring(input: $input) {
|
|
32570
|
+
__typename
|
|
32571
|
+
...RecurringFields
|
|
32572
|
+
rule {
|
|
32573
|
+
__typename
|
|
32574
|
+
...RecurringRuleFields
|
|
32575
|
+
}
|
|
32576
|
+
payments {
|
|
32577
|
+
__typename
|
|
32578
|
+
...RecurringPaymentFields
|
|
32579
|
+
}
|
|
32580
|
+
}
|
|
32581
|
+
}
|
|
32582
|
+
|
|
32583
|
+
fragment RecurringFields on Recurring {
|
|
32584
|
+
__typename
|
|
32585
|
+
nextPaymentAmount
|
|
32586
|
+
nextPaymentDate
|
|
32587
|
+
categoryId
|
|
32588
|
+
frequency
|
|
32589
|
+
emoji
|
|
32590
|
+
icon {
|
|
32591
|
+
__typename
|
|
32592
|
+
... on EmojiUnicode {
|
|
32593
|
+
__typename
|
|
32594
|
+
unicode
|
|
32595
|
+
}
|
|
32596
|
+
... on Genmoji {
|
|
32597
|
+
__typename
|
|
32598
|
+
id
|
|
32599
|
+
src
|
|
32600
|
+
}
|
|
32601
|
+
}
|
|
32602
|
+
state
|
|
32603
|
+
name
|
|
32604
|
+
id
|
|
32605
|
+
}
|
|
32606
|
+
|
|
32607
|
+
fragment RecurringRuleFields on RecurringRule {
|
|
32608
|
+
__typename
|
|
32609
|
+
nameContains
|
|
32610
|
+
minAmount
|
|
32611
|
+
maxAmount
|
|
32612
|
+
days
|
|
32613
|
+
}
|
|
32614
|
+
|
|
32615
|
+
fragment RecurringPaymentFields on RecurringPayment {
|
|
32616
|
+
__typename
|
|
32617
|
+
amount
|
|
32618
|
+
isPaid
|
|
32619
|
+
date
|
|
32620
|
+
}`;
|
|
32621
|
+
var EDIT_RECURRING = `mutation EditRecurring($id: ID!, $input: EditRecurringInput!) {
|
|
32622
|
+
editRecurring(id: $id, input: $input) {
|
|
32623
|
+
__typename
|
|
32624
|
+
recurring {
|
|
32625
|
+
__typename
|
|
32626
|
+
...RecurringFields
|
|
32627
|
+
rule {
|
|
32628
|
+
__typename
|
|
32629
|
+
...RecurringRuleFields
|
|
32630
|
+
}
|
|
32631
|
+
payments {
|
|
32632
|
+
__typename
|
|
32633
|
+
...RecurringPaymentFields
|
|
32634
|
+
}
|
|
32635
|
+
}
|
|
32636
|
+
}
|
|
32637
|
+
}
|
|
32638
|
+
|
|
32639
|
+
fragment RecurringFields on Recurring {
|
|
32640
|
+
__typename
|
|
32641
|
+
nextPaymentAmount
|
|
32642
|
+
nextPaymentDate
|
|
32643
|
+
categoryId
|
|
32644
|
+
frequency
|
|
32645
|
+
emoji
|
|
32646
|
+
icon {
|
|
32647
|
+
__typename
|
|
32648
|
+
... on EmojiUnicode {
|
|
32649
|
+
__typename
|
|
32650
|
+
unicode
|
|
32651
|
+
}
|
|
32652
|
+
... on Genmoji {
|
|
32653
|
+
__typename
|
|
32654
|
+
id
|
|
32655
|
+
src
|
|
32656
|
+
}
|
|
32657
|
+
}
|
|
32658
|
+
state
|
|
32659
|
+
name
|
|
32660
|
+
id
|
|
32661
|
+
}
|
|
32662
|
+
|
|
32663
|
+
fragment RecurringRuleFields on RecurringRule {
|
|
32664
|
+
__typename
|
|
32665
|
+
nameContains
|
|
32666
|
+
minAmount
|
|
32667
|
+
maxAmount
|
|
32668
|
+
days
|
|
32669
|
+
}
|
|
32670
|
+
|
|
32671
|
+
fragment RecurringPaymentFields on RecurringPayment {
|
|
32672
|
+
__typename
|
|
32673
|
+
amount
|
|
32674
|
+
isPaid
|
|
32675
|
+
date
|
|
32676
|
+
}`;
|
|
32677
|
+
var DELETE_RECURRING = `mutation DeleteRecurring($deleteRecurringId: ID!) {
|
|
32678
|
+
deleteRecurring(id: $deleteRecurringId)
|
|
32679
|
+
}`;
|
|
32680
|
+
var EDIT_BUDGET = `mutation EditBudget($categoryId: ID!, $input: EditCategoryBudgetInput!) {
|
|
32681
|
+
editCategoryBudget(categoryId: $categoryId, input: $input)
|
|
32682
|
+
}`;
|
|
32683
|
+
var EDIT_BUDGET_MONTHLY = `mutation EditBudgetMonthly($categoryId: ID!, $input: [EditCategoryBudgetMonthlyInput!]!) {
|
|
32684
|
+
editCategoryBudgetMonthly(categoryId: $categoryId, input: $input)
|
|
32685
|
+
}`;
|
|
32686
|
+
|
|
32687
|
+
// src/core/graphql/transactions.ts
|
|
32688
|
+
async function editTransaction(client, args) {
|
|
32689
|
+
const data = await client.mutate("EditTransaction", EDIT_TRANSACTION, args);
|
|
32690
|
+
const tx = data.editTransaction.transaction;
|
|
32691
|
+
const changed = {};
|
|
32692
|
+
if ("categoryId" in args.input)
|
|
32693
|
+
changed.categoryId = tx.categoryId;
|
|
32694
|
+
if ("userNotes" in args.input)
|
|
32695
|
+
changed.userNotes = tx.userNotes;
|
|
32696
|
+
if ("isReviewed" in args.input)
|
|
32697
|
+
changed.isReviewed = tx.isReviewed;
|
|
32698
|
+
if ("tagIds" in args.input)
|
|
32699
|
+
changed.tagIds = tx.tags.map((t) => t.id);
|
|
32700
|
+
return { id: tx.id, changed };
|
|
32701
|
+
}
|
|
32702
|
+
|
|
32703
|
+
// src/core/graphql/categories.ts
|
|
32704
|
+
async function createCategory(client, args) {
|
|
32705
|
+
const data = await client.mutate("CreateCategory", CREATE_CATEGORY, { spend: false, budget: false, input: args.input });
|
|
32706
|
+
return {
|
|
32707
|
+
id: data.createCategory.id,
|
|
32708
|
+
name: data.createCategory.name,
|
|
32709
|
+
colorName: data.createCategory.colorName
|
|
32710
|
+
};
|
|
32711
|
+
}
|
|
32712
|
+
async function editCategory(client, args) {
|
|
32713
|
+
const data = await client.mutate("EditCategory", EDIT_CATEGORY, {
|
|
32714
|
+
id: args.id,
|
|
32715
|
+
spend: false,
|
|
32716
|
+
budget: false,
|
|
32717
|
+
input: args.input
|
|
32718
|
+
});
|
|
32719
|
+
const cat = data.editCategory.category;
|
|
32720
|
+
const changed = {};
|
|
32721
|
+
if ("name" in args.input)
|
|
32722
|
+
changed.name = cat.name;
|
|
32723
|
+
if ("colorName" in args.input)
|
|
32724
|
+
changed.colorName = cat.colorName;
|
|
32725
|
+
if ("emoji" in args.input)
|
|
32726
|
+
changed.emoji = args.input.emoji;
|
|
32727
|
+
if ("isExcluded" in args.input)
|
|
32728
|
+
changed.isExcluded = args.input.isExcluded;
|
|
32729
|
+
return { id: cat.id, changed };
|
|
32730
|
+
}
|
|
32731
|
+
async function deleteCategory(client, args) {
|
|
32732
|
+
await client.mutate("DeleteCategory", DELETE_CATEGORY, { id: args.id });
|
|
32733
|
+
return { id: args.id, deleted: true };
|
|
32734
|
+
}
|
|
32735
|
+
|
|
32736
|
+
// src/core/graphql/tags.ts
|
|
32737
|
+
async function createTag(client, args) {
|
|
32738
|
+
const data = await client.mutate("CreateTag", CREATE_TAG, args);
|
|
32739
|
+
return {
|
|
32740
|
+
id: data.createTag.id,
|
|
32741
|
+
name: data.createTag.name,
|
|
32742
|
+
colorName: data.createTag.colorName
|
|
32743
|
+
};
|
|
32744
|
+
}
|
|
32745
|
+
async function editTag(client, args) {
|
|
32746
|
+
const data = await client.mutate("EditTag", EDIT_TAG, args);
|
|
32747
|
+
const tag = data.editTag;
|
|
32748
|
+
const changed = {};
|
|
32749
|
+
if ("name" in args.input)
|
|
32750
|
+
changed.name = tag.name;
|
|
32751
|
+
if ("colorName" in args.input)
|
|
32752
|
+
changed.colorName = tag.colorName;
|
|
32753
|
+
return { id: tag.id, changed };
|
|
32754
|
+
}
|
|
32755
|
+
async function deleteTag(client, args) {
|
|
32756
|
+
await client.mutate("DeleteTag", DELETE_TAG, {
|
|
32757
|
+
id: args.id
|
|
32758
|
+
});
|
|
32759
|
+
return { id: args.id, deleted: true };
|
|
32760
|
+
}
|
|
32761
|
+
|
|
32762
|
+
// src/core/graphql/recurrings.ts
|
|
32763
|
+
async function createRecurring(client, args) {
|
|
32764
|
+
const data = await client.mutate("CreateRecurring", CREATE_RECURRING, args);
|
|
32765
|
+
return {
|
|
32766
|
+
id: data.createRecurring.id,
|
|
32767
|
+
name: data.createRecurring.name,
|
|
32768
|
+
state: data.createRecurring.state,
|
|
32769
|
+
frequency: data.createRecurring.frequency
|
|
32770
|
+
};
|
|
32771
|
+
}
|
|
32772
|
+
function toFloatOrThrow(value, path3) {
|
|
32773
|
+
if (!/^\d+(\.\d+)?$/.test(value)) {
|
|
32774
|
+
throw new Error(`editRecurring: invalid ${path3} ${value}`);
|
|
32775
|
+
}
|
|
32776
|
+
const n = parseFloat(value);
|
|
32777
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
32778
|
+
throw new Error(`editRecurring: invalid ${path3} ${value}`);
|
|
32779
|
+
}
|
|
32780
|
+
return n;
|
|
32781
|
+
}
|
|
32782
|
+
async function editRecurring(client, args) {
|
|
32783
|
+
const wireInput = {};
|
|
32784
|
+
if ("state" in args.input)
|
|
32785
|
+
wireInput.state = args.input.state;
|
|
32786
|
+
if ("rule" in args.input && args.input.rule) {
|
|
32787
|
+
const rule = args.input.rule;
|
|
32788
|
+
const wireRule = {};
|
|
32789
|
+
if ("nameContains" in rule)
|
|
32790
|
+
wireRule.nameContains = rule.nameContains;
|
|
32791
|
+
if ("minAmount" in rule && rule.minAmount !== undefined) {
|
|
32792
|
+
wireRule.minAmount = toFloatOrThrow(rule.minAmount, "rule.minAmount");
|
|
32793
|
+
}
|
|
32794
|
+
if ("maxAmount" in rule && rule.maxAmount !== undefined) {
|
|
32795
|
+
wireRule.maxAmount = toFloatOrThrow(rule.maxAmount, "rule.maxAmount");
|
|
32796
|
+
}
|
|
32797
|
+
if ("days" in rule)
|
|
32798
|
+
wireRule.days = rule.days;
|
|
32799
|
+
wireInput.rule = wireRule;
|
|
32800
|
+
}
|
|
32801
|
+
const data = await client.mutate("EditRecurring", EDIT_RECURRING, { id: args.id, input: wireInput });
|
|
32802
|
+
const recurring = data.editRecurring.recurring;
|
|
32803
|
+
const changed = {};
|
|
32804
|
+
if ("state" in args.input)
|
|
32805
|
+
changed.state = recurring.state;
|
|
32806
|
+
if ("rule" in args.input && recurring.rule) {
|
|
32807
|
+
const serverRule = recurring.rule;
|
|
32808
|
+
const stringRule = {};
|
|
32809
|
+
if ("nameContains" in serverRule)
|
|
32810
|
+
stringRule.nameContains = serverRule.nameContains;
|
|
32811
|
+
if ("minAmount" in serverRule && serverRule.minAmount !== undefined) {
|
|
32812
|
+
stringRule.minAmount = String(serverRule.minAmount);
|
|
32813
|
+
}
|
|
32814
|
+
if ("maxAmount" in serverRule && serverRule.maxAmount !== undefined) {
|
|
32815
|
+
stringRule.maxAmount = String(serverRule.maxAmount);
|
|
32816
|
+
}
|
|
32817
|
+
if ("days" in serverRule)
|
|
32818
|
+
stringRule.days = serverRule.days;
|
|
32819
|
+
changed.rule = stringRule;
|
|
32820
|
+
}
|
|
32821
|
+
return { id: recurring.id, changed };
|
|
32822
|
+
}
|
|
32823
|
+
async function deleteRecurring(client, args) {
|
|
32824
|
+
await client.mutate("DeleteRecurring", DELETE_RECURRING, { deleteRecurringId: args.id });
|
|
32825
|
+
return { id: args.id, deleted: true };
|
|
32826
|
+
}
|
|
32827
|
+
|
|
32828
|
+
// src/core/graphql/budgets.ts
|
|
32829
|
+
async function setBudget(client, args) {
|
|
32830
|
+
const cleared = args.amount === "0";
|
|
32831
|
+
const amountFloat = parseFloat(args.amount);
|
|
32832
|
+
if (!Number.isFinite(amountFloat) || amountFloat < 0) {
|
|
32833
|
+
throw new Error(`setBudget: invalid amount ${args.amount}`);
|
|
32834
|
+
}
|
|
32835
|
+
if (args.month) {
|
|
32836
|
+
await client.mutate("EditBudgetMonthly", EDIT_BUDGET_MONTHLY, {
|
|
32837
|
+
categoryId: args.categoryId,
|
|
32838
|
+
input: [{ amount: amountFloat, month: args.month }]
|
|
32839
|
+
});
|
|
32840
|
+
return { categoryId: args.categoryId, amount: args.amount, month: args.month, cleared };
|
|
32841
|
+
}
|
|
32842
|
+
await client.mutate("EditBudget", EDIT_BUDGET, {
|
|
32843
|
+
categoryId: args.categoryId,
|
|
32844
|
+
input: { amount: amountFloat }
|
|
32845
|
+
});
|
|
32846
|
+
return { categoryId: args.categoryId, amount: args.amount, cleared };
|
|
32847
|
+
}
|
|
32848
|
+
|
|
32849
|
+
// src/tools/errors.ts
|
|
32850
|
+
function graphQLErrorToMcpError(e) {
|
|
32851
|
+
switch (e.code) {
|
|
32852
|
+
case "AUTH_FAILED":
|
|
32853
|
+
return "Authentication with Copilot failed. Sign in to the Copilot web app and try again.";
|
|
32854
|
+
case "SCHEMA_ERROR":
|
|
32855
|
+
return "Copilot's API changed in a way this tool doesn't handle yet. Please report this issue.";
|
|
32856
|
+
case "USER_ACTION_REQUIRED":
|
|
32857
|
+
return e.message;
|
|
32858
|
+
case "NETWORK":
|
|
32859
|
+
return `Network error contacting Copilot: ${e.message}`;
|
|
32860
|
+
case "UNKNOWN":
|
|
32861
|
+
default:
|
|
32862
|
+
return `Copilot API request failed: ${e.message}`;
|
|
32236
32863
|
}
|
|
32237
|
-
return fields;
|
|
32238
32864
|
}
|
|
32239
32865
|
|
|
32240
32866
|
// src/utils/date.ts
|
|
@@ -32317,28 +32943,11 @@ function validateDocId(id, label) {
|
|
|
32317
32943
|
throw new Error(`Invalid ${label} format: ${id}`);
|
|
32318
32944
|
}
|
|
32319
32945
|
}
|
|
32320
|
-
var HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
32321
|
-
function validateHexColor(color) {
|
|
32322
|
-
if (!HEX_COLOR_RE.test(color)) {
|
|
32323
|
-
throw new Error(`Invalid color format: ${color} (expected #RRGGBB)`);
|
|
32324
|
-
}
|
|
32325
|
-
}
|
|
32326
|
-
function hexToBgColor(hex3) {
|
|
32327
|
-
const r = parseInt(hex3.slice(1, 3), 16);
|
|
32328
|
-
const g = parseInt(hex3.slice(3, 5), 16);
|
|
32329
|
-
const b = parseInt(hex3.slice(5, 7), 16);
|
|
32330
|
-
const t = 0.05;
|
|
32331
|
-
const blend = (c) => Math.round(255 - t * (255 - c));
|
|
32332
|
-
const toHex = (n) => n.toString(16).padStart(2, "0").toUpperCase();
|
|
32333
|
-
return `#${toHex(blend(r))}${toHex(blend(g))}${toHex(blend(b))}`;
|
|
32334
|
-
}
|
|
32335
32946
|
var CATEGORY_FOREIGN_TX_FEE_SNAKE = "bank_fees_foreign_transaction_fees";
|
|
32336
32947
|
var CATEGORY_FOREIGN_TX_FEE_NUMERIC = "10005000";
|
|
32337
32948
|
var MAX_QUERY_LIMIT = 1e4;
|
|
32338
32949
|
var DEFAULT_QUERY_LIMIT = 100;
|
|
32339
32950
|
var MIN_QUERY_LIMIT = 1;
|
|
32340
|
-
var REVIEW_BATCH_SIZE = 10;
|
|
32341
|
-
var VALID_RECURRING_FREQUENCIES = ["weekly", "biweekly", "monthly", "yearly"];
|
|
32342
32951
|
function validateLimit(limit, defaultValue = DEFAULT_QUERY_LIMIT) {
|
|
32343
32952
|
if (limit === undefined)
|
|
32344
32953
|
return defaultValue;
|
|
@@ -32417,18 +33026,18 @@ function normalizeMerchantName(name) {
|
|
|
32417
33026
|
|
|
32418
33027
|
class CopilotMoneyTools {
|
|
32419
33028
|
db;
|
|
32420
|
-
|
|
33029
|
+
graphqlClient;
|
|
32421
33030
|
_userCategoryMap = null;
|
|
32422
33031
|
_excludedCategoryIds = null;
|
|
32423
|
-
constructor(database,
|
|
33032
|
+
constructor(database, graphqlClient) {
|
|
32424
33033
|
this.db = database;
|
|
32425
|
-
this.
|
|
33034
|
+
this.graphqlClient = graphqlClient ?? null;
|
|
32426
33035
|
}
|
|
32427
|
-
|
|
32428
|
-
if (!this.
|
|
32429
|
-
throw new Error("Write
|
|
33036
|
+
getGraphQLClient() {
|
|
33037
|
+
if (!this.graphqlClient) {
|
|
33038
|
+
throw new Error("Write tools require --write flag to be set");
|
|
32430
33039
|
}
|
|
32431
|
-
return this.
|
|
33040
|
+
return this.graphqlClient;
|
|
32432
33041
|
}
|
|
32433
33042
|
async getUserCategoryMap() {
|
|
32434
33043
|
if (this._userCategoryMap === null) {
|
|
@@ -33451,92 +34060,41 @@ class CopilotMoneyTools {
|
|
|
33451
34060
|
};
|
|
33452
34061
|
}
|
|
33453
34062
|
async createCategory(args) {
|
|
33454
|
-
const client = this.
|
|
33455
|
-
|
|
33456
|
-
if (!name.trim()) {
|
|
34063
|
+
const client = this.getGraphQLClient();
|
|
34064
|
+
if (!args.name?.trim())
|
|
33457
34065
|
throw new Error("Category name must not be empty");
|
|
34066
|
+
if (!args.color_name?.trim())
|
|
34067
|
+
throw new Error("color_name is required");
|
|
34068
|
+
if (!args.emoji?.trim())
|
|
34069
|
+
throw new Error("emoji is required");
|
|
34070
|
+
if (args.parent_id !== undefined) {
|
|
34071
|
+
throw new Error("parent_id is not supported on create_category: Copilot's GraphQL API " + "does not accept parentId on CreateCategoryInput. Create the category " + "without a parent; the Copilot web app does not currently expose a " + "mutation to re-parent categories.");
|
|
33458
34072
|
}
|
|
33459
|
-
|
|
33460
|
-
|
|
33461
|
-
|
|
33462
|
-
|
|
33463
|
-
|
|
33464
|
-
|
|
33465
|
-
|
|
33466
|
-
|
|
33467
|
-
|
|
33468
|
-
|
|
33469
|
-
|
|
33470
|
-
|
|
33471
|
-
|
|
33472
|
-
|
|
33473
|
-
|
|
33474
|
-
|
|
33475
|
-
|
|
33476
|
-
|
|
33477
|
-
|
|
33478
|
-
const categoryEmoji = emoji3 ?? "\uD83D\uDCC1";
|
|
33479
|
-
const docFields = {
|
|
33480
|
-
name: trimmedName,
|
|
33481
|
-
emoji: categoryEmoji,
|
|
33482
|
-
color: categoryColor,
|
|
33483
|
-
bg_color: hexToBgColor(categoryColor),
|
|
33484
|
-
order: maxOrder + 1,
|
|
33485
|
-
excluded,
|
|
33486
|
-
is_other: false,
|
|
33487
|
-
auto_budget_lock: false,
|
|
33488
|
-
auto_delete_lock: false,
|
|
33489
|
-
plaid_category_ids: [],
|
|
33490
|
-
partial_name_rules: []
|
|
33491
|
-
};
|
|
33492
|
-
if (parent_category_id)
|
|
33493
|
-
docFields.parent_category_id = parent_category_id;
|
|
33494
|
-
const collectionPath = `users/${userId}/categories`;
|
|
33495
|
-
const firestoreFields = toFirestoreFields(docFields);
|
|
33496
|
-
const categoryId = await client.createDocument(collectionPath, undefined, firestoreFields);
|
|
33497
|
-
const idFields = toFirestoreFields({ id: categoryId });
|
|
33498
|
-
await client.updateDocument(collectionPath, categoryId, idFields, ["id"]);
|
|
33499
|
-
this.db.clearCache();
|
|
33500
|
-
this._userCategoryMap = null;
|
|
33501
|
-
const result = {
|
|
33502
|
-
success: true,
|
|
33503
|
-
category_id: categoryId,
|
|
33504
|
-
name: trimmedName,
|
|
33505
|
-
emoji: categoryEmoji,
|
|
33506
|
-
color: categoryColor,
|
|
33507
|
-
excluded
|
|
33508
|
-
};
|
|
33509
|
-
if (parent_category_id)
|
|
33510
|
-
result.parent_category_id = parent_category_id;
|
|
33511
|
-
return result;
|
|
33512
|
-
}
|
|
33513
|
-
async resolveTransaction(transactionId) {
|
|
33514
|
-
validateDocId(transactionId, "transaction_id");
|
|
33515
|
-
const transactions = await this.db.getAllTransactions();
|
|
33516
|
-
const txn = transactions.find((t) => t.transaction_id === transactionId);
|
|
33517
|
-
if (!txn) {
|
|
33518
|
-
throw new Error(`Transaction not found: ${transactionId}`);
|
|
33519
|
-
}
|
|
33520
|
-
if (!txn.item_id || !txn.account_id) {
|
|
33521
|
-
throw new Error(`Transaction ${transactionId} is missing item_id or account_id — cannot determine Firestore path`);
|
|
34073
|
+
try {
|
|
34074
|
+
const result = await createCategory(client, {
|
|
34075
|
+
input: {
|
|
34076
|
+
name: args.name.trim(),
|
|
34077
|
+
colorName: args.color_name,
|
|
34078
|
+
emoji: args.emoji,
|
|
34079
|
+
isExcluded: args.is_excluded ?? false
|
|
34080
|
+
}
|
|
34081
|
+
});
|
|
34082
|
+
return {
|
|
34083
|
+
success: true,
|
|
34084
|
+
category_id: result.id,
|
|
34085
|
+
name: result.name,
|
|
34086
|
+
color_name: result.colorName
|
|
34087
|
+
};
|
|
34088
|
+
} catch (e) {
|
|
34089
|
+
if (e instanceof GraphQLError)
|
|
34090
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34091
|
+
throw e;
|
|
33522
34092
|
}
|
|
33523
|
-
return {
|
|
33524
|
-
txn,
|
|
33525
|
-
collectionPath: `items/${txn.item_id}/accounts/${txn.account_id}/transactions`
|
|
33526
|
-
};
|
|
33527
34093
|
}
|
|
33528
34094
|
async updateTransaction(args) {
|
|
34095
|
+
const client = this.getGraphQLClient();
|
|
33529
34096
|
const { transaction_id } = args;
|
|
33530
|
-
const allowedKeys = new Set([
|
|
33531
|
-
"transaction_id",
|
|
33532
|
-
"category_id",
|
|
33533
|
-
"note",
|
|
33534
|
-
"tag_ids",
|
|
33535
|
-
"excluded",
|
|
33536
|
-
"name",
|
|
33537
|
-
"internal_transfer",
|
|
33538
|
-
"goal_id"
|
|
33539
|
-
]);
|
|
34097
|
+
const allowedKeys = new Set(["transaction_id", "category_id", "note", "tag_ids"]);
|
|
33540
34098
|
for (const key of Object.keys(args)) {
|
|
33541
34099
|
if (!allowedKeys.has(key)) {
|
|
33542
34100
|
throw new Error(`update_transaction: unknown field "${key}"`);
|
|
@@ -33546,19 +34104,16 @@ class CopilotMoneyTools {
|
|
|
33546
34104
|
if (mutableKeys.length === 0) {
|
|
33547
34105
|
throw new Error("update_transaction requires at least one field to update");
|
|
33548
34106
|
}
|
|
33549
|
-
|
|
33550
|
-
|
|
33551
|
-
|
|
33552
|
-
|
|
33553
|
-
|
|
33554
|
-
throw new Error("Transaction name must not be empty");
|
|
33555
|
-
}
|
|
34107
|
+
validateDocId(transaction_id, "transaction_id");
|
|
34108
|
+
const allTxns = await this.db.getAllTransactions();
|
|
34109
|
+
const txn = allTxns.find((t) => t.transaction_id === transaction_id);
|
|
34110
|
+
if (!txn) {
|
|
34111
|
+
throw new Error(`Transaction not found: ${transaction_id}`);
|
|
33556
34112
|
}
|
|
33557
34113
|
if ("category_id" in args && args.category_id !== undefined) {
|
|
33558
34114
|
validateDocId(args.category_id, "category_id");
|
|
33559
34115
|
const categories = await this.db.getUserCategories();
|
|
33560
|
-
|
|
33561
|
-
if (!category) {
|
|
34116
|
+
if (!categories.find((c) => c.category_id === args.category_id)) {
|
|
33562
34117
|
throw new Error(`Category not found: ${args.category_id}`);
|
|
33563
34118
|
}
|
|
33564
34119
|
}
|
|
@@ -33575,61 +34130,44 @@ class CopilotMoneyTools {
|
|
|
33575
34130
|
}
|
|
33576
34131
|
}
|
|
33577
34132
|
}
|
|
33578
|
-
|
|
33579
|
-
|
|
33580
|
-
|
|
33581
|
-
|
|
33582
|
-
|
|
33583
|
-
|
|
33584
|
-
|
|
33585
|
-
|
|
33586
|
-
|
|
33587
|
-
const cacheFields = {};
|
|
33588
|
-
if ("category_id" in args && args.category_id !== undefined) {
|
|
33589
|
-
firestoreFields.category_id = args.category_id;
|
|
33590
|
-
cacheFields.category_id = args.category_id;
|
|
34133
|
+
const input = {};
|
|
34134
|
+
if ("category_id" in args && args.category_id !== undefined)
|
|
34135
|
+
input.categoryId = args.category_id;
|
|
34136
|
+
if ("note" in args && args.note !== undefined)
|
|
34137
|
+
input.userNotes = args.note;
|
|
34138
|
+
if ("tag_ids" in args && args.tag_ids !== undefined)
|
|
34139
|
+
input.tagIds = args.tag_ids;
|
|
34140
|
+
if (!txn.account_id || !txn.item_id) {
|
|
34141
|
+
throw new Error(`Transaction ${transaction_id} missing account_id or item_id in local cache`);
|
|
33591
34142
|
}
|
|
33592
|
-
|
|
33593
|
-
|
|
33594
|
-
|
|
34143
|
+
try {
|
|
34144
|
+
const result = await editTransaction(client, {
|
|
34145
|
+
id: transaction_id,
|
|
34146
|
+
accountId: txn.account_id,
|
|
34147
|
+
itemId: txn.item_id,
|
|
34148
|
+
input
|
|
34149
|
+
});
|
|
34150
|
+
const graphqlToApiName = {
|
|
34151
|
+
categoryId: "category_id",
|
|
34152
|
+
userNotes: "note",
|
|
34153
|
+
tagIds: "tag_ids",
|
|
34154
|
+
isReviewed: "reviewed"
|
|
34155
|
+
};
|
|
34156
|
+
const updated = Object.keys(result.changed).map((k) => graphqlToApiName[k] ?? k);
|
|
34157
|
+
return {
|
|
34158
|
+
success: true,
|
|
34159
|
+
transaction_id: result.id,
|
|
34160
|
+
updated
|
|
34161
|
+
};
|
|
34162
|
+
} catch (e) {
|
|
34163
|
+
if (e instanceof GraphQLError) {
|
|
34164
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34165
|
+
}
|
|
34166
|
+
throw e;
|
|
33595
34167
|
}
|
|
33596
|
-
if ("tag_ids" in args && args.tag_ids !== undefined) {
|
|
33597
|
-
firestoreFields.tag_ids = args.tag_ids;
|
|
33598
|
-
cacheFields.tag_ids = args.tag_ids;
|
|
33599
|
-
}
|
|
33600
|
-
if ("excluded" in args && args.excluded !== undefined) {
|
|
33601
|
-
firestoreFields.excluded = args.excluded;
|
|
33602
|
-
cacheFields.excluded = args.excluded;
|
|
33603
|
-
}
|
|
33604
|
-
if ("name" in args && trimmedName !== undefined) {
|
|
33605
|
-
firestoreFields.name = trimmedName;
|
|
33606
|
-
cacheFields.name = trimmedName;
|
|
33607
|
-
}
|
|
33608
|
-
if ("internal_transfer" in args && args.internal_transfer !== undefined) {
|
|
33609
|
-
firestoreFields.internal_transfer = args.internal_transfer;
|
|
33610
|
-
cacheFields.internal_transfer = args.internal_transfer;
|
|
33611
|
-
}
|
|
33612
|
-
if ("goal_id" in args && args.goal_id !== undefined) {
|
|
33613
|
-
firestoreFields.goal_id = args.goal_id ?? "";
|
|
33614
|
-
cacheFields.goal_id = args.goal_id ?? undefined;
|
|
33615
|
-
}
|
|
33616
|
-
const client = this.getFirestoreClient();
|
|
33617
|
-
const firestoreValue = toFirestoreFields(firestoreFields);
|
|
33618
|
-
const updateMask = Object.keys(firestoreFields);
|
|
33619
|
-
await client.updateDocument(collectionPath, transaction_id, firestoreValue, updateMask);
|
|
33620
|
-
if (!this.db.patchCachedTransaction(transaction_id, cacheFields)) {
|
|
33621
|
-
this.db.clearCache();
|
|
33622
|
-
}
|
|
33623
|
-
const firestoreToApiName = { user_note: "note" };
|
|
33624
|
-
const updated = updateMask.map((k) => firestoreToApiName[k] ?? k);
|
|
33625
|
-
return {
|
|
33626
|
-
success: true,
|
|
33627
|
-
transaction_id,
|
|
33628
|
-
updated
|
|
33629
|
-
};
|
|
33630
34168
|
}
|
|
33631
34169
|
async reviewTransactions(args) {
|
|
33632
|
-
const client = this.
|
|
34170
|
+
const client = this.getGraphQLClient();
|
|
33633
34171
|
const { transaction_ids, reviewed = true } = args;
|
|
33634
34172
|
if (!Array.isArray(transaction_ids) || transaction_ids.length === 0) {
|
|
33635
34173
|
throw new Error("transaction_ids must be a non-empty array");
|
|
@@ -33639,663 +34177,274 @@ class CopilotMoneyTools {
|
|
|
33639
34177
|
}
|
|
33640
34178
|
const allTransactions = await this.db.getAllTransactions();
|
|
33641
34179
|
const txnMap = new Map(allTransactions.map((t) => [t.transaction_id, t]));
|
|
33642
|
-
const
|
|
34180
|
+
const missing = transaction_ids.filter((id) => !txnMap.has(id));
|
|
34181
|
+
if (missing.length > 0) {
|
|
34182
|
+
throw new Error(`Transactions not found: ${missing.join(", ")}`);
|
|
34183
|
+
}
|
|
33643
34184
|
for (const id of transaction_ids) {
|
|
33644
34185
|
const txn = txnMap.get(id);
|
|
33645
|
-
if (!txn) {
|
|
33646
|
-
throw new Error(`Transaction
|
|
33647
|
-
}
|
|
33648
|
-
if (!txn.item_id || !txn.account_id) {
|
|
33649
|
-
throw new Error(`Transaction ${id} is missing item_id or account_id — cannot determine Firestore path`);
|
|
34186
|
+
if (!txn.account_id || !txn.item_id) {
|
|
34187
|
+
throw new Error(`Transaction ${id} missing account_id or item_id in local cache`);
|
|
33650
34188
|
}
|
|
33651
|
-
resolvedTxns.push(txn);
|
|
33652
34189
|
}
|
|
33653
|
-
const
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
|
|
33661
|
-
|
|
33662
|
-
|
|
34190
|
+
const CONCURRENCY = 5;
|
|
34191
|
+
let reviewed_count = 0;
|
|
34192
|
+
let firstError = null;
|
|
34193
|
+
let cursor = 0;
|
|
34194
|
+
const worker = async () => {
|
|
34195
|
+
while (true) {
|
|
34196
|
+
if (firstError)
|
|
34197
|
+
return;
|
|
34198
|
+
const idx = cursor++;
|
|
34199
|
+
if (idx >= transaction_ids.length)
|
|
34200
|
+
return;
|
|
34201
|
+
const id = transaction_ids[idx];
|
|
34202
|
+
const txn = txnMap.get(id);
|
|
34203
|
+
try {
|
|
34204
|
+
await editTransaction(client, {
|
|
34205
|
+
id,
|
|
34206
|
+
accountId: txn.account_id,
|
|
34207
|
+
itemId: txn.item_id,
|
|
34208
|
+
input: { isReviewed: reviewed }
|
|
34209
|
+
});
|
|
34210
|
+
reviewed_count++;
|
|
34211
|
+
} catch (e) {
|
|
34212
|
+
if (!firstError)
|
|
34213
|
+
firstError = { id, error: e };
|
|
34214
|
+
return;
|
|
33663
34215
|
}
|
|
33664
|
-
}
|
|
34216
|
+
}
|
|
34217
|
+
};
|
|
34218
|
+
const workerCount = Math.min(CONCURRENCY, transaction_ids.length);
|
|
34219
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
34220
|
+
if (firstError) {
|
|
34221
|
+
const { id, error: error48 } = firstError;
|
|
34222
|
+
if (error48 instanceof GraphQLError) {
|
|
34223
|
+
throw new Error(`review_transactions failed at id=${id} (${reviewed_count}/${transaction_ids.length} succeeded): ${graphQLErrorToMcpError(error48)}`, { cause: error48 });
|
|
34224
|
+
}
|
|
34225
|
+
throw error48;
|
|
33665
34226
|
}
|
|
33666
34227
|
return {
|
|
33667
34228
|
success: true,
|
|
33668
|
-
reviewed_count
|
|
33669
|
-
transaction_ids
|
|
34229
|
+
reviewed_count,
|
|
34230
|
+
transaction_ids
|
|
33670
34231
|
};
|
|
33671
34232
|
}
|
|
33672
34233
|
async createTag(args) {
|
|
33673
|
-
const client = this.
|
|
33674
|
-
|
|
33675
|
-
const trimmedName = name.trim();
|
|
33676
|
-
if (!trimmedName) {
|
|
34234
|
+
const client = this.getGraphQLClient();
|
|
34235
|
+
if (!args.name?.trim())
|
|
33677
34236
|
throw new Error("Tag name must not be empty");
|
|
34237
|
+
const colorName = args.color_name ?? "PURPLE2";
|
|
34238
|
+
try {
|
|
34239
|
+
const result = await createTag(client, {
|
|
34240
|
+
input: { name: args.name.trim(), colorName }
|
|
34241
|
+
});
|
|
34242
|
+
return {
|
|
34243
|
+
success: true,
|
|
34244
|
+
tag_id: result.id,
|
|
34245
|
+
name: result.name,
|
|
34246
|
+
color_name: result.colorName
|
|
34247
|
+
};
|
|
34248
|
+
} catch (e) {
|
|
34249
|
+
if (e instanceof GraphQLError)
|
|
34250
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34251
|
+
throw e;
|
|
33678
34252
|
}
|
|
33679
|
-
const tag_id = trimmedName.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_-]/g, "");
|
|
33680
|
-
if (!tag_id) {
|
|
33681
|
-
throw new Error(`Cannot generate a valid tag_id from name: ${trimmedName}`);
|
|
33682
|
-
}
|
|
33683
|
-
if (hex_color !== undefined)
|
|
33684
|
-
validateHexColor(hex_color);
|
|
33685
|
-
const existingTags = await this.db.getTags();
|
|
33686
|
-
const duplicate = existingTags.find((t) => t.tag_id === tag_id);
|
|
33687
|
-
if (duplicate) {
|
|
33688
|
-
throw new Error(`Tag "${trimmedName}" already exists (id: ${tag_id})`);
|
|
33689
|
-
}
|
|
33690
|
-
const userId = await client.requireUserId();
|
|
33691
|
-
const docFields = { name: trimmedName };
|
|
33692
|
-
if (color_name !== undefined)
|
|
33693
|
-
docFields.color_name = color_name;
|
|
33694
|
-
if (hex_color !== undefined)
|
|
33695
|
-
docFields.hex_color = hex_color;
|
|
33696
|
-
const firestoreFields = toFirestoreFields(docFields);
|
|
33697
|
-
const collectionPath = `users/${userId}/tags`;
|
|
33698
|
-
await client.createDocument(collectionPath, tag_id, firestoreFields);
|
|
33699
|
-
this.db.clearCache();
|
|
33700
|
-
const result = {
|
|
33701
|
-
success: true,
|
|
33702
|
-
tag_id,
|
|
33703
|
-
name: trimmedName
|
|
33704
|
-
};
|
|
33705
|
-
if (color_name !== undefined)
|
|
33706
|
-
result.color_name = color_name;
|
|
33707
|
-
if (hex_color !== undefined)
|
|
33708
|
-
result.hex_color = hex_color;
|
|
33709
|
-
return result;
|
|
33710
34253
|
}
|
|
33711
34254
|
async deleteTag(args) {
|
|
33712
|
-
const client = this.
|
|
33713
|
-
|
|
33714
|
-
|
|
33715
|
-
|
|
33716
|
-
|
|
33717
|
-
|
|
33718
|
-
|
|
33719
|
-
|
|
33720
|
-
|
|
33721
|
-
const collectionPath = `users/${userId}/tags`;
|
|
33722
|
-
await client.deleteDocument(collectionPath, tag_id);
|
|
33723
|
-
this.db.clearCache();
|
|
33724
|
-
return {
|
|
33725
|
-
success: true,
|
|
33726
|
-
tag_id,
|
|
33727
|
-
deleted_name: tag.name ?? tag_id
|
|
33728
|
-
};
|
|
34255
|
+
const client = this.getGraphQLClient();
|
|
34256
|
+
try {
|
|
34257
|
+
const result = await deleteTag(client, { id: args.tag_id });
|
|
34258
|
+
return { success: true, tag_id: result.id, deleted: true };
|
|
34259
|
+
} catch (e) {
|
|
34260
|
+
if (e instanceof GraphQLError)
|
|
34261
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34262
|
+
throw e;
|
|
34263
|
+
}
|
|
33729
34264
|
}
|
|
33730
34265
|
async updateCategory(args) {
|
|
33731
|
-
const client = this.
|
|
33732
|
-
const
|
|
33733
|
-
|
|
33734
|
-
|
|
33735
|
-
|
|
33736
|
-
|
|
33737
|
-
|
|
33738
|
-
|
|
33739
|
-
|
|
33740
|
-
|
|
33741
|
-
if (
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
33745
|
-
}
|
|
33746
|
-
|
|
33747
|
-
|
|
33748
|
-
|
|
33749
|
-
|
|
33750
|
-
|
|
33751
|
-
|
|
33752
|
-
|
|
33753
|
-
|
|
33754
|
-
|
|
33755
|
-
|
|
33756
|
-
}
|
|
33757
|
-
if (color !== undefined) {
|
|
33758
|
-
validateHexColor(color);
|
|
33759
|
-
fieldsToUpdate.color = color;
|
|
33760
|
-
fieldsToUpdate.bg_color = hexToBgColor(color);
|
|
33761
|
-
updateMask.push("color", "bg_color");
|
|
33762
|
-
}
|
|
33763
|
-
if (excluded !== undefined) {
|
|
33764
|
-
fieldsToUpdate.excluded = excluded;
|
|
33765
|
-
updateMask.push("excluded");
|
|
33766
|
-
}
|
|
33767
|
-
if (parent_category_id !== undefined) {
|
|
33768
|
-
if (parent_category_id !== null) {
|
|
33769
|
-
validateDocId(parent_category_id, "parent_category_id");
|
|
33770
|
-
if (parent_category_id === category_id) {
|
|
33771
|
-
throw new Error("A category cannot be its own parent");
|
|
33772
|
-
}
|
|
33773
|
-
const parent = existingCategories.find((c) => c.category_id === parent_category_id);
|
|
33774
|
-
if (!parent) {
|
|
33775
|
-
throw new Error(`Parent category not found: ${parent_category_id}`);
|
|
33776
|
-
}
|
|
33777
|
-
}
|
|
33778
|
-
fieldsToUpdate.parent_category_id = parent_category_id ?? "";
|
|
33779
|
-
updateMask.push("parent_category_id");
|
|
33780
|
-
}
|
|
33781
|
-
if (updateMask.length === 0) {
|
|
33782
|
-
throw new Error("No fields to update");
|
|
33783
|
-
}
|
|
33784
|
-
const userIdFromCategories = existingCategories.find((c) => c.user_id)?.user_id;
|
|
33785
|
-
const userId = userIdFromCategories ?? await client.requireUserId();
|
|
33786
|
-
const collectionPath = `users/${userId}/categories`;
|
|
33787
|
-
const firestoreFields = toFirestoreFields(fieldsToUpdate);
|
|
33788
|
-
await client.updateDocument(collectionPath, category_id, firestoreFields, updateMask);
|
|
33789
|
-
this.db.clearCache();
|
|
33790
|
-
this._userCategoryMap = null;
|
|
33791
|
-
return {
|
|
33792
|
-
success: true,
|
|
33793
|
-
category_id,
|
|
33794
|
-
updated_fields: updateMask
|
|
33795
|
-
};
|
|
34266
|
+
const client = this.getGraphQLClient();
|
|
34267
|
+
const input = {};
|
|
34268
|
+
if (args.name !== undefined)
|
|
34269
|
+
input.name = args.name;
|
|
34270
|
+
if (args.color_name !== undefined)
|
|
34271
|
+
input.colorName = args.color_name;
|
|
34272
|
+
if (args.emoji !== undefined)
|
|
34273
|
+
input.emoji = args.emoji;
|
|
34274
|
+
if (args.is_excluded !== undefined)
|
|
34275
|
+
input.isExcluded = args.is_excluded;
|
|
34276
|
+
if (Object.keys(input).length === 0) {
|
|
34277
|
+
throw new Error("update_category requires at least one field to update");
|
|
34278
|
+
}
|
|
34279
|
+
try {
|
|
34280
|
+
const result = await editCategory(client, { id: args.category_id, input });
|
|
34281
|
+
return {
|
|
34282
|
+
success: true,
|
|
34283
|
+
category_id: result.id,
|
|
34284
|
+
updated: Object.keys(result.changed)
|
|
34285
|
+
};
|
|
34286
|
+
} catch (e) {
|
|
34287
|
+
if (e instanceof GraphQLError)
|
|
34288
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34289
|
+
throw e;
|
|
34290
|
+
}
|
|
33796
34291
|
}
|
|
33797
34292
|
async deleteCategory(args) {
|
|
33798
|
-
const client = this.
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
33806
|
-
const userIdFromCategories = existingCategories.find((c) => c.user_id)?.user_id;
|
|
33807
|
-
const userId = userIdFromCategories ?? await client.requireUserId();
|
|
33808
|
-
const collectionPath = `users/${userId}/categories`;
|
|
33809
|
-
await client.deleteDocument(collectionPath, category_id);
|
|
33810
|
-
this.db.clearCache();
|
|
33811
|
-
this._userCategoryMap = null;
|
|
33812
|
-
return {
|
|
33813
|
-
success: true,
|
|
33814
|
-
category_id,
|
|
33815
|
-
deleted_name: category.name ?? category_id
|
|
33816
|
-
};
|
|
33817
|
-
}
|
|
33818
|
-
async createBudget(args) {
|
|
33819
|
-
const client = this.getFirestoreClient();
|
|
33820
|
-
const { category_id, amount, period = "monthly", name } = args;
|
|
33821
|
-
validateDocId(category_id, "category_id");
|
|
33822
|
-
if (amount <= 0) {
|
|
33823
|
-
throw new Error("Budget amount must be greater than 0");
|
|
33824
|
-
}
|
|
33825
|
-
if (!KNOWN_PERIODS.includes(period)) {
|
|
33826
|
-
throw new Error(`Invalid period: ${period}. Must be one of: ${KNOWN_PERIODS.join(", ")}`);
|
|
33827
|
-
}
|
|
33828
|
-
const categories = await this.db.getUserCategories();
|
|
33829
|
-
const category = categories.find((c) => c.category_id === category_id);
|
|
33830
|
-
if (!category) {
|
|
33831
|
-
throw new Error(`Category not found: ${category_id}`);
|
|
33832
|
-
}
|
|
33833
|
-
const existingBudgets = await this.db.getBudgets();
|
|
33834
|
-
const duplicate = existingBudgets.find((b) => b.category_id === category_id);
|
|
33835
|
-
if (duplicate) {
|
|
33836
|
-
throw new Error(`A budget already exists for category "${category_id}" (budget_id: ${duplicate.budget_id})`);
|
|
33837
|
-
}
|
|
33838
|
-
const budgetId = `budget_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
33839
|
-
const userId = await client.requireUserId();
|
|
33840
|
-
const docFields = {
|
|
33841
|
-
budget_id: budgetId,
|
|
33842
|
-
category_id,
|
|
33843
|
-
amount,
|
|
33844
|
-
period,
|
|
33845
|
-
is_active: true
|
|
33846
|
-
};
|
|
33847
|
-
if (name)
|
|
33848
|
-
docFields.name = name;
|
|
33849
|
-
const collectionPath = `users/${userId}/budgets`;
|
|
33850
|
-
const firestoreFields = toFirestoreFields(docFields);
|
|
33851
|
-
await client.createDocument(collectionPath, budgetId, firestoreFields);
|
|
33852
|
-
this.db.clearCache();
|
|
33853
|
-
const result = {
|
|
33854
|
-
success: true,
|
|
33855
|
-
budget_id: budgetId,
|
|
33856
|
-
category_id,
|
|
33857
|
-
amount,
|
|
33858
|
-
period
|
|
33859
|
-
};
|
|
33860
|
-
if (name)
|
|
33861
|
-
result.name = name;
|
|
33862
|
-
return result;
|
|
33863
|
-
}
|
|
33864
|
-
async updateBudget(args) {
|
|
33865
|
-
const client = this.getFirestoreClient();
|
|
33866
|
-
const { budget_id, amount, period, name, is_active } = args;
|
|
33867
|
-
validateDocId(budget_id, "budget_id");
|
|
33868
|
-
const existingBudgets = await this.db.getBudgets();
|
|
33869
|
-
const budget = existingBudgets.find((b) => b.budget_id === budget_id);
|
|
33870
|
-
if (!budget) {
|
|
33871
|
-
throw new Error(`Budget not found: ${budget_id}`);
|
|
33872
|
-
}
|
|
33873
|
-
const updateFields = {};
|
|
33874
|
-
const updatedFieldNames = [];
|
|
33875
|
-
if (amount !== undefined) {
|
|
33876
|
-
if (amount <= 0) {
|
|
33877
|
-
throw new Error("Budget amount must be greater than 0");
|
|
33878
|
-
}
|
|
33879
|
-
updateFields.amount = amount;
|
|
33880
|
-
updatedFieldNames.push("amount");
|
|
33881
|
-
}
|
|
33882
|
-
if (period !== undefined) {
|
|
33883
|
-
if (!KNOWN_PERIODS.includes(period)) {
|
|
33884
|
-
throw new Error(`Invalid period: ${period}. Must be one of: ${KNOWN_PERIODS.join(", ")}`);
|
|
33885
|
-
}
|
|
33886
|
-
updateFields.period = period;
|
|
33887
|
-
updatedFieldNames.push("period");
|
|
33888
|
-
}
|
|
33889
|
-
if (name !== undefined) {
|
|
33890
|
-
const trimmedName = name.trim();
|
|
33891
|
-
if (!trimmedName) {
|
|
33892
|
-
throw new Error("Budget name must not be empty");
|
|
33893
|
-
}
|
|
33894
|
-
updateFields.name = trimmedName;
|
|
33895
|
-
updatedFieldNames.push("name");
|
|
33896
|
-
}
|
|
33897
|
-
if (is_active !== undefined) {
|
|
33898
|
-
updateFields.is_active = is_active;
|
|
33899
|
-
updatedFieldNames.push("is_active");
|
|
33900
|
-
}
|
|
33901
|
-
if (updatedFieldNames.length === 0) {
|
|
33902
|
-
throw new Error("No fields to update. Provide at least one of: amount, period, name, is_active");
|
|
33903
|
-
}
|
|
33904
|
-
const userId = await client.requireUserId();
|
|
33905
|
-
const collectionPath = `users/${userId}/budgets`;
|
|
33906
|
-
const firestoreFields = toFirestoreFields(updateFields);
|
|
33907
|
-
await client.updateDocument(collectionPath, budget_id, firestoreFields, updatedFieldNames);
|
|
33908
|
-
this.db.clearCache();
|
|
33909
|
-
return {
|
|
33910
|
-
success: true,
|
|
33911
|
-
budget_id,
|
|
33912
|
-
updated_fields: updatedFieldNames
|
|
33913
|
-
};
|
|
33914
|
-
}
|
|
33915
|
-
async deleteBudget(args) {
|
|
33916
|
-
const client = this.getFirestoreClient();
|
|
33917
|
-
const { budget_id } = args;
|
|
33918
|
-
validateDocId(budget_id, "budget_id");
|
|
33919
|
-
const existingBudgets = await this.db.getBudgets();
|
|
33920
|
-
const budget = existingBudgets.find((b) => b.budget_id === budget_id);
|
|
33921
|
-
if (!budget) {
|
|
33922
|
-
throw new Error(`Budget not found: ${budget_id}`);
|
|
33923
|
-
}
|
|
33924
|
-
const userId = await client.requireUserId();
|
|
33925
|
-
const collectionPath = `users/${userId}/budgets`;
|
|
33926
|
-
await client.deleteDocument(collectionPath, budget_id);
|
|
33927
|
-
this.db.clearCache();
|
|
33928
|
-
return {
|
|
33929
|
-
success: true,
|
|
33930
|
-
budget_id,
|
|
33931
|
-
deleted_name: budget.name ?? budget.category_id ?? budget_id
|
|
33932
|
-
};
|
|
33933
|
-
}
|
|
33934
|
-
async setRecurringState(args) {
|
|
33935
|
-
const client = this.getFirestoreClient();
|
|
33936
|
-
const { recurring_id, state } = args;
|
|
33937
|
-
validateDocId(recurring_id, "recurring_id");
|
|
33938
|
-
if (!RECURRING_STATES.includes(state)) {
|
|
33939
|
-
throw new Error(`Invalid state: ${state}. Must be one of: ${RECURRING_STATES.join(", ")}`);
|
|
33940
|
-
}
|
|
33941
|
-
const allRecurring = await this.db.getRecurring(false);
|
|
33942
|
-
const recurring = allRecurring.find((r) => r.recurring_id === recurring_id);
|
|
33943
|
-
if (!recurring) {
|
|
33944
|
-
throw new Error(`Recurring item not found: ${recurring_id}`);
|
|
33945
|
-
}
|
|
33946
|
-
const displayName = getRecurringDisplayName(recurring);
|
|
33947
|
-
const oldState = recurring.state ?? (recurring.is_active ? "active" : "paused");
|
|
33948
|
-
const userId = await client.requireUserId();
|
|
33949
|
-
const is_active = state === "active";
|
|
33950
|
-
const firestoreFields = toFirestoreFields({ state, is_active });
|
|
33951
|
-
const collectionPath = `users/${userId}/recurring`;
|
|
33952
|
-
await client.updateDocument(collectionPath, recurring_id, firestoreFields, [
|
|
33953
|
-
"state",
|
|
33954
|
-
"is_active"
|
|
33955
|
-
]);
|
|
33956
|
-
this.db.clearCache();
|
|
33957
|
-
return {
|
|
33958
|
-
success: true,
|
|
33959
|
-
recurring_id,
|
|
33960
|
-
name: displayName,
|
|
33961
|
-
old_state: oldState,
|
|
33962
|
-
new_state: state
|
|
33963
|
-
};
|
|
33964
|
-
}
|
|
33965
|
-
async deleteRecurring(args) {
|
|
33966
|
-
const client = this.getFirestoreClient();
|
|
33967
|
-
const { recurring_id } = args;
|
|
33968
|
-
validateDocId(recurring_id, "recurring_id");
|
|
33969
|
-
const allRecurring = await this.db.getRecurring(false);
|
|
33970
|
-
const recurring = allRecurring.find((r) => r.recurring_id === recurring_id);
|
|
33971
|
-
if (!recurring) {
|
|
33972
|
-
throw new Error(`Recurring item not found: ${recurring_id}`);
|
|
33973
|
-
}
|
|
33974
|
-
const displayName = getRecurringDisplayName(recurring);
|
|
33975
|
-
const userId = await client.requireUserId();
|
|
33976
|
-
const collectionPath = `users/${userId}/recurring`;
|
|
33977
|
-
await client.deleteDocument(collectionPath, recurring_id);
|
|
33978
|
-
this.db.clearCache();
|
|
33979
|
-
return {
|
|
33980
|
-
success: true,
|
|
33981
|
-
recurring_id,
|
|
33982
|
-
deleted_name: displayName
|
|
33983
|
-
};
|
|
33984
|
-
}
|
|
33985
|
-
async updateGoal(args) {
|
|
33986
|
-
const client = this.getFirestoreClient();
|
|
33987
|
-
const { goal_id, name, emoji: emoji3, target_amount, monthly_contribution, status } = args;
|
|
33988
|
-
validateDocId(goal_id, "goal_id");
|
|
33989
|
-
const goals = await this.db.getGoals(false);
|
|
33990
|
-
const goal = goals.find((g) => g.goal_id === goal_id);
|
|
33991
|
-
if (!goal) {
|
|
33992
|
-
throw new Error(`Goal not found: ${goal_id}`);
|
|
33993
|
-
}
|
|
33994
|
-
const fieldsToUpdate = {};
|
|
33995
|
-
const updateMask = [];
|
|
33996
|
-
if (name !== undefined) {
|
|
33997
|
-
if (!name.trim()) {
|
|
33998
|
-
throw new Error("Goal name must not be empty");
|
|
33999
|
-
}
|
|
34000
|
-
fieldsToUpdate.name = name.trim();
|
|
34001
|
-
updateMask.push("name");
|
|
34293
|
+
const client = this.getGraphQLClient();
|
|
34294
|
+
try {
|
|
34295
|
+
const result = await deleteCategory(client, { id: args.category_id });
|
|
34296
|
+
return { success: true, category_id: result.id, deleted: true };
|
|
34297
|
+
} catch (e) {
|
|
34298
|
+
if (e instanceof GraphQLError)
|
|
34299
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34300
|
+
throw e;
|
|
34002
34301
|
}
|
|
34003
|
-
|
|
34004
|
-
|
|
34005
|
-
|
|
34302
|
+
}
|
|
34303
|
+
async setBudget(args) {
|
|
34304
|
+
const client = this.getGraphQLClient();
|
|
34305
|
+
if (!args.category_id?.trim())
|
|
34306
|
+
throw new Error("category_id is required");
|
|
34307
|
+
if (typeof args.amount !== "string") {
|
|
34308
|
+
throw new Error('amount must be a string (e.g. "250.00")');
|
|
34006
34309
|
}
|
|
34007
|
-
|
|
34008
|
-
|
|
34009
|
-
if (target_amount <= 0) {
|
|
34010
|
-
throw new Error("target_amount must be greater than 0");
|
|
34011
|
-
}
|
|
34012
|
-
savingsUpdate.target_amount = target_amount;
|
|
34310
|
+
if (!/^\d+(\.\d{1,2})?$/.test(args.amount)) {
|
|
34311
|
+
throw new Error('amount must be a non-negative decimal like "250.00" or "0" to clear the budget');
|
|
34013
34312
|
}
|
|
34014
|
-
if (
|
|
34015
|
-
|
|
34016
|
-
throw new Error("monthly_contribution must be >= 0");
|
|
34017
|
-
}
|
|
34018
|
-
savingsUpdate.tracking_type_monthly_contribution = monthly_contribution;
|
|
34313
|
+
if (args.month !== undefined && !/^\d{4}-\d{2}$/.test(args.month)) {
|
|
34314
|
+
throw new Error('month must be "YYYY-MM"');
|
|
34019
34315
|
}
|
|
34020
|
-
|
|
34021
|
-
|
|
34316
|
+
try {
|
|
34317
|
+
const result = await setBudget(client, {
|
|
34318
|
+
categoryId: args.category_id,
|
|
34319
|
+
amount: args.amount,
|
|
34320
|
+
month: args.month
|
|
34321
|
+
});
|
|
34322
|
+
return {
|
|
34323
|
+
success: true,
|
|
34324
|
+
category_id: result.categoryId,
|
|
34325
|
+
amount: result.amount,
|
|
34326
|
+
...result.month ? { month: result.month } : {},
|
|
34327
|
+
cleared: result.cleared
|
|
34328
|
+
};
|
|
34329
|
+
} catch (e) {
|
|
34330
|
+
if (e instanceof GraphQLError)
|
|
34331
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34332
|
+
throw e;
|
|
34022
34333
|
}
|
|
34023
|
-
|
|
34024
|
-
|
|
34025
|
-
|
|
34334
|
+
}
|
|
34335
|
+
async setRecurringState(args) {
|
|
34336
|
+
const client = this.getGraphQLClient();
|
|
34337
|
+
const VALID_STATES = ["ACTIVE", "PAUSED", "ARCHIVED"];
|
|
34338
|
+
if (!VALID_STATES.includes(args.state)) {
|
|
34339
|
+
throw new Error(`state must be one of: ${VALID_STATES.join(", ")}. Got: ${args.state}`);
|
|
34026
34340
|
}
|
|
34027
|
-
|
|
34028
|
-
|
|
34341
|
+
try {
|
|
34342
|
+
const result = await editRecurring(client, {
|
|
34343
|
+
id: args.recurring_id,
|
|
34344
|
+
input: { state: args.state }
|
|
34345
|
+
});
|
|
34346
|
+
return { success: true, recurring_id: result.id, state: args.state };
|
|
34347
|
+
} catch (e) {
|
|
34348
|
+
if (e instanceof GraphQLError)
|
|
34349
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34350
|
+
throw e;
|
|
34029
34351
|
}
|
|
34030
|
-
const userId = await client.requireUserId();
|
|
34031
|
-
const collectionPath = `users/${userId}/financial_goals`;
|
|
34032
|
-
const firestoreFields = toFirestoreFields(fieldsToUpdate);
|
|
34033
|
-
await client.updateDocument(collectionPath, goal_id, firestoreFields, updateMask);
|
|
34034
|
-
this.db.clearCache();
|
|
34035
|
-
return {
|
|
34036
|
-
success: true,
|
|
34037
|
-
goal_id,
|
|
34038
|
-
updated_fields: updateMask
|
|
34039
|
-
};
|
|
34040
34352
|
}
|
|
34041
|
-
async
|
|
34042
|
-
const client = this.
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34046
|
-
|
|
34047
|
-
|
|
34048
|
-
|
|
34049
|
-
|
|
34050
|
-
|
|
34051
|
-
const collectionPath = `users/${userId}/financial_goals`;
|
|
34052
|
-
await client.deleteDocument(collectionPath, goal_id);
|
|
34053
|
-
this.db.clearCache();
|
|
34054
|
-
return {
|
|
34055
|
-
success: true,
|
|
34056
|
-
goal_id,
|
|
34057
|
-
deleted_name: goal.name ?? goal_id
|
|
34058
|
-
};
|
|
34353
|
+
async deleteRecurring(args) {
|
|
34354
|
+
const client = this.getGraphQLClient();
|
|
34355
|
+
try {
|
|
34356
|
+
const result = await deleteRecurring(client, { id: args.recurring_id });
|
|
34357
|
+
return { success: true, recurring_id: result.id, deleted: true };
|
|
34358
|
+
} catch (e) {
|
|
34359
|
+
if (e instanceof GraphQLError)
|
|
34360
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34361
|
+
throw e;
|
|
34362
|
+
}
|
|
34059
34363
|
}
|
|
34060
34364
|
async updateTag(args) {
|
|
34061
|
-
const client = this.
|
|
34062
|
-
const
|
|
34063
|
-
|
|
34064
|
-
|
|
34065
|
-
|
|
34066
|
-
|
|
34067
|
-
|
|
34068
|
-
|
|
34069
|
-
|
|
34070
|
-
|
|
34071
|
-
|
|
34072
|
-
|
|
34073
|
-
|
|
34074
|
-
|
|
34075
|
-
|
|
34076
|
-
|
|
34077
|
-
|
|
34078
|
-
}
|
|
34079
|
-
if (color_name !== undefined) {
|
|
34080
|
-
fieldsToUpdate.color_name = color_name;
|
|
34081
|
-
updateMask.push("color_name");
|
|
34082
|
-
}
|
|
34083
|
-
if (hex_color !== undefined) {
|
|
34084
|
-
validateHexColor(hex_color);
|
|
34085
|
-
fieldsToUpdate.hex_color = hex_color;
|
|
34086
|
-
updateMask.push("hex_color");
|
|
34087
|
-
}
|
|
34088
|
-
if (updateMask.length === 0) {
|
|
34089
|
-
throw new Error("No fields to update. Provide at least one of: name, color_name, hex_color");
|
|
34090
|
-
}
|
|
34091
|
-
const userId = await client.requireUserId();
|
|
34092
|
-
const collectionPath = `users/${userId}/tags`;
|
|
34093
|
-
const firestoreFields = toFirestoreFields(fieldsToUpdate);
|
|
34094
|
-
await client.updateDocument(collectionPath, tag_id, firestoreFields, updateMask);
|
|
34095
|
-
this.db.clearCache();
|
|
34096
|
-
return {
|
|
34097
|
-
success: true,
|
|
34098
|
-
tag_id,
|
|
34099
|
-
updated_fields: updateMask
|
|
34100
|
-
};
|
|
34365
|
+
const client = this.getGraphQLClient();
|
|
34366
|
+
const input = {};
|
|
34367
|
+
if (args.name !== undefined)
|
|
34368
|
+
input.name = args.name;
|
|
34369
|
+
if (args.color_name !== undefined)
|
|
34370
|
+
input.colorName = args.color_name;
|
|
34371
|
+
if (Object.keys(input).length === 0) {
|
|
34372
|
+
throw new Error("update_tag requires at least one field to update");
|
|
34373
|
+
}
|
|
34374
|
+
try {
|
|
34375
|
+
const result = await editTag(client, { id: args.tag_id, input });
|
|
34376
|
+
return { success: true, tag_id: result.id, updated: Object.keys(result.changed) };
|
|
34377
|
+
} catch (e) {
|
|
34378
|
+
if (e instanceof GraphQLError)
|
|
34379
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34380
|
+
throw e;
|
|
34381
|
+
}
|
|
34101
34382
|
}
|
|
34102
34383
|
async createRecurring(args) {
|
|
34103
|
-
const client = this.
|
|
34104
|
-
const
|
|
34105
|
-
|
|
34106
|
-
|
|
34107
|
-
throw new Error("Recurring name must not be empty");
|
|
34108
|
-
}
|
|
34109
|
-
if (amount <= 0) {
|
|
34110
|
-
throw new Error("Recurring amount must be greater than 0");
|
|
34111
|
-
}
|
|
34112
|
-
if (!VALID_RECURRING_FREQUENCIES.includes(frequency)) {
|
|
34113
|
-
throw new Error(`Invalid frequency: ${frequency}. Must be one of: ${VALID_RECURRING_FREQUENCIES.join(", ")}`);
|
|
34114
|
-
}
|
|
34115
|
-
if (category_id !== undefined)
|
|
34116
|
-
validateDocId(category_id, "category_id");
|
|
34117
|
-
if (account_id !== undefined)
|
|
34118
|
-
validateDocId(account_id, "account_id");
|
|
34119
|
-
if (start_date !== undefined)
|
|
34120
|
-
validateDate(start_date, "start_date");
|
|
34121
|
-
const recurringId = crypto.randomUUID();
|
|
34122
|
-
const userId = await client.requireUserId();
|
|
34123
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
34124
|
-
const docFields = {
|
|
34125
|
-
recurring_id: recurringId,
|
|
34126
|
-
name: trimmedName,
|
|
34127
|
-
amount,
|
|
34128
|
-
frequency,
|
|
34129
|
-
is_active: true,
|
|
34130
|
-
state: "active",
|
|
34131
|
-
latest_date: start_date ?? today
|
|
34132
|
-
};
|
|
34133
|
-
if (category_id !== undefined)
|
|
34134
|
-
docFields.category_id = category_id;
|
|
34135
|
-
if (account_id !== undefined)
|
|
34136
|
-
docFields.account_id = account_id;
|
|
34137
|
-
if (merchant_name !== undefined)
|
|
34138
|
-
docFields.merchant_name = merchant_name;
|
|
34139
|
-
if (start_date !== undefined)
|
|
34140
|
-
docFields.start_date = start_date;
|
|
34141
|
-
const collectionPath = `users/${userId}/recurring`;
|
|
34142
|
-
const firestoreFields = toFirestoreFields(docFields);
|
|
34143
|
-
await client.createDocument(collectionPath, recurringId, firestoreFields);
|
|
34144
|
-
this.db.clearCache();
|
|
34145
|
-
return {
|
|
34146
|
-
success: true,
|
|
34147
|
-
recurring_id: recurringId,
|
|
34148
|
-
name: trimmedName,
|
|
34149
|
-
amount,
|
|
34150
|
-
frequency
|
|
34151
|
-
};
|
|
34152
|
-
}
|
|
34153
|
-
async createGoal(args) {
|
|
34154
|
-
const client = this.getFirestoreClient();
|
|
34155
|
-
const { name, target_amount, emoji: emoji3, monthly_contribution, start_date } = args;
|
|
34156
|
-
const trimmedName = name.trim();
|
|
34157
|
-
if (!trimmedName) {
|
|
34158
|
-
throw new Error("Goal name must not be empty");
|
|
34384
|
+
const client = this.getGraphQLClient();
|
|
34385
|
+
const VALID_FREQUENCIES = ["WEEKLY", "BIWEEKLY", "MONTHLY", "YEARLY"];
|
|
34386
|
+
if (!VALID_FREQUENCIES.includes(args.frequency)) {
|
|
34387
|
+
throw new Error(`frequency must be one of: ${VALID_FREQUENCIES.join(", ")}. Got: ${args.frequency}`);
|
|
34159
34388
|
}
|
|
34160
|
-
|
|
34161
|
-
|
|
34389
|
+
const all = await this.db.getAllTransactions();
|
|
34390
|
+
const txn = all.find((t) => t.transaction_id === args.transaction_id);
|
|
34391
|
+
if (!txn)
|
|
34392
|
+
throw new Error(`Transaction not found: ${args.transaction_id}`);
|
|
34393
|
+
if (!txn.account_id || !txn.item_id) {
|
|
34394
|
+
throw new Error(`Transaction ${args.transaction_id} missing account_id or item_id in local cache`);
|
|
34162
34395
|
}
|
|
34163
|
-
|
|
34164
|
-
|
|
34396
|
+
try {
|
|
34397
|
+
const result = await createRecurring(client, {
|
|
34398
|
+
input: {
|
|
34399
|
+
frequency: args.frequency,
|
|
34400
|
+
transaction: {
|
|
34401
|
+
accountId: txn.account_id,
|
|
34402
|
+
itemId: txn.item_id,
|
|
34403
|
+
transactionId: args.transaction_id
|
|
34404
|
+
}
|
|
34405
|
+
}
|
|
34406
|
+
});
|
|
34407
|
+
return {
|
|
34408
|
+
success: true,
|
|
34409
|
+
recurring_id: result.id,
|
|
34410
|
+
name: result.name,
|
|
34411
|
+
state: result.state,
|
|
34412
|
+
frequency: result.frequency
|
|
34413
|
+
};
|
|
34414
|
+
} catch (e) {
|
|
34415
|
+
if (e instanceof GraphQLError)
|
|
34416
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34417
|
+
throw e;
|
|
34165
34418
|
}
|
|
34166
|
-
if (start_date !== undefined)
|
|
34167
|
-
validateDate(start_date, "start_date");
|
|
34168
|
-
const goalId = crypto.randomUUID();
|
|
34169
|
-
const userId = await client.requireUserId();
|
|
34170
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
34171
|
-
const docFields = {
|
|
34172
|
-
goal_id: goalId,
|
|
34173
|
-
name: trimmedName,
|
|
34174
|
-
savings: {
|
|
34175
|
-
type: "savings",
|
|
34176
|
-
status: "active",
|
|
34177
|
-
target_amount,
|
|
34178
|
-
tracking_type: monthly_contribution !== undefined ? "monthly_contribution" : "manual",
|
|
34179
|
-
tracking_type_monthly_contribution: monthly_contribution ?? 0,
|
|
34180
|
-
start_date: start_date ?? today,
|
|
34181
|
-
is_ongoing: false
|
|
34182
|
-
}
|
|
34183
|
-
};
|
|
34184
|
-
if (emoji3 !== undefined)
|
|
34185
|
-
docFields.emoji = emoji3;
|
|
34186
|
-
const collectionPath = `users/${userId}/financial_goals`;
|
|
34187
|
-
const firestoreFields = toFirestoreFields(docFields);
|
|
34188
|
-
await client.createDocument(collectionPath, goalId, firestoreFields);
|
|
34189
|
-
this.db.clearCache();
|
|
34190
|
-
return {
|
|
34191
|
-
success: true,
|
|
34192
|
-
goal_id: goalId,
|
|
34193
|
-
name: trimmedName,
|
|
34194
|
-
target_amount
|
|
34195
|
-
};
|
|
34196
34419
|
}
|
|
34197
34420
|
async updateRecurring(args) {
|
|
34198
|
-
const client = this.
|
|
34199
|
-
const {
|
|
34200
|
-
|
|
34201
|
-
|
|
34202
|
-
|
|
34203
|
-
|
|
34204
|
-
|
|
34205
|
-
|
|
34206
|
-
|
|
34207
|
-
|
|
34208
|
-
|
|
34209
|
-
|
|
34210
|
-
|
|
34211
|
-
|
|
34212
|
-
|
|
34213
|
-
}
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
34218
|
-
|
|
34219
|
-
|
|
34220
|
-
|
|
34221
|
-
|
|
34222
|
-
|
|
34223
|
-
|
|
34224
|
-
|
|
34225
|
-
}
|
|
34226
|
-
fieldsToUpdate.name = name.trim();
|
|
34227
|
-
updateMask.push("name");
|
|
34228
|
-
}
|
|
34229
|
-
if (amount !== undefined) {
|
|
34230
|
-
if (amount <= 0) {
|
|
34231
|
-
throw new Error("amount must be greater than 0");
|
|
34232
|
-
}
|
|
34233
|
-
fieldsToUpdate.amount = amount;
|
|
34234
|
-
updateMask.push("amount");
|
|
34235
|
-
}
|
|
34236
|
-
if (frequency !== undefined) {
|
|
34237
|
-
if (!VALID_RECURRING_FREQUENCIES.includes(frequency)) {
|
|
34238
|
-
throw new Error(`Invalid frequency: ${frequency}. Must be one of: ${VALID_RECURRING_FREQUENCIES.join(", ")}`);
|
|
34239
|
-
}
|
|
34240
|
-
fieldsToUpdate.frequency = frequency;
|
|
34241
|
-
updateMask.push("frequency");
|
|
34242
|
-
}
|
|
34243
|
-
if (category_id !== undefined) {
|
|
34244
|
-
validateDocId(category_id, "category_id");
|
|
34245
|
-
fieldsToUpdate.category_id = category_id;
|
|
34246
|
-
updateMask.push("category_id");
|
|
34247
|
-
}
|
|
34248
|
-
if (account_id !== undefined) {
|
|
34249
|
-
validateDocId(account_id, "account_id");
|
|
34250
|
-
fieldsToUpdate.account_id = account_id;
|
|
34251
|
-
updateMask.push("account_id");
|
|
34252
|
-
}
|
|
34253
|
-
if (merchant_name !== undefined) {
|
|
34254
|
-
fieldsToUpdate.merchant_name = merchant_name;
|
|
34255
|
-
updateMask.push("merchant_name");
|
|
34256
|
-
}
|
|
34257
|
-
if (emoji3 !== undefined) {
|
|
34258
|
-
fieldsToUpdate.emoji = emoji3;
|
|
34259
|
-
updateMask.push("emoji");
|
|
34260
|
-
}
|
|
34261
|
-
if (match_string !== undefined) {
|
|
34262
|
-
if (!match_string.trim()) {
|
|
34263
|
-
throw new Error("match_string must not be empty");
|
|
34264
|
-
}
|
|
34265
|
-
fieldsToUpdate.match_string = match_string.trim();
|
|
34266
|
-
updateMask.push("match_string");
|
|
34267
|
-
}
|
|
34268
|
-
if (transaction_ids !== undefined) {
|
|
34269
|
-
fieldsToUpdate.transaction_ids = transaction_ids;
|
|
34270
|
-
updateMask.push("transaction_ids");
|
|
34271
|
-
}
|
|
34272
|
-
if (excluded_transaction_ids !== undefined) {
|
|
34273
|
-
fieldsToUpdate.excluded_transaction_ids = excluded_transaction_ids;
|
|
34274
|
-
updateMask.push("excluded_transaction_ids");
|
|
34275
|
-
}
|
|
34276
|
-
if (included_transaction_ids !== undefined) {
|
|
34277
|
-
fieldsToUpdate.included_transaction_ids = included_transaction_ids;
|
|
34278
|
-
updateMask.push("included_transaction_ids");
|
|
34279
|
-
}
|
|
34280
|
-
if (days_filter !== undefined) {
|
|
34281
|
-
fieldsToUpdate.days_filter = days_filter;
|
|
34282
|
-
updateMask.push("days_filter");
|
|
34283
|
-
}
|
|
34284
|
-
if (updateMask.length === 0) {
|
|
34285
|
-
throw new Error("No fields to update");
|
|
34286
|
-
}
|
|
34287
|
-
const userId = await client.requireUserId();
|
|
34288
|
-
const collectionPath = `users/${userId}/recurring`;
|
|
34289
|
-
const firestoreFields = toFirestoreFields(fieldsToUpdate);
|
|
34290
|
-
await client.updateDocument(collectionPath, recurring_id, firestoreFields, updateMask);
|
|
34291
|
-
this.db.clearCache();
|
|
34292
|
-
const displayName = name?.trim() ?? recurring.name ?? recurring.merchant_name ?? recurring_id;
|
|
34293
|
-
return {
|
|
34294
|
-
success: true,
|
|
34295
|
-
recurring_id,
|
|
34296
|
-
name: displayName,
|
|
34297
|
-
updated_fields: updateMask
|
|
34298
|
-
};
|
|
34421
|
+
const client = this.getGraphQLClient();
|
|
34422
|
+
const input = {};
|
|
34423
|
+
if (args.state !== undefined)
|
|
34424
|
+
input.state = args.state;
|
|
34425
|
+
if (args.rule !== undefined) {
|
|
34426
|
+
const rule = {};
|
|
34427
|
+
if (args.rule.name_contains !== undefined)
|
|
34428
|
+
rule.nameContains = args.rule.name_contains;
|
|
34429
|
+
if (args.rule.min_amount !== undefined)
|
|
34430
|
+
rule.minAmount = args.rule.min_amount;
|
|
34431
|
+
if (args.rule.max_amount !== undefined)
|
|
34432
|
+
rule.maxAmount = args.rule.max_amount;
|
|
34433
|
+
if (args.rule.days !== undefined)
|
|
34434
|
+
rule.days = args.rule.days;
|
|
34435
|
+
input.rule = rule;
|
|
34436
|
+
}
|
|
34437
|
+
if (Object.keys(input).length === 0) {
|
|
34438
|
+
throw new Error("update_recurring requires at least one field to update");
|
|
34439
|
+
}
|
|
34440
|
+
try {
|
|
34441
|
+
const result = await editRecurring(client, { id: args.recurring_id, input });
|
|
34442
|
+
return { success: true, recurring_id: result.id, updated: Object.keys(result.changed) };
|
|
34443
|
+
} catch (e) {
|
|
34444
|
+
if (e instanceof GraphQLError)
|
|
34445
|
+
throw new Error(graphQLErrorToMcpError(e), { cause: e });
|
|
34446
|
+
throw e;
|
|
34447
|
+
}
|
|
34299
34448
|
}
|
|
34300
34449
|
async getBalanceHistory(options) {
|
|
34301
34450
|
const { account_id, start_date, end_date, granularity } = options;
|
|
@@ -34746,7 +34895,7 @@ function createToolSchemas() {
|
|
|
34746
34895
|
},
|
|
34747
34896
|
{
|
|
34748
34897
|
name: "get_budgets",
|
|
34749
|
-
description: "Get budgets from Copilot's native budget tracking. " + "Retrieves user-defined spending limits and budget rules stored in the app. " + "Returns budget details including amounts, periods (monthly/yearly/weekly), " + "category associations, and active status. Calculates total budgeted amount as monthly equivalent.",
|
|
34898
|
+
description: "Get budgets from Copilot's native budget tracking. " + "Retrieves user-defined spending limits and budget rules stored in the app. " + "Returns budget details including amounts, periods (monthly/yearly/weekly), " + "category associations, and active status. Calculates total budgeted amount as monthly equivalent. " + "Sync note: after `set_budget` writes, budget changes can take significantly longer " + "to reflect in this read than other collections (transactions/tags/categories/recurrings " + "sync in seconds; budgets may take minutes). Do not assume a fresh `set_budget` is " + "immediately observable here — poll with `refresh_database` or verify directly in the " + "Copilot app. Per-month overrides written via `set_budget(month=...)` are not surfaced " + "in this view; only the all-months default `amount` is returned.",
|
|
34750
34899
|
inputSchema: {
|
|
34751
34900
|
type: "object",
|
|
34752
34901
|
properties: {
|
|
@@ -34905,7 +35054,7 @@ function createToolSchemas() {
|
|
|
34905
35054
|
},
|
|
34906
35055
|
{
|
|
34907
35056
|
name: "get_investment_performance",
|
|
34908
|
-
description: "Get per-security investment performance data. Returns structured performance records " + "from
|
|
35057
|
+
description: "Get per-security investment performance data. Returns structured performance records " + "from the local LevelDB cache, enriched with ticker symbol and name from the securities collection. " + "Filter by ticker symbol or security ID.",
|
|
34909
35058
|
inputSchema: {
|
|
34910
35059
|
type: "object",
|
|
34911
35060
|
properties: {
|
|
@@ -35033,7 +35182,7 @@ function createWriteToolSchemas() {
|
|
|
35033
35182
|
return [
|
|
35034
35183
|
{
|
|
35035
35184
|
name: "update_transaction",
|
|
35036
|
-
description: "Update
|
|
35185
|
+
description: "Update a single transaction's category, note, or tags. Pass transaction_id plus " + "any combination of category_id, note, or tag_ids — only specified fields are changed. " + 'Pass note="" to clear the note. Pass tag_ids=[] to clear all tags. At least one mutable ' + "field must be provided besides transaction_id. Other fields (name, excluded, " + "internal_transfer, goal_id) are not writable through the GraphQL API and were removed " + "from this tool when the backend was migrated.",
|
|
35037
35186
|
inputSchema: {
|
|
35038
35187
|
type: "object",
|
|
35039
35188
|
additionalProperties: false,
|
|
@@ -35054,22 +35203,6 @@ function createWriteToolSchemas() {
|
|
|
35054
35203
|
type: "array",
|
|
35055
35204
|
items: { type: "string" },
|
|
35056
35205
|
description: "Tag IDs to set. Pass empty array to clear all tags."
|
|
35057
|
-
},
|
|
35058
|
-
excluded: {
|
|
35059
|
-
type: "boolean",
|
|
35060
|
-
description: "Whether the transaction is excluded from spending reports."
|
|
35061
|
-
},
|
|
35062
|
-
name: {
|
|
35063
|
-
type: "string",
|
|
35064
|
-
description: "Display name (will be trimmed; must be non-empty if present)."
|
|
35065
|
-
},
|
|
35066
|
-
internal_transfer: {
|
|
35067
|
-
type: "boolean",
|
|
35068
|
-
description: "Whether the transaction is an internal transfer."
|
|
35069
|
-
},
|
|
35070
|
-
goal_id: {
|
|
35071
|
-
type: ["string", "null"],
|
|
35072
|
-
description: "Financial goal ID to link to. Pass null to unlink the existing goal."
|
|
35073
35206
|
}
|
|
35074
35207
|
},
|
|
35075
35208
|
required: ["transaction_id"]
|
|
@@ -35082,7 +35215,7 @@ function createWriteToolSchemas() {
|
|
|
35082
35215
|
},
|
|
35083
35216
|
{
|
|
35084
35217
|
name: "review_transactions",
|
|
35085
|
-
description: "Mark one or more transactions as reviewed (or unreviewed). " + "Accepts an array of transaction_ids. Writes directly to Copilot Money via
|
|
35218
|
+
description: "Mark one or more transactions as reviewed (or unreviewed). " + "Accepts an array of transaction_ids. Writes directly to Copilot Money via GraphQL.",
|
|
35086
35219
|
inputSchema: {
|
|
35087
35220
|
type: "object",
|
|
35088
35221
|
properties: {
|
|
@@ -35106,7 +35239,7 @@ function createWriteToolSchemas() {
|
|
|
35106
35239
|
},
|
|
35107
35240
|
{
|
|
35108
35241
|
name: "create_tag",
|
|
35109
|
-
description: "Create a new user-defined tag for categorizing transactions. Tags appear in the " + "Copilot Money app and are stored in the tag_ids field on transactions. " + "Optionally set a color. Writes directly to Copilot Money via
|
|
35242
|
+
description: "Create a new user-defined tag for categorizing transactions. Tags appear in the " + "Copilot Money app and are stored in the tag_ids field on transactions. " + "Optionally set a color. Writes directly to Copilot Money via GraphQL.",
|
|
35110
35243
|
inputSchema: {
|
|
35111
35244
|
type: "object",
|
|
35112
35245
|
properties: {
|
|
@@ -35133,7 +35266,7 @@ function createWriteToolSchemas() {
|
|
|
35133
35266
|
},
|
|
35134
35267
|
{
|
|
35135
35268
|
name: "delete_tag",
|
|
35136
|
-
description: "Delete a user-defined tag. The tag_id can be obtained from the tag definitions " + "in the local cache. Writes directly to Copilot Money via
|
|
35269
|
+
description: "Delete a user-defined tag. The tag_id can be obtained from the tag definitions " + "in the local cache. Writes directly to Copilot Money via GraphQL.",
|
|
35137
35270
|
inputSchema: {
|
|
35138
35271
|
type: "object",
|
|
35139
35272
|
properties: {
|
|
@@ -35152,7 +35285,7 @@ function createWriteToolSchemas() {
|
|
|
35152
35285
|
},
|
|
35153
35286
|
{
|
|
35154
35287
|
name: "create_category",
|
|
35155
|
-
description: "Create a new custom category in Copilot Money. Provide
|
|
35288
|
+
description: "Create a new custom category in Copilot Money. Provide name, color_name, " + "and emoji (all required). Optionally set is_excluded. Returns the generated " + "category_id. The new category can then be used with update_transaction. " + "Note: parent/child category hierarchies are not writable through Copilot's " + "GraphQL API — create flat categories only. Writes directly to Copilot Money via GraphQL.",
|
|
35156
35289
|
inputSchema: {
|
|
35157
35290
|
type: "object",
|
|
35158
35291
|
properties: {
|
|
@@ -35160,25 +35293,22 @@ function createWriteToolSchemas() {
|
|
|
35160
35293
|
type: "string",
|
|
35161
35294
|
description: 'Display name for the new category (e.g., "Subscriptions")'
|
|
35162
35295
|
},
|
|
35163
|
-
|
|
35164
|
-
type: "string",
|
|
35165
|
-
description: 'Emoji icon for the category (e.g., "\uD83C\uDFAC")'
|
|
35166
|
-
},
|
|
35167
|
-
color: {
|
|
35296
|
+
color_name: {
|
|
35168
35297
|
type: "string",
|
|
35169
|
-
description: '
|
|
35298
|
+
description: 'Named color from the Copilot palette (e.g., "RED1", "OLIVE1", "PURPLE2"). ' + "See existing categories via get_categories for valid values."
|
|
35170
35299
|
},
|
|
35171
|
-
|
|
35300
|
+
emoji: {
|
|
35172
35301
|
type: "string",
|
|
35173
|
-
description:
|
|
35302
|
+
description: 'Emoji icon for the category (e.g., "\uD83C\uDFAC")'
|
|
35174
35303
|
},
|
|
35175
|
-
|
|
35304
|
+
is_excluded: {
|
|
35176
35305
|
type: "boolean",
|
|
35177
35306
|
description: "Exclude this category from spending totals (default: false)",
|
|
35178
35307
|
default: false
|
|
35179
35308
|
}
|
|
35180
35309
|
},
|
|
35181
|
-
required: ["name"]
|
|
35310
|
+
required: ["name", "color_name", "emoji"],
|
|
35311
|
+
additionalProperties: false
|
|
35182
35312
|
},
|
|
35183
35313
|
annotations: {
|
|
35184
35314
|
readOnlyHint: false,
|
|
@@ -35188,7 +35318,7 @@ function createWriteToolSchemas() {
|
|
|
35188
35318
|
},
|
|
35189
35319
|
{
|
|
35190
35320
|
name: "update_category",
|
|
35191
|
-
description: "Update an existing user-defined category. Provide category_id (required) and any " + "fields to change: name, emoji,
|
|
35321
|
+
description: "Update an existing user-defined category. Provide category_id (required) and any " + "fields to change: name, emoji, color_name, or is_excluded. Only the specified " + "fields are updated. Note: parent/child category hierarchies are not writable " + "through Copilot's GraphQL API. Writes directly to Copilot Money via GraphQL.",
|
|
35192
35322
|
inputSchema: {
|
|
35193
35323
|
type: "object",
|
|
35194
35324
|
properties: {
|
|
@@ -35204,20 +35334,17 @@ function createWriteToolSchemas() {
|
|
|
35204
35334
|
type: "string",
|
|
35205
35335
|
description: 'New emoji icon for the category (e.g., "\uD83C\uDFAC")'
|
|
35206
35336
|
},
|
|
35207
|
-
|
|
35337
|
+
color_name: {
|
|
35208
35338
|
type: "string",
|
|
35209
|
-
description: 'New
|
|
35339
|
+
description: 'New named color from the Copilot palette (e.g., "RED1", "OLIVE1", "PURPLE2").'
|
|
35210
35340
|
},
|
|
35211
|
-
|
|
35341
|
+
is_excluded: {
|
|
35212
35342
|
type: "boolean",
|
|
35213
35343
|
description: "Exclude this category from spending totals"
|
|
35214
|
-
},
|
|
35215
|
-
parent_category_id: {
|
|
35216
|
-
type: ["string", "null"],
|
|
35217
|
-
description: "Parent category ID to nest under, or null to ungroup. " + "Use get_categories to find valid parent IDs."
|
|
35218
35344
|
}
|
|
35219
35345
|
},
|
|
35220
|
-
required: ["category_id"]
|
|
35346
|
+
required: ["category_id"],
|
|
35347
|
+
additionalProperties: false
|
|
35221
35348
|
},
|
|
35222
35349
|
annotations: {
|
|
35223
35350
|
readOnlyHint: false,
|
|
@@ -35227,7 +35354,7 @@ function createWriteToolSchemas() {
|
|
|
35227
35354
|
},
|
|
35228
35355
|
{
|
|
35229
35356
|
name: "delete_category",
|
|
35230
|
-
description: "Delete a user-defined category. The category_id can be obtained from get_categories. " + "Writes directly to Copilot Money via
|
|
35357
|
+
description: "Delete a user-defined category. The category_id can be obtained from get_categories. " + "Writes directly to Copilot Money via GraphQL.",
|
|
35231
35358
|
inputSchema: {
|
|
35232
35359
|
type: "object",
|
|
35233
35360
|
properties: {
|
|
@@ -35245,66 +35372,26 @@ function createWriteToolSchemas() {
|
|
|
35245
35372
|
}
|
|
35246
35373
|
},
|
|
35247
35374
|
{
|
|
35248
|
-
name: "
|
|
35249
|
-
description: "
|
|
35375
|
+
name: "set_budget",
|
|
35376
|
+
description: 'Set the monthly budget amount for a category. amount="0" clears the budget. ' + 'Pass month="YYYY-MM" for a single-month override; omit for the all-months default. ' + 'Note: if the user has disabled "Enable budgeting" or "Enable rollover" in ' + "Copilot → Settings → General, the budget write still succeeds on the server, but " + "the value will not appear in the Copilot UI until those toggles are re-enabled. " + 'Rollover behavior also depends on the "Rollover categories" selection in the same ' + "settings pane, which is not writable through this tool. " + "Sync delay: successful writes may not be visible via `get_budgets` for minutes — " + "budget docs sync on a slower cadence than other collections. Do not retry the write " + "just because the read does not reflect it yet.",
|
|
35250
35377
|
inputSchema: {
|
|
35251
35378
|
type: "object",
|
|
35252
35379
|
properties: {
|
|
35253
35380
|
category_id: {
|
|
35254
35381
|
type: "string",
|
|
35255
|
-
description: "
|
|
35382
|
+
description: "ID of the category to budget."
|
|
35256
35383
|
},
|
|
35257
35384
|
amount: {
|
|
35258
|
-
type: "number",
|
|
35259
|
-
description: "Budget amount (must be greater than 0)"
|
|
35260
|
-
},
|
|
35261
|
-
period: {
|
|
35262
35385
|
type: "string",
|
|
35263
|
-
description:
|
|
35264
|
-
enum: ["monthly", "yearly", "weekly", "daily"]
|
|
35386
|
+
description: 'Decimal amount as a string (e.g. "250.00"). "0" clears the budget.'
|
|
35265
35387
|
},
|
|
35266
|
-
|
|
35388
|
+
month: {
|
|
35267
35389
|
type: "string",
|
|
35268
|
-
description: "Optional
|
|
35390
|
+
description: "Optional. YYYY-MM for a single-month override. Omit to set the all-months default."
|
|
35269
35391
|
}
|
|
35270
35392
|
},
|
|
35271
|
-
required: ["category_id", "amount"]
|
|
35272
|
-
|
|
35273
|
-
annotations: {
|
|
35274
|
-
readOnlyHint: false,
|
|
35275
|
-
destructiveHint: false,
|
|
35276
|
-
idempotentHint: false
|
|
35277
|
-
}
|
|
35278
|
-
},
|
|
35279
|
-
{
|
|
35280
|
-
name: "update_budget",
|
|
35281
|
-
description: "Update an existing budget in Copilot Money. Requires budget_id and at least one " + "field to update (amount, period, name, or is_active). Only provided fields are changed.",
|
|
35282
|
-
inputSchema: {
|
|
35283
|
-
type: "object",
|
|
35284
|
-
properties: {
|
|
35285
|
-
budget_id: {
|
|
35286
|
-
type: "string",
|
|
35287
|
-
description: "Budget ID to update (from get_budgets)"
|
|
35288
|
-
},
|
|
35289
|
-
amount: {
|
|
35290
|
-
type: "number",
|
|
35291
|
-
description: "New budget amount (must be greater than 0)"
|
|
35292
|
-
},
|
|
35293
|
-
period: {
|
|
35294
|
-
type: "string",
|
|
35295
|
-
description: "New budget period: monthly, yearly, weekly, or daily",
|
|
35296
|
-
enum: ["monthly", "yearly", "weekly", "daily"]
|
|
35297
|
-
},
|
|
35298
|
-
name: {
|
|
35299
|
-
type: "string",
|
|
35300
|
-
description: "New display name for the budget"
|
|
35301
|
-
},
|
|
35302
|
-
is_active: {
|
|
35303
|
-
type: "boolean",
|
|
35304
|
-
description: "Set to false to deactivate the budget, true to reactivate"
|
|
35305
|
-
}
|
|
35306
|
-
},
|
|
35307
|
-
required: ["budget_id"]
|
|
35393
|
+
required: ["category_id", "amount"],
|
|
35394
|
+
additionalProperties: false
|
|
35308
35395
|
},
|
|
35309
35396
|
annotations: {
|
|
35310
35397
|
readOnlyHint: false,
|
|
@@ -35312,28 +35399,9 @@ function createWriteToolSchemas() {
|
|
|
35312
35399
|
idempotentHint: true
|
|
35313
35400
|
}
|
|
35314
35401
|
},
|
|
35315
|
-
{
|
|
35316
|
-
name: "delete_budget",
|
|
35317
|
-
description: "Delete a budget from Copilot Money. Requires the budget_id (from get_budgets). " + "This permanently removes the budget. Writes directly to Copilot Money via Firestore.",
|
|
35318
|
-
inputSchema: {
|
|
35319
|
-
type: "object",
|
|
35320
|
-
properties: {
|
|
35321
|
-
budget_id: {
|
|
35322
|
-
type: "string",
|
|
35323
|
-
description: "Budget ID to delete (from get_budgets)"
|
|
35324
|
-
}
|
|
35325
|
-
},
|
|
35326
|
-
required: ["budget_id"]
|
|
35327
|
-
},
|
|
35328
|
-
annotations: {
|
|
35329
|
-
readOnlyHint: false,
|
|
35330
|
-
destructiveHint: true,
|
|
35331
|
-
idempotentHint: true
|
|
35332
|
-
}
|
|
35333
|
-
},
|
|
35334
35402
|
{
|
|
35335
35403
|
name: "set_recurring_state",
|
|
35336
|
-
description: "Change the state of a recurring item (subscription/charge). " + "Set to
|
|
35404
|
+
description: "Change the state of a recurring item (subscription/charge). " + "Set to ACTIVE, PAUSED, or ARCHIVED (uppercase, matching the GraphQL API). " + "Requires recurring_id (from get_recurring_transactions). " + "Writes directly to Copilot Money via GraphQL.",
|
|
35337
35405
|
inputSchema: {
|
|
35338
35406
|
type: "object",
|
|
35339
35407
|
properties: {
|
|
@@ -35343,8 +35411,8 @@ function createWriteToolSchemas() {
|
|
|
35343
35411
|
},
|
|
35344
35412
|
state: {
|
|
35345
35413
|
type: "string",
|
|
35346
|
-
enum: ["
|
|
35347
|
-
description: "New state for the recurring item"
|
|
35414
|
+
enum: ["ACTIVE", "PAUSED", "ARCHIVED"],
|
|
35415
|
+
description: "New state for the recurring item (uppercase: ACTIVE, PAUSED, ARCHIVED)"
|
|
35348
35416
|
}
|
|
35349
35417
|
},
|
|
35350
35418
|
required: ["recurring_id", "state"]
|
|
@@ -35357,7 +35425,7 @@ function createWriteToolSchemas() {
|
|
|
35357
35425
|
},
|
|
35358
35426
|
{
|
|
35359
35427
|
name: "delete_recurring",
|
|
35360
|
-
description: "Delete a recurring item (subscription/charge). " + "Requires recurring_id (from get_recurring_transactions). " + "Writes directly to Copilot Money via
|
|
35428
|
+
description: "Delete a recurring item (subscription/charge). " + "Requires recurring_id (from get_recurring_transactions). " + "Writes directly to Copilot Money via GraphQL.",
|
|
35361
35429
|
inputSchema: {
|
|
35362
35430
|
type: "object",
|
|
35363
35431
|
properties: {
|
|
@@ -35374,68 +35442,9 @@ function createWriteToolSchemas() {
|
|
|
35374
35442
|
idempotentHint: true
|
|
35375
35443
|
}
|
|
35376
35444
|
},
|
|
35377
|
-
{
|
|
35378
|
-
name: "update_goal",
|
|
35379
|
-
description: "Update a financial goal's properties. Provide goal_id (required) and any combination " + "of name, emoji, target_amount, monthly_contribution, or status. Only the fields you " + "provide will be updated. Writes directly to Copilot Money via Firestore.",
|
|
35380
|
-
inputSchema: {
|
|
35381
|
-
type: "object",
|
|
35382
|
-
properties: {
|
|
35383
|
-
goal_id: {
|
|
35384
|
-
type: "string",
|
|
35385
|
-
description: "Goal ID to update (from get_goals results)"
|
|
35386
|
-
},
|
|
35387
|
-
name: {
|
|
35388
|
-
type: "string",
|
|
35389
|
-
description: "New display name for the goal"
|
|
35390
|
-
},
|
|
35391
|
-
emoji: {
|
|
35392
|
-
type: "string",
|
|
35393
|
-
description: "New emoji icon for the goal"
|
|
35394
|
-
},
|
|
35395
|
-
target_amount: {
|
|
35396
|
-
type: "number",
|
|
35397
|
-
description: "New target savings amount (must be > 0)"
|
|
35398
|
-
},
|
|
35399
|
-
monthly_contribution: {
|
|
35400
|
-
type: "number",
|
|
35401
|
-
description: "New monthly contribution amount (must be >= 0)"
|
|
35402
|
-
},
|
|
35403
|
-
status: {
|
|
35404
|
-
type: "string",
|
|
35405
|
-
enum: ["active", "paused"],
|
|
35406
|
-
description: "Set goal status to active or paused"
|
|
35407
|
-
}
|
|
35408
|
-
},
|
|
35409
|
-
required: ["goal_id"]
|
|
35410
|
-
},
|
|
35411
|
-
annotations: {
|
|
35412
|
-
readOnlyHint: false,
|
|
35413
|
-
destructiveHint: false,
|
|
35414
|
-
idempotentHint: true
|
|
35415
|
-
}
|
|
35416
|
-
},
|
|
35417
|
-
{
|
|
35418
|
-
name: "delete_goal",
|
|
35419
|
-
description: "Delete a financial goal. The goal_id can be obtained from get_goals results. " + "Writes directly to Copilot Money via Firestore.",
|
|
35420
|
-
inputSchema: {
|
|
35421
|
-
type: "object",
|
|
35422
|
-
properties: {
|
|
35423
|
-
goal_id: {
|
|
35424
|
-
type: "string",
|
|
35425
|
-
description: "Goal ID to delete"
|
|
35426
|
-
}
|
|
35427
|
-
},
|
|
35428
|
-
required: ["goal_id"]
|
|
35429
|
-
},
|
|
35430
|
-
annotations: {
|
|
35431
|
-
readOnlyHint: false,
|
|
35432
|
-
destructiveHint: true,
|
|
35433
|
-
idempotentHint: true
|
|
35434
|
-
}
|
|
35435
|
-
},
|
|
35436
35445
|
{
|
|
35437
35446
|
name: "update_tag",
|
|
35438
|
-
description: "Update an existing tag. Provide tag_id (required) and at least one of name, " + "color_name, or hex_color. Only the specified fields are updated. " + "Writes directly to Copilot Money via
|
|
35447
|
+
description: "Update an existing tag. Provide tag_id (required) and at least one of name, " + "color_name, or hex_color. Only the specified fields are updated. " + "Writes directly to Copilot Money via GraphQL.",
|
|
35439
35448
|
inputSchema: {
|
|
35440
35449
|
type: "object",
|
|
35441
35450
|
properties: {
|
|
@@ -35466,76 +35475,22 @@ function createWriteToolSchemas() {
|
|
|
35466
35475
|
},
|
|
35467
35476
|
{
|
|
35468
35477
|
name: "create_recurring",
|
|
35469
|
-
description: "Create a new recurring/subscription item
|
|
35478
|
+
description: "Create a new recurring/subscription item by seeding it from an existing transaction. " + "The recurring inherits its merchant name, account, and initial amount from that transaction; " + "you only supply the cadence (frequency). Writes directly to Copilot Money via GraphQL.",
|
|
35470
35479
|
inputSchema: {
|
|
35471
35480
|
type: "object",
|
|
35472
35481
|
properties: {
|
|
35473
|
-
|
|
35482
|
+
transaction_id: {
|
|
35474
35483
|
type: "string",
|
|
35475
|
-
description:
|
|
35476
|
-
},
|
|
35477
|
-
amount: {
|
|
35478
|
-
type: "number",
|
|
35479
|
-
description: "Recurring amount (must be greater than 0)"
|
|
35484
|
+
description: "ID of an existing transaction to seed the recurring from. The recurring inherits its merchant name, account, and initial amount from this transaction."
|
|
35480
35485
|
},
|
|
35481
35486
|
frequency: {
|
|
35482
35487
|
type: "string",
|
|
35483
|
-
enum: ["
|
|
35484
|
-
description: "How often the
|
|
35485
|
-
},
|
|
35486
|
-
category_id: {
|
|
35487
|
-
type: "string",
|
|
35488
|
-
description: "Category ID for this recurring item (from get_categories)"
|
|
35489
|
-
},
|
|
35490
|
-
account_id: {
|
|
35491
|
-
type: "string",
|
|
35492
|
-
description: "Account ID for this recurring item (from get_accounts)"
|
|
35493
|
-
},
|
|
35494
|
-
merchant_name: {
|
|
35495
|
-
type: "string",
|
|
35496
|
-
description: "Merchant name for the recurring charge"
|
|
35497
|
-
},
|
|
35498
|
-
start_date: {
|
|
35499
|
-
type: "string",
|
|
35500
|
-
description: "Start date in ISO format (YYYY-MM-DD). Defaults to today."
|
|
35501
|
-
}
|
|
35502
|
-
},
|
|
35503
|
-
required: ["name", "amount", "frequency"]
|
|
35504
|
-
},
|
|
35505
|
-
annotations: {
|
|
35506
|
-
readOnlyHint: false,
|
|
35507
|
-
destructiveHint: false,
|
|
35508
|
-
idempotentHint: false
|
|
35509
|
-
}
|
|
35510
|
-
},
|
|
35511
|
-
{
|
|
35512
|
-
name: "create_goal",
|
|
35513
|
-
description: "Create a new financial goal. Requires a name and target amount. " + "Optionally set an emoji, monthly contribution, or start date. " + "Writes directly to Copilot Money via Firestore.",
|
|
35514
|
-
inputSchema: {
|
|
35515
|
-
type: "object",
|
|
35516
|
-
properties: {
|
|
35517
|
-
name: {
|
|
35518
|
-
type: "string",
|
|
35519
|
-
description: 'Name of the financial goal (e.g. "Emergency Fund", "Vacation")'
|
|
35520
|
-
},
|
|
35521
|
-
target_amount: {
|
|
35522
|
-
type: "number",
|
|
35523
|
-
description: "Target savings amount (must be greater than 0)"
|
|
35524
|
-
},
|
|
35525
|
-
emoji: {
|
|
35526
|
-
type: "string",
|
|
35527
|
-
description: 'Emoji icon for the goal (e.g. "\uD83C\uDFD6️")'
|
|
35528
|
-
},
|
|
35529
|
-
monthly_contribution: {
|
|
35530
|
-
type: "number",
|
|
35531
|
-
description: "Monthly contribution amount (must be >= 0). " + "Sets tracking to monthly_contribution mode when provided."
|
|
35532
|
-
},
|
|
35533
|
-
start_date: {
|
|
35534
|
-
type: "string",
|
|
35535
|
-
description: "Start date in ISO format (YYYY-MM-DD). Defaults to today."
|
|
35488
|
+
enum: ["WEEKLY", "BIWEEKLY", "MONTHLY", "YEARLY"],
|
|
35489
|
+
description: "How often the recurring payment occurs."
|
|
35536
35490
|
}
|
|
35537
35491
|
},
|
|
35538
|
-
required: ["
|
|
35492
|
+
required: ["transaction_id", "frequency"],
|
|
35493
|
+
additionalProperties: false
|
|
35539
35494
|
},
|
|
35540
35495
|
annotations: {
|
|
35541
35496
|
readOnlyHint: false,
|
|
@@ -35545,68 +35500,46 @@ function createWriteToolSchemas() {
|
|
|
35545
35500
|
},
|
|
35546
35501
|
{
|
|
35547
35502
|
name: "update_recurring",
|
|
35548
|
-
description: "Update an existing recurring
|
|
35503
|
+
description: "Update an existing recurring transaction. Pass recurring_id plus any combination of " + "state or rule (name_contains, min_amount, max_amount, days). The recurring cannot be " + "renamed or re-linked to a different transaction through this tool — those fields must " + "be changed in the Copilot Money web app. Writes directly to Copilot Money via GraphQL.",
|
|
35549
35504
|
inputSchema: {
|
|
35550
35505
|
type: "object",
|
|
35551
35506
|
properties: {
|
|
35552
35507
|
recurring_id: {
|
|
35553
35508
|
type: "string",
|
|
35554
|
-
description: "ID of the recurring
|
|
35555
|
-
},
|
|
35556
|
-
name: {
|
|
35557
|
-
type: "string",
|
|
35558
|
-
description: "New display name for the recurring charge"
|
|
35559
|
-
},
|
|
35560
|
-
amount: {
|
|
35561
|
-
type: "number",
|
|
35562
|
-
description: "Expected recurring amount (must be > 0)"
|
|
35563
|
-
},
|
|
35564
|
-
frequency: {
|
|
35565
|
-
type: "string",
|
|
35566
|
-
enum: ["weekly", "biweekly", "monthly", "yearly"],
|
|
35567
|
-
description: "How often this charge recurs"
|
|
35568
|
-
},
|
|
35569
|
-
category_id: {
|
|
35570
|
-
type: "string",
|
|
35571
|
-
description: "Category ID to assign (from get_categories)"
|
|
35572
|
-
},
|
|
35573
|
-
account_id: {
|
|
35574
|
-
type: "string",
|
|
35575
|
-
description: "Account ID this recurring charge is associated with"
|
|
35576
|
-
},
|
|
35577
|
-
merchant_name: {
|
|
35578
|
-
type: "string",
|
|
35579
|
-
description: "Merchant name for the recurring charge"
|
|
35580
|
-
},
|
|
35581
|
-
emoji: {
|
|
35582
|
-
type: "string",
|
|
35583
|
-
description: "Emoji icon for the recurring item"
|
|
35509
|
+
description: "ID of the recurring to update."
|
|
35584
35510
|
},
|
|
35585
|
-
|
|
35511
|
+
state: {
|
|
35586
35512
|
type: "string",
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
transaction_ids: {
|
|
35590
|
-
type: "array",
|
|
35591
|
-
items: { type: "string" },
|
|
35592
|
-
description: "Transaction IDs that belong to this recurring item"
|
|
35593
|
-
},
|
|
35594
|
-
excluded_transaction_ids: {
|
|
35595
|
-
type: "array",
|
|
35596
|
-
items: { type: "string" },
|
|
35597
|
-
description: "Transaction IDs explicitly excluded from this recurring item"
|
|
35598
|
-
},
|
|
35599
|
-
included_transaction_ids: {
|
|
35600
|
-
type: "array",
|
|
35601
|
-
items: { type: "string" },
|
|
35602
|
-
description: "Transaction IDs explicitly included in this recurring item"
|
|
35513
|
+
enum: ["ACTIVE", "PAUSED", "ARCHIVED"],
|
|
35514
|
+
description: "State of the recurring. Use set_recurring_state instead if you only want to " + "change state — this tool is for broader edits."
|
|
35603
35515
|
},
|
|
35604
|
-
|
|
35605
|
-
type: "
|
|
35606
|
-
description: "
|
|
35516
|
+
rule: {
|
|
35517
|
+
type: "object",
|
|
35518
|
+
description: "Matching rule. Controls how Copilot auto-detects future payments.",
|
|
35519
|
+
properties: {
|
|
35520
|
+
name_contains: {
|
|
35521
|
+
type: "string",
|
|
35522
|
+
description: "Substring that must appear in the merchant/payee name."
|
|
35523
|
+
},
|
|
35524
|
+
min_amount: {
|
|
35525
|
+
type: "string",
|
|
35526
|
+
description: "Minimum amount (as a decimal string) for a transaction to match."
|
|
35527
|
+
},
|
|
35528
|
+
max_amount: {
|
|
35529
|
+
type: "string",
|
|
35530
|
+
description: "Maximum amount (as a decimal string) for a transaction to match."
|
|
35531
|
+
},
|
|
35532
|
+
days: {
|
|
35533
|
+
type: "array",
|
|
35534
|
+
items: { type: "number" },
|
|
35535
|
+
description: "Days of the month (1-31) when this recurring is expected."
|
|
35536
|
+
}
|
|
35537
|
+
},
|
|
35538
|
+
additionalProperties: false
|
|
35607
35539
|
}
|
|
35608
35540
|
},
|
|
35609
|
-
required: ["recurring_id"]
|
|
35541
|
+
required: ["recurring_id"],
|
|
35542
|
+
additionalProperties: false
|
|
35610
35543
|
},
|
|
35611
35544
|
annotations: {
|
|
35612
35545
|
readOnlyHint: false,
|
|
@@ -35616,100 +35549,6 @@ function createWriteToolSchemas() {
|
|
|
35616
35549
|
}
|
|
35617
35550
|
];
|
|
35618
35551
|
}
|
|
35619
|
-
// src/core/firestore-client.ts
|
|
35620
|
-
var FIRESTORE_PROJECT_ID = "copilot-production-22904";
|
|
35621
|
-
var FIRESTORE_BASE_URL = "https://firestore.googleapis.com/v1";
|
|
35622
|
-
|
|
35623
|
-
class FirestoreClient {
|
|
35624
|
-
auth;
|
|
35625
|
-
constructor(auth) {
|
|
35626
|
-
this.auth = auth;
|
|
35627
|
-
}
|
|
35628
|
-
getUserId() {
|
|
35629
|
-
return this.auth.getUserId();
|
|
35630
|
-
}
|
|
35631
|
-
async requireUserId() {
|
|
35632
|
-
await this.auth.getIdToken();
|
|
35633
|
-
const uid = this.auth.getUserId();
|
|
35634
|
-
if (!uid) {
|
|
35635
|
-
throw new Error("Firebase user ID unavailable after authentication");
|
|
35636
|
-
}
|
|
35637
|
-
return uid;
|
|
35638
|
-
}
|
|
35639
|
-
async updateDocument(collectionPath, documentId, fields, updateMask) {
|
|
35640
|
-
const idToken = await this.auth.getIdToken();
|
|
35641
|
-
const docPath = `projects/${FIRESTORE_PROJECT_ID}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
35642
|
-
const url2 = new URL(`${FIRESTORE_BASE_URL}/${docPath}`);
|
|
35643
|
-
for (const field of updateMask) {
|
|
35644
|
-
url2.searchParams.append("updateMask.fieldPaths", field);
|
|
35645
|
-
}
|
|
35646
|
-
const response = await fetch(url2.toString(), {
|
|
35647
|
-
method: "PATCH",
|
|
35648
|
-
headers: {
|
|
35649
|
-
Authorization: `Bearer ${idToken}`,
|
|
35650
|
-
"Content-Type": "application/json"
|
|
35651
|
-
},
|
|
35652
|
-
body: JSON.stringify({ fields })
|
|
35653
|
-
});
|
|
35654
|
-
if (!response.ok) {
|
|
35655
|
-
const errorBody = await response.text();
|
|
35656
|
-
throw new Error(`Firestore update failed (${response.status}): ${errorBody}`);
|
|
35657
|
-
}
|
|
35658
|
-
}
|
|
35659
|
-
async getDocument(collectionPath, documentId) {
|
|
35660
|
-
const idToken = await this.auth.getIdToken();
|
|
35661
|
-
const docPath = `projects/${FIRESTORE_PROJECT_ID}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
35662
|
-
const response = await fetch(`${FIRESTORE_BASE_URL}/${docPath}`, {
|
|
35663
|
-
method: "GET",
|
|
35664
|
-
headers: {
|
|
35665
|
-
Authorization: `Bearer ${idToken}`
|
|
35666
|
-
}
|
|
35667
|
-
});
|
|
35668
|
-
if (!response.ok) {
|
|
35669
|
-
const errorBody = await response.text();
|
|
35670
|
-
throw new Error(`Firestore get failed (${response.status}): ${errorBody}`);
|
|
35671
|
-
}
|
|
35672
|
-
const doc2 = await response.json();
|
|
35673
|
-
return doc2.fields ?? {};
|
|
35674
|
-
}
|
|
35675
|
-
async createDocument(collectionPath, documentId, fields) {
|
|
35676
|
-
const idToken = await this.auth.getIdToken();
|
|
35677
|
-
const parentPath = `projects/${FIRESTORE_PROJECT_ID}/databases/(default)/documents`;
|
|
35678
|
-
const url2 = new URL(`${FIRESTORE_BASE_URL}/${parentPath}/${collectionPath}`);
|
|
35679
|
-
if (documentId) {
|
|
35680
|
-
url2.searchParams.set("documentId", documentId);
|
|
35681
|
-
}
|
|
35682
|
-
const response = await fetch(url2.toString(), {
|
|
35683
|
-
method: "POST",
|
|
35684
|
-
headers: {
|
|
35685
|
-
Authorization: `Bearer ${idToken}`,
|
|
35686
|
-
"Content-Type": "application/json"
|
|
35687
|
-
},
|
|
35688
|
-
body: JSON.stringify({ fields })
|
|
35689
|
-
});
|
|
35690
|
-
if (!response.ok) {
|
|
35691
|
-
const errorBody = await response.text();
|
|
35692
|
-
throw new Error(`Firestore create failed (${response.status}): ${errorBody}`);
|
|
35693
|
-
}
|
|
35694
|
-
const doc2 = await response.json();
|
|
35695
|
-
return doc2.name.split("/").pop();
|
|
35696
|
-
}
|
|
35697
|
-
async deleteDocument(collectionPath, documentId) {
|
|
35698
|
-
const idToken = await this.auth.getIdToken();
|
|
35699
|
-
const docPath = `projects/${FIRESTORE_PROJECT_ID}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
35700
|
-
const response = await fetch(`${FIRESTORE_BASE_URL}/${docPath}`, {
|
|
35701
|
-
method: "DELETE",
|
|
35702
|
-
headers: {
|
|
35703
|
-
Authorization: `Bearer ${idToken}`
|
|
35704
|
-
}
|
|
35705
|
-
});
|
|
35706
|
-
if (!response.ok) {
|
|
35707
|
-
const errorBody = await response.text();
|
|
35708
|
-
throw new Error(`Firestore delete failed (${response.status}): ${errorBody}`);
|
|
35709
|
-
}
|
|
35710
|
-
}
|
|
35711
|
-
}
|
|
35712
|
-
|
|
35713
35552
|
// src/core/auth/firebase-auth.ts
|
|
35714
35553
|
var FIREBASE_API_KEY = "AIzaSyAMgjkeOSkHj4J4rlswOkD16N3WQOoNPpk";
|
|
35715
35554
|
var TOKEN_ENDPOINT = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
|
|
@@ -35918,12 +35757,12 @@ class CopilotMoneyServer {
|
|
|
35918
35757
|
constructor(dbPath, decodeTimeoutMs, writeEnabled = false) {
|
|
35919
35758
|
this.db = new CopilotDatabase(dbPath, decodeTimeoutMs);
|
|
35920
35759
|
this.writeEnabled = writeEnabled;
|
|
35921
|
-
let
|
|
35760
|
+
let graphqlClient;
|
|
35922
35761
|
if (writeEnabled) {
|
|
35923
35762
|
const auth = new FirebaseAuth(() => extractRefreshToken());
|
|
35924
|
-
|
|
35763
|
+
graphqlClient = new GraphQLClient(auth);
|
|
35925
35764
|
}
|
|
35926
|
-
this.tools = new CopilotMoneyTools(this.db,
|
|
35765
|
+
this.tools = new CopilotMoneyTools(this.db, graphqlClient);
|
|
35927
35766
|
this.server = new Server({
|
|
35928
35767
|
name: "copilot-money-mcp",
|
|
35929
35768
|
version: SERVER_VERSION
|
|
@@ -35949,21 +35788,16 @@ class CopilotMoneyServer {
|
|
|
35949
35788
|
"update_transaction",
|
|
35950
35789
|
"review_transactions",
|
|
35951
35790
|
"create_tag",
|
|
35791
|
+
"update_tag",
|
|
35952
35792
|
"delete_tag",
|
|
35953
35793
|
"create_category",
|
|
35954
35794
|
"update_category",
|
|
35955
35795
|
"delete_category",
|
|
35956
|
-
"
|
|
35957
|
-
"update_budget",
|
|
35958
|
-
"delete_budget",
|
|
35796
|
+
"set_budget",
|
|
35959
35797
|
"set_recurring_state",
|
|
35960
|
-
"delete_recurring",
|
|
35961
|
-
"update_goal",
|
|
35962
|
-
"delete_goal",
|
|
35963
|
-
"update_tag",
|
|
35964
35798
|
"create_recurring",
|
|
35965
|
-
"
|
|
35966
|
-
"
|
|
35799
|
+
"update_recurring",
|
|
35800
|
+
"delete_recurring"
|
|
35967
35801
|
]);
|
|
35968
35802
|
async handleCallTool(name, typedArgs) {
|
|
35969
35803
|
if (CopilotMoneyServer.WRITE_TOOLS.has(name) && !this.writeEnabled) {
|
|
@@ -36062,14 +35896,8 @@ class CopilotMoneyServer {
|
|
|
36062
35896
|
case "delete_category":
|
|
36063
35897
|
result = await this.tools.deleteCategory(typedArgs);
|
|
36064
35898
|
break;
|
|
36065
|
-
case "
|
|
36066
|
-
result = await this.tools.
|
|
36067
|
-
break;
|
|
36068
|
-
case "update_budget":
|
|
36069
|
-
result = await this.tools.updateBudget(typedArgs);
|
|
36070
|
-
break;
|
|
36071
|
-
case "delete_budget":
|
|
36072
|
-
result = await this.tools.deleteBudget(typedArgs);
|
|
35899
|
+
case "set_budget":
|
|
35900
|
+
result = await this.tools.setBudget(typedArgs);
|
|
36073
35901
|
break;
|
|
36074
35902
|
case "set_recurring_state":
|
|
36075
35903
|
result = await this.tools.setRecurringState(typedArgs);
|
|
@@ -36077,21 +35905,12 @@ class CopilotMoneyServer {
|
|
|
36077
35905
|
case "delete_recurring":
|
|
36078
35906
|
result = await this.tools.deleteRecurring(typedArgs);
|
|
36079
35907
|
break;
|
|
36080
|
-
case "update_goal":
|
|
36081
|
-
result = await this.tools.updateGoal(typedArgs);
|
|
36082
|
-
break;
|
|
36083
|
-
case "delete_goal":
|
|
36084
|
-
result = await this.tools.deleteGoal(typedArgs);
|
|
36085
|
-
break;
|
|
36086
35908
|
case "update_tag":
|
|
36087
35909
|
result = await this.tools.updateTag(typedArgs);
|
|
36088
35910
|
break;
|
|
36089
35911
|
case "create_recurring":
|
|
36090
35912
|
result = await this.tools.createRecurring(typedArgs);
|
|
36091
35913
|
break;
|
|
36092
|
-
case "create_goal":
|
|
36093
|
-
result = await this.tools.createGoal(typedArgs);
|
|
36094
|
-
break;
|
|
36095
35914
|
case "update_recurring":
|
|
36096
35915
|
result = await this.tools.updateRecurring(typedArgs);
|
|
36097
35916
|
break;
|