@robbeverhelst/do 1.332.0 → 1.333.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/do.js +486 -33
- package/package.json +1 -1
package/dist/do.js
CHANGED
|
@@ -14410,7 +14410,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
|
|
|
14410
14410
|
|
|
14411
14411
|
// src/meta.ts
|
|
14412
14412
|
var CLI_NAME = "do";
|
|
14413
|
-
var VERSION = "1.
|
|
14413
|
+
var VERSION = "1.333.0";
|
|
14414
14414
|
var API_PREFIX = "/api/v1";
|
|
14415
14415
|
var DEFAULT_API_URL = "http://localhost:3001";
|
|
14416
14416
|
|
|
@@ -18355,6 +18355,7 @@ var userSettings = pgTable("user_settings", {
|
|
|
18355
18355
|
foodDiaryMode: boolean4("food_diary_mode").notNull().default(false),
|
|
18356
18356
|
calendarZoom: integer2("calendar_zoom"),
|
|
18357
18357
|
lifeBlockSharing: jsonb("life_block_sharing").$type(),
|
|
18358
|
+
recentSearches: jsonb("recent_searches").$type().notNull().default([]),
|
|
18358
18359
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
18359
18360
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
18360
18361
|
}, (t) => [unique("user_settings_tenant_user_uniq").on(t.tenantId, t.userId)]);
|
|
@@ -19955,9 +19956,29 @@ var animalLostCards = pgTable("animal_lost_cards", {
|
|
|
19955
19956
|
phone: text("phone"),
|
|
19956
19957
|
showChipped: boolean4("show_chipped").notNull().default(false),
|
|
19957
19958
|
note: text("note"),
|
|
19959
|
+
description: text("description"),
|
|
19960
|
+
answersTo: text("answers_to"),
|
|
19961
|
+
missingSince: timestamp("missing_since", { withTimezone: true }),
|
|
19962
|
+
missingWhere: text("missing_where"),
|
|
19958
19963
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
19959
19964
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
19960
19965
|
}, (t) => [index("animal_lost_cards_tenant_idx").on(t.tenantId)]);
|
|
19966
|
+
var animalLostReportFrom = pgEnum("animal_lost_report_from", ["owner", "finder"]);
|
|
19967
|
+
var animalLostReports = pgTable("animal_lost_reports", {
|
|
19968
|
+
id: text("id").primaryKey(),
|
|
19969
|
+
tenantId: text("tenant_id").notNull().references(() => tenants.id),
|
|
19970
|
+
animalId: text("animal_id").notNull().references(() => animals.id, { onDelete: "cascade" }),
|
|
19971
|
+
from: animalLostReportFrom("from").notNull(),
|
|
19972
|
+
at: timestamp("at", { withTimezone: true }).notNull(),
|
|
19973
|
+
title: text("title"),
|
|
19974
|
+
note: text("note"),
|
|
19975
|
+
place: text("place"),
|
|
19976
|
+
lat: doublePrecision("lat"),
|
|
19977
|
+
lng: doublePrecision("lng"),
|
|
19978
|
+
phone: text("phone"),
|
|
19979
|
+
photoFileId: text("photo_file_id"),
|
|
19980
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
19981
|
+
}, (t) => [index("animal_lost_reports_animal_idx").on(t.tenantId, t.animalId, t.at)]);
|
|
19961
19982
|
var animalHandoverLinks = pgTable("animal_handover_links", {
|
|
19962
19983
|
id: text("id").primaryKey(),
|
|
19963
19984
|
tenantId: text("tenant_id").notNull().references(() => tenants.id),
|
|
@@ -20769,22 +20790,72 @@ var animalSitterNoteInputSchema = object({
|
|
|
20769
20790
|
body: string2().trim().max(1000).default("")
|
|
20770
20791
|
}).strict();
|
|
20771
20792
|
var animalSitterPhoneSchema = object({ phone: string2() });
|
|
20793
|
+
var lostCardPhoneSchema = string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -");
|
|
20772
20794
|
var animalLostCardInputSchema = object({
|
|
20773
20795
|
photoFileId: ulidSchema.nullish(),
|
|
20774
|
-
phone:
|
|
20796
|
+
phone: lostCardPhoneSchema.nullish(),
|
|
20775
20797
|
showChipped: boolean2().optional(),
|
|
20776
|
-
note: string2().trim().min(1).max(200).nullish()
|
|
20798
|
+
note: string2().trim().min(1).max(200).nullish(),
|
|
20799
|
+
description: string2().trim().min(1).max(400).nullish(),
|
|
20800
|
+
answersTo: string2().trim().min(1).max(120).nullish(),
|
|
20801
|
+
missingSince: exports_coerce.date().nullish(),
|
|
20802
|
+
missingWhere: string2().trim().min(1).max(200).nullish()
|
|
20777
20803
|
}).strict();
|
|
20778
20804
|
var animalLostCardPhotoSchema = object({ id: ulidSchema, name: string2() });
|
|
20805
|
+
var animalLostUpdateSchema = object({
|
|
20806
|
+
id: ulidSchema,
|
|
20807
|
+
at: exports_coerce.date(),
|
|
20808
|
+
title: string2(),
|
|
20809
|
+
note: string2().nullable(),
|
|
20810
|
+
place: string2().nullable()
|
|
20811
|
+
});
|
|
20779
20812
|
var animalLostCardPublicSchema = object({
|
|
20780
20813
|
name: string2(),
|
|
20781
20814
|
speciesKey: string2().nullable(),
|
|
20782
20815
|
kind: string2().nullable(),
|
|
20783
20816
|
missing: boolean2(),
|
|
20784
20817
|
hasPhoto: boolean2(),
|
|
20785
|
-
|
|
20818
|
+
phoneMasked: string2().nullable(),
|
|
20786
20819
|
chipped: boolean2(),
|
|
20787
|
-
note: string2().nullable()
|
|
20820
|
+
note: string2().nullable(),
|
|
20821
|
+
description: string2().nullable(),
|
|
20822
|
+
answersTo: string2().nullable(),
|
|
20823
|
+
missingSince: exports_coerce.date().nullable(),
|
|
20824
|
+
missingWhere: string2().nullable(),
|
|
20825
|
+
pronoun: _enum2(["he", "she", "they"]),
|
|
20826
|
+
sharedBy: string2().nullable(),
|
|
20827
|
+
owners: array(string2()),
|
|
20828
|
+
vet: string2().nullable(),
|
|
20829
|
+
updates: array(animalLostUpdateSchema)
|
|
20830
|
+
});
|
|
20831
|
+
var animalLostCardCallSchema = object({ phone: string2() });
|
|
20832
|
+
var blankless = (v) => typeof v === "string" && v.trim() === "" ? undefined : v;
|
|
20833
|
+
var animalLostFoundInputSchema = object({
|
|
20834
|
+
where: string2().trim().min(1).max(300),
|
|
20835
|
+
lat: preprocess(blankless, exports_coerce.number().min(-90).max(90).optional()),
|
|
20836
|
+
lng: preprocess(blankless, exports_coerce.number().min(-180).max(180).optional()),
|
|
20837
|
+
phone: preprocess(blankless, lostCardPhoneSchema.optional())
|
|
20838
|
+
}).strict().refine((v) => v.lat === undefined === (v.lng === undefined), {
|
|
20839
|
+
message: "Send both lat and lng, or neither."
|
|
20840
|
+
});
|
|
20841
|
+
var animalLostFoundSentSchema = object({ to: array(string2()) });
|
|
20842
|
+
var animalLostUpdateInputSchema = object({
|
|
20843
|
+
at: exports_coerce.date(),
|
|
20844
|
+
title: string2().trim().min(1).max(120),
|
|
20845
|
+
note: string2().trim().min(1).max(200).nullish(),
|
|
20846
|
+
place: string2().trim().min(1).max(200).nullish()
|
|
20847
|
+
}).strict();
|
|
20848
|
+
var animalLostReportSchema = object({
|
|
20849
|
+
id: ulidSchema,
|
|
20850
|
+
from: _enum2(["owner", "finder"]),
|
|
20851
|
+
at: exports_coerce.date(),
|
|
20852
|
+
title: string2().nullable(),
|
|
20853
|
+
note: string2().nullable(),
|
|
20854
|
+
place: string2().nullable(),
|
|
20855
|
+
lat: number2().nullable(),
|
|
20856
|
+
lng: number2().nullable(),
|
|
20857
|
+
phone: string2().nullable(),
|
|
20858
|
+
photoFileId: ulidSchema.nullable()
|
|
20788
20859
|
});
|
|
20789
20860
|
var animalLostCardStateSchema = object({
|
|
20790
20861
|
card: object({
|
|
@@ -20793,7 +20864,12 @@ var animalLostCardStateSchema = object({
|
|
|
20793
20864
|
phone: string2().nullable(),
|
|
20794
20865
|
showChipped: boolean2(),
|
|
20795
20866
|
note: string2().nullable(),
|
|
20867
|
+
description: string2().nullable(),
|
|
20868
|
+
answersTo: string2().nullable(),
|
|
20869
|
+
missingSince: exports_coerce.date().nullable(),
|
|
20870
|
+
missingWhere: string2().nullable(),
|
|
20796
20871
|
since: exports_coerce.date(),
|
|
20872
|
+
reports: array(animalLostReportSchema),
|
|
20797
20873
|
preview: animalLostCardPublicSchema
|
|
20798
20874
|
}).nullable(),
|
|
20799
20875
|
photos: array(animalLostCardPhotoSchema),
|
|
@@ -21044,7 +21120,7 @@ var animalOperations = defineOperations({
|
|
|
21044
21120
|
scopes: READ,
|
|
21045
21121
|
tag: "Animals",
|
|
21046
21122
|
summary: "An animal's lost-pet card",
|
|
21047
|
-
description: "`card` is null while it's off; else its `path` (`/lost/<token>`, the page its QR code opens, no account), what it shows (`photoFileId`, `phone`, `showChipped`, `note`) and `preview`, the finder's page. `photos` are the pictures on the animal it may show; `chipOnFile` whether it can say chipped.",
|
|
21123
|
+
description: "`card` is null while it's off; else its `path` (`/lost/<token>`, the page its QR code opens, no account), what it shows (`photoFileId`, `phone`, `showChipped`, `note`, `description`, `answersTo`, `missingSince`, `missingWhere`), `reports` (the household's updates and finders' reports, with a finder's number) and `preview`, the finder's page (its number masked). `photos` are the pictures on the animal it may show; `chipOnFile` whether it can say chipped.",
|
|
21048
21124
|
status: 200,
|
|
21049
21125
|
response: { description: "The card's state." },
|
|
21050
21126
|
errors: { 404: true }
|
|
@@ -21736,6 +21812,8 @@ var taskOwnerSchema = actorRefSchema.extend({ desk: string2().nullable() });
|
|
|
21736
21812
|
var taskFilingSuggestionSchema = object({
|
|
21737
21813
|
areaId: ulidSchema.nullable(),
|
|
21738
21814
|
area: string2().nullable(),
|
|
21815
|
+
projectId: ulidSchema.nullable().default(null),
|
|
21816
|
+
project: string2().nullable().default(null),
|
|
21739
21817
|
owner: taskOwnerSchema.nullable(),
|
|
21740
21818
|
scheduledAt: exports_coerce.date().nullable(),
|
|
21741
21819
|
by: actorRefSchema
|
|
@@ -23146,6 +23224,8 @@ var moneyBankPendingSchema = object({
|
|
|
23146
23224
|
pendingId: ulidSchema,
|
|
23147
23225
|
bank: string2(),
|
|
23148
23226
|
consentValidUntil: exports_coerce.date(),
|
|
23227
|
+
createdAt: exports_coerce.date(),
|
|
23228
|
+
expiresAt: exports_coerce.date(),
|
|
23149
23229
|
accounts: array(object({
|
|
23150
23230
|
index: number2().int().min(0),
|
|
23151
23231
|
name: string2().nullable(),
|
|
@@ -23181,6 +23261,33 @@ var moneyBankSyncResultSchema = object({
|
|
|
23181
23261
|
error: true
|
|
23182
23262
|
}).extend({ accountTitle: string2() }))
|
|
23183
23263
|
});
|
|
23264
|
+
var moneyDedupeInputSchema = object({ accountId: ulidSchema, commit: boolean2().optional() }).strict();
|
|
23265
|
+
var moneyDedupePairSchema = object({
|
|
23266
|
+
keepId: string2(),
|
|
23267
|
+
twinId: string2(),
|
|
23268
|
+
bookedOn: string2(),
|
|
23269
|
+
twinBookedOn: string2(),
|
|
23270
|
+
amount: string2(),
|
|
23271
|
+
description: string2().nullable(),
|
|
23272
|
+
counterpartyName: string2().nullable(),
|
|
23273
|
+
ambiguous: boolean2()
|
|
23274
|
+
});
|
|
23275
|
+
var moneyDedupeChangesSchema = object({
|
|
23276
|
+
merged: number2().int(),
|
|
23277
|
+
links: number2().int(),
|
|
23278
|
+
splits: number2().int(),
|
|
23279
|
+
settlements: number2().int(),
|
|
23280
|
+
transfers: number2().int(),
|
|
23281
|
+
rules: number2().int(),
|
|
23282
|
+
notes: number2().int()
|
|
23283
|
+
});
|
|
23284
|
+
var moneyDedupeResultSchema = object({
|
|
23285
|
+
accountId: string2(),
|
|
23286
|
+
committed: boolean2(),
|
|
23287
|
+
pairs: array(moneyDedupePairSchema),
|
|
23288
|
+
counts: object({ pairs: number2().int(), ambiguous: number2().int() }),
|
|
23289
|
+
changed: moneyDedupeChangesSchema
|
|
23290
|
+
});
|
|
23184
23291
|
|
|
23185
23292
|
// ../../packages/schema/src/operations/bank-link.ts
|
|
23186
23293
|
var CONNECT = ["connections:write"];
|
|
@@ -23263,6 +23370,18 @@ var bankLinkOperations = defineOperations({
|
|
|
23263
23370
|
response: { description: "The bank accounts to map." },
|
|
23264
23371
|
errors: { 400: true, 403: true, 409: true }
|
|
23265
23372
|
},
|
|
23373
|
+
pendings: {
|
|
23374
|
+
method: "get",
|
|
23375
|
+
path: "/money/bank-link/pending",
|
|
23376
|
+
scopes: CONNECT,
|
|
23377
|
+
afterScopes: AFTER,
|
|
23378
|
+
tag: "Money",
|
|
23379
|
+
summary: "List the bank links waiting to be mapped",
|
|
23380
|
+
description: "Every authorisation the key's member brought back from a bank and hasn't mapped yet, newest first, while it lasts (a day): each as the pending answers it, with `createdAt` and `expiresAt`. For when the page that finished it is gone; map one with link.",
|
|
23381
|
+
status: 200,
|
|
23382
|
+
response: { description: "The bank links waiting to be mapped." },
|
|
23383
|
+
errors: { 403: true }
|
|
23384
|
+
},
|
|
23266
23385
|
pending: {
|
|
23267
23386
|
method: "get",
|
|
23268
23387
|
path: "/money/bank-link/pending/{id}",
|
|
@@ -25085,7 +25204,8 @@ var moneyAccountSchema = createSelectSchema(moneyAccounts, {
|
|
|
25085
25204
|
});
|
|
25086
25205
|
var moneyAccountViewSchema = moneyAccountSchema.omit({ ibanHash: true }).extend({
|
|
25087
25206
|
title: string2(),
|
|
25088
|
-
visibility: _enum2(NODE_VISIBILITY_VALUES)
|
|
25207
|
+
visibility: _enum2(NODE_VISIBILITY_VALUES),
|
|
25208
|
+
balanceKnown: boolean2()
|
|
25089
25209
|
});
|
|
25090
25210
|
var moneyCategorySchema = createSelectSchema(moneyCategories, {
|
|
25091
25211
|
archivedAt: exports_coerce.date().nullable(),
|
|
@@ -25145,7 +25265,14 @@ var createMoneyAccountInputSchema = object({
|
|
|
25145
25265
|
includeInNetWorth: boolean2().optional(),
|
|
25146
25266
|
visibility: _enum2(NODE_VISIBILITY_VALUES).optional()
|
|
25147
25267
|
}).strict();
|
|
25148
|
-
var
|
|
25268
|
+
var moneyBalanceAtSchema = union([keyDateDaySchema, exports_iso.datetime({ offset: true })]);
|
|
25269
|
+
var updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().extend({
|
|
25270
|
+
balance: moneyAmountSchema.optional(),
|
|
25271
|
+
balanceAt: moneyBalanceAtSchema.optional()
|
|
25272
|
+
}).strict().refine((u) => u.balance === undefined === (u.balanceAt === undefined), {
|
|
25273
|
+
message: "a balance and the day it's from go together",
|
|
25274
|
+
path: ["balanceAt"]
|
|
25275
|
+
});
|
|
25149
25276
|
var createMoneyCategoryInputSchema = object({
|
|
25150
25277
|
id: ulidSchema,
|
|
25151
25278
|
name: string2().trim().min(1).max(60),
|
|
@@ -25240,8 +25367,13 @@ var moneyTransactionPageSchema = object({
|
|
|
25240
25367
|
var MONEY_IMPORT_MAX_BYTES = 5 * 1024 * 1024;
|
|
25241
25368
|
var moneyImportFieldsSchema = object({
|
|
25242
25369
|
accountId: ulidSchema,
|
|
25243
|
-
since: keyDateDaySchema.optional()
|
|
25244
|
-
|
|
25370
|
+
since: keyDateDaySchema.optional(),
|
|
25371
|
+
balance: moneyAmountSchema.optional(),
|
|
25372
|
+
balanceAt: moneyBalanceAtSchema.optional()
|
|
25373
|
+
}).strict().refine((f) => f.balance === undefined === (f.balanceAt === undefined), {
|
|
25374
|
+
message: "a balance and the day it's from go together",
|
|
25375
|
+
path: ["balanceAt"]
|
|
25376
|
+
});
|
|
25245
25377
|
var moneyImportOutcomeSchema = _enum2([
|
|
25246
25378
|
"new",
|
|
25247
25379
|
"duplicate",
|
|
@@ -26945,6 +27077,8 @@ var healthShares = pgTable("health_shares", {
|
|
|
26945
27077
|
episodeId: text("episode_id"),
|
|
26946
27078
|
sections: text("sections").array().notNull().default(sql`'{}'::text[]`),
|
|
26947
27079
|
snapshot: jsonb("snapshot").notNull(),
|
|
27080
|
+
note: text("note"),
|
|
27081
|
+
attachments: jsonb("attachments").notNull().default(sql`'[]'::jsonb`),
|
|
26948
27082
|
tokenHash: text("token_hash").notNull().unique(),
|
|
26949
27083
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
26950
27084
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
@@ -32057,6 +32191,98 @@ var keyOperations = defineOperations({
|
|
|
32057
32191
|
}
|
|
32058
32192
|
});
|
|
32059
32193
|
|
|
32194
|
+
// ../../packages/schema/src/contracts/money-kraken.ts
|
|
32195
|
+
var KRAKEN_SECRET = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
32196
|
+
var moneyKrakenKeyInputSchema = object({
|
|
32197
|
+
apiKey: string2().trim().min(16).max(200).regex(/^\S+$/, "The API key has no spaces."),
|
|
32198
|
+
apiSecret: string2().trim().min(40).max(200).regex(KRAKEN_SECRET, { message: "Paste the private key as Kraken shows it (base64)." }),
|
|
32199
|
+
accountId: ulidSchema.optional()
|
|
32200
|
+
}).strict();
|
|
32201
|
+
var moneyHoldingSchema = object({
|
|
32202
|
+
asset: string2(),
|
|
32203
|
+
amount: string2(),
|
|
32204
|
+
priceEur: string2().nullable(),
|
|
32205
|
+
valueEur: string2().nullable(),
|
|
32206
|
+
share: number2().nullable()
|
|
32207
|
+
});
|
|
32208
|
+
var moneyHoldingsSchema = object({
|
|
32209
|
+
accountId: ulidSchema,
|
|
32210
|
+
at: exports_coerce.date(),
|
|
32211
|
+
totalEur: string2(),
|
|
32212
|
+
dustEur: string2(),
|
|
32213
|
+
holdings: array(moneyHoldingSchema)
|
|
32214
|
+
});
|
|
32215
|
+
var moneyAccountHoldingsSchema = object({ holdings: moneyHoldingsSchema.nullable() });
|
|
32216
|
+
var moneyKrakenLinkStatusSchema = object({
|
|
32217
|
+
configured: boolean2(),
|
|
32218
|
+
link: object({
|
|
32219
|
+
id: ulidSchema,
|
|
32220
|
+
accountId: ulidSchema,
|
|
32221
|
+
accountTitle: string2(),
|
|
32222
|
+
status: _enum2(moneyConnectionStatus.enumValues),
|
|
32223
|
+
lastSyncAt: exports_coerce.date().nullable(),
|
|
32224
|
+
lastError: string2().nullable()
|
|
32225
|
+
}).nullable(),
|
|
32226
|
+
holdings: moneyHoldingsSchema.nullable()
|
|
32227
|
+
});
|
|
32228
|
+
|
|
32229
|
+
// ../../packages/schema/src/operations/kraken-link.ts
|
|
32230
|
+
var CONNECT2 = ["connections:write"];
|
|
32231
|
+
var AFTER2 = "`*` and `private:*` never grant `connections:write`: a key holds it by name, and a guest's key never does.";
|
|
32232
|
+
var krakenLinkOperations = defineOperations({
|
|
32233
|
+
status: {
|
|
32234
|
+
method: "get",
|
|
32235
|
+
path: "/money/kraken-link",
|
|
32236
|
+
scopes: CONNECT2,
|
|
32237
|
+
afterScopes: AFTER2,
|
|
32238
|
+
tag: "Money",
|
|
32239
|
+
summary: "Get the Kraken link",
|
|
32240
|
+
description: "Whether the key's member has a Kraken key set (`configured`), the investment account it feeds with how its last sync went (`link`: `status` `active` or `failing`, `lastSyncAt`, `lastError`), and the `holdings` that sync saw: per asset its `amount`, EUR `priceEur` and `valueEur` and `share`, with `totalEur` and `at`; holdings worth under \u20AC0.50 are left out of the list (`dustEur` sums them), never out of the total. The key is never returned.",
|
|
32241
|
+
status: 200,
|
|
32242
|
+
response: { description: "The Kraken link." },
|
|
32243
|
+
errors: { 403: true }
|
|
32244
|
+
},
|
|
32245
|
+
set: {
|
|
32246
|
+
method: "put",
|
|
32247
|
+
path: "/money/kraken-link",
|
|
32248
|
+
scopes: CONNECT2,
|
|
32249
|
+
afterScopes: AFTER2,
|
|
32250
|
+
tag: "Money",
|
|
32251
|
+
summary: "Set the Kraken key",
|
|
32252
|
+
description: "Keep the key's member's Kraken `apiKey` and `apiSecret` (the private key), sealed, then sync. Make the key at Kraken with only Query Funds ticked. The key is checked, never echoed: it must read the balance and must not see open orders (a key that can may do more than read, and is refused); the answer is the link, without it. `accountId` picks the investment account it feeds; left out, the link keeps its account, or a first set makes one named Kraken. 400 for a key Kraken refuses, a key that isn't read-only, or an account that isn't an EUR investment account.",
|
|
32253
|
+
body: moneyKrakenKeyInputSchema,
|
|
32254
|
+
status: 200,
|
|
32255
|
+
response: { description: "The Kraken link, without the key." },
|
|
32256
|
+
errors: { 400: true, 403: true, 404: true, 503: true },
|
|
32257
|
+
throttle: "strict",
|
|
32258
|
+
mcp: false
|
|
32259
|
+
},
|
|
32260
|
+
sync: {
|
|
32261
|
+
method: "post",
|
|
32262
|
+
path: "/money/kraken-link/sync",
|
|
32263
|
+
scopes: CONNECT2,
|
|
32264
|
+
afterScopes: AFTER2,
|
|
32265
|
+
tag: "Money",
|
|
32266
|
+
summary: "Sync Kraken now",
|
|
32267
|
+
description: "Read the holdings from Kraken now, value them in EUR and set the account's balance, as the daily sync does. A failure keeps the last holdings and marks the link `failing` with its reason. Answers the link. 400 before a key is set.",
|
|
32268
|
+
status: 200,
|
|
32269
|
+
response: { description: "The Kraken link after the sync." },
|
|
32270
|
+
errors: { 400: true, 403: true }
|
|
32271
|
+
},
|
|
32272
|
+
remove: {
|
|
32273
|
+
method: "delete",
|
|
32274
|
+
path: "/money/kraken-link",
|
|
32275
|
+
scopes: CONNECT2,
|
|
32276
|
+
afterScopes: AFTER2,
|
|
32277
|
+
tag: "Money",
|
|
32278
|
+
summary: "Remove the Kraken link",
|
|
32279
|
+
description: "Forget the key's member's Kraken key and stop syncing. The account stays, with its last balance and holdings.",
|
|
32280
|
+
status: 200,
|
|
32281
|
+
response: { description: "The Kraken link, not configured." },
|
|
32282
|
+
errors: { 403: true }
|
|
32283
|
+
}
|
|
32284
|
+
});
|
|
32285
|
+
|
|
32060
32286
|
// ../../packages/schema/src/db/mail.ts
|
|
32061
32287
|
var mailCampaignStatus = pgEnum("mail_campaign_status", [
|
|
32062
32288
|
"draft",
|
|
@@ -32602,6 +32828,13 @@ var moneyNetWorthAccountSchema = object({
|
|
|
32602
32828
|
source: moneySnapshotSourceSchema,
|
|
32603
32829
|
month: moneyMonthSchema
|
|
32604
32830
|
});
|
|
32831
|
+
var moneyNetWorthUnknownSchema = moneyNetWorthAccountSchema.pick({
|
|
32832
|
+
accountId: true,
|
|
32833
|
+
title: true,
|
|
32834
|
+
kind: true,
|
|
32835
|
+
visibility: true,
|
|
32836
|
+
currency: true
|
|
32837
|
+
});
|
|
32605
32838
|
var moneyNetWorthSchema = object({
|
|
32606
32839
|
scope: moneyBudgetScopeSchema,
|
|
32607
32840
|
month: moneyMonthSchema,
|
|
@@ -32609,7 +32842,9 @@ var moneyNetWorthSchema = object({
|
|
|
32609
32842
|
have: amount5,
|
|
32610
32843
|
owe: amount5,
|
|
32611
32844
|
points: array(moneyNetWorthPointSchema),
|
|
32612
|
-
accounts: array(moneyNetWorthAccountSchema)
|
|
32845
|
+
accounts: array(moneyNetWorthAccountSchema),
|
|
32846
|
+
complete: boolean2(),
|
|
32847
|
+
unknown: array(moneyNetWorthUnknownSchema)
|
|
32613
32848
|
});
|
|
32614
32849
|
var setMoneyNetWorthValueInputSchema = object({
|
|
32615
32850
|
accountId: ulidSchema,
|
|
@@ -32791,7 +33026,7 @@ var moneyOperations = defineOperations({
|
|
|
32791
33026
|
scopes: ["finance:write"],
|
|
32792
33027
|
tag: "Money",
|
|
32793
33028
|
summary: "Import a statement",
|
|
32794
|
-
description: "Book a statement's new lines as unreviewed transactions (the same upload as the preview). Re-importing a file, or an overlapping one, books nothing twice. An account without an IBAN takes the file's; the newest line's balance becomes the account's.",
|
|
33029
|
+
description: "Book a statement's new lines as unreviewed transactions (the same upload as the preview). Re-importing a file, or an overlapping one, books nothing twice. An account without an IBAN takes the file's; the newest line's balance becomes the account's. For a bank whose export has no running balance, `balance` + `balanceAt` (a day, read as its end, or a moment; together, never a day ahead) state what the account holds and anchor it there, as an account update does; a preview ignores them.",
|
|
32795
33030
|
body: moneyImportFieldsSchema,
|
|
32796
33031
|
multipart: STATEMENT,
|
|
32797
33032
|
status: 201,
|
|
@@ -32838,7 +33073,7 @@ var moneyOperations = defineOperations({
|
|
|
32838
33073
|
scopes: ["finance:write"],
|
|
32839
33074
|
tag: "Money",
|
|
32840
33075
|
summary: "Sync bank connections now",
|
|
32841
|
-
description: "Sync every live bank link the key may see, or with `accountId` the one feeding that account, the way the daily run does: booked transactions go through the CSV import's pipeline (the bank's entry id, else the CSV row of the same day
|
|
33076
|
+
description: "Sync every live bank link the key may see, or with `accountId` the one feeding that account, the way the daily run does: booked transactions go through the CSV import's pipeline (the bank's entry id, else the CSV row of the same amount booked within a day \u2014 the most alike when several fit \u2014 is matched: it adopts the bank's entry id instead of the row being booked twice), the rules and transfer matching run, and the balance is updated. Per link: outcome (ok \xB7 failed), fetched, imported, matched, duplicates and the error in plain words. An account is the household's (`visibility: space`) or \"Only me\" (`owner`, the default for a new one); another member never sees an Only me account or anything on it.",
|
|
32842
33077
|
body: moneyBankSyncInputSchema,
|
|
32843
33078
|
status: 200,
|
|
32844
33079
|
response: { description: "What each sync did." },
|
|
@@ -32860,6 +33095,18 @@ var moneyOperations = defineOperations({
|
|
|
32860
33095
|
response: { description: "The sync log." },
|
|
32861
33096
|
errors: { 404: `No such account or transaction, or it is another member's "Only me" one.` }
|
|
32862
33097
|
},
|
|
33098
|
+
dedupe: {
|
|
33099
|
+
method: "post",
|
|
33100
|
+
path: "/money/dedupe",
|
|
33101
|
+
scopes: ["finance:write"],
|
|
33102
|
+
tag: "Money",
|
|
33103
|
+
summary: "Fold a bank link's twins into the statement's rows",
|
|
33104
|
+
description: "Find, on one `accountId`, the movements booked twice: a statement import's row (no bank entry id) and a bank link's row (with one), paired by the bank sync's own rule \u2014 the same amount, booked a day apart at most, the most alike when several fit (counterparty IBAN, structured reference, name, then the words), never one row twice. A preview by default: the pairs (`keepId` the statement's row, `twinId` the bank's, their days, amount, description) and their counts, writing nothing. With `commit: true` each pair is folded: the statement's row keeps its category, review status, notes, links, splits and transfer, takes the bank's entry id and the details it lacked; whatever pointed at the twin (links, splits, settlements, its transfer peer, a rule made from it, its note) moves to the kept row, and the twin is deleted. `changed` counts what moved. A pair marked `ambiguous` (another row fit just as well) is listed but never committed. Running it again changes nothing.",
|
|
33105
|
+
body: moneyDedupeInputSchema,
|
|
33106
|
+
status: 200,
|
|
33107
|
+
response: { description: "The pairs, their counts and what changed." },
|
|
33108
|
+
errors: { 400: true, 404: NO_ACCOUNT }
|
|
33109
|
+
},
|
|
32863
33110
|
getAccount: {
|
|
32864
33111
|
method: "get",
|
|
32865
33112
|
path: "/money/accounts/{id}",
|
|
@@ -32879,7 +33126,7 @@ var moneyOperations = defineOperations({
|
|
|
32879
33126
|
scopes: ["finance:write"],
|
|
32880
33127
|
tag: "Money",
|
|
32881
33128
|
summary: "Update a money account",
|
|
32882
|
-
description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it.",
|
|
33129
|
+
description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it. `balance` with `balanceAt` (a day, read as its end, or a moment) states what it held then: the anchor net worth counts from, back and forward through the ledger, for a bank whose export has no running balance. The two go together; a day ahead, or an account a bank link keeps, is a 400.",
|
|
32883
33130
|
pathParams: { id: "The account's ULID." },
|
|
32884
33131
|
pathParamSchemas: { id: ulidSchema },
|
|
32885
33132
|
body: updateMoneyAccountInputSchema,
|
|
@@ -33399,7 +33646,7 @@ var moneyOperations = defineOperations({
|
|
|
33399
33646
|
scopes: ["finance:read"],
|
|
33400
33647
|
tag: "Money",
|
|
33401
33648
|
summary: "Get net worth and its monthly line",
|
|
33402
|
-
description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `points` (one per month, oldest first, the current month last; `total` null before any account had a value) and `accounts` (biggest first, each with its `value`, `source` \u2014 balance, ledger or manual \u2014 and the `month` that value is from). A loan or a credit card always counts as owed. This month follows each account's balance
|
|
33649
|
+
description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `points` (one per month, oldest first, the current month last; `total` null before any account had a value) and `accounts` (biggest first, each with its `value`, `source` \u2014 balance, ledger or manual \u2014 and the `month` that value is from). A loan or a credit card always counts as owed. This month follows each account's balance, carried forward by what was booked after `balanceAt`; the months before an account was first read are worked back from its ledger; a value set by hand carries on until the next one. An account whose ledger came from a bank but has no balance yet (an export with no running balance) is never counted as 0: it is listed under `unknown` and left out of the total, and `complete` is false. `scope`: `all` (default), `space` or `owner`; `months` 2\u201360 (12 by default).",
|
|
33403
33650
|
query: [
|
|
33404
33651
|
{ name: "scope", description: "all, space or owner." },
|
|
33405
33652
|
{ name: "months", description: "How many months the line covers (2\u201360, default 12)." }
|
|
@@ -39215,7 +39462,7 @@ var STATUS_MARK = {
|
|
|
39215
39462
|
};
|
|
39216
39463
|
function suggestionText(s) {
|
|
39217
39464
|
const parts = [
|
|
39218
|
-
s.area,
|
|
39465
|
+
[s.area, s.project].filter(Boolean).join(" \u203A ") || null,
|
|
39219
39466
|
s.owner ? `owner ${ownerText(s.owner)}` : null,
|
|
39220
39467
|
s.scheduledAt ? shortDate(s.scheduledAt) : null
|
|
39221
39468
|
].filter(Boolean);
|
|
@@ -42682,12 +42929,18 @@ var CONNECTION_COMMAND_SPECS = [
|
|
|
42682
42929
|
},
|
|
42683
42930
|
{
|
|
42684
42931
|
path: ["money", "bank-link", "pending"],
|
|
42685
|
-
summary: "
|
|
42686
|
-
args: [
|
|
42932
|
+
summary: "The bank authorisations waiting to be mapped, or one by its id: bank accounts and suggestions.",
|
|
42933
|
+
args: [
|
|
42934
|
+
{
|
|
42935
|
+
name: "id",
|
|
42936
|
+
required: false,
|
|
42937
|
+
description: "The pendingId from `complete`; without it, every one still waiting."
|
|
42938
|
+
}
|
|
42939
|
+
],
|
|
42687
42940
|
flags: [],
|
|
42688
42941
|
scopes: scopesOf(bankLinkOperations.pending),
|
|
42689
|
-
returns: "MoneyBankPending",
|
|
42690
|
-
notes: `${routeOf(bankLinkOperations.pending)}.`
|
|
42942
|
+
returns: "MoneyBankPending (one) or MoneyBankPending[] (all)",
|
|
42943
|
+
notes: `${routeOf(bankLinkOperations.pending)}; without an id ${routeOf(bankLinkOperations.pendings)}, newest first, each with createdAt and expiresAt (a day after). Finds an authorisation whose browser tab is gone, before it runs out.`
|
|
42691
42944
|
},
|
|
42692
42945
|
{
|
|
42693
42946
|
path: ["money", "bank-link", "link"],
|
|
@@ -42705,6 +42958,53 @@ var CONNECTION_COMMAND_SPECS = [
|
|
|
42705
42958
|
returns: "MoneyConnectionView[]",
|
|
42706
42959
|
notes: `${routeOf(bankLinkOperations.link)}. Each do account is fed by one bank account.`
|
|
42707
42960
|
},
|
|
42961
|
+
{
|
|
42962
|
+
path: ["money", "kraken-link", "status"],
|
|
42963
|
+
summary: "Your Kraken link: the account it feeds, how its last sync went, and the holdings it saw.",
|
|
42964
|
+
args: [],
|
|
42965
|
+
flags: [],
|
|
42966
|
+
scopes: scopesOf(krakenLinkOperations.status),
|
|
42967
|
+
returns: "MoneyKrakenLinkStatus",
|
|
42968
|
+
notes: `${routeOf(krakenLinkOperations.status)}. The key is never returned.`
|
|
42969
|
+
},
|
|
42970
|
+
{
|
|
42971
|
+
path: ["money", "kraken-link", "set"],
|
|
42972
|
+
summary: "Give do your read-only Kraken API key, read from a file, and sync.",
|
|
42973
|
+
args: [],
|
|
42974
|
+
flags: [
|
|
42975
|
+
{
|
|
42976
|
+
name: "key-file",
|
|
42977
|
+
type: "string",
|
|
42978
|
+
description: "Path to a file holding the API key and its private key (two lines, or JSON with apiKey and apiSecret), or - to read it from stdin."
|
|
42979
|
+
},
|
|
42980
|
+
{
|
|
42981
|
+
name: "account",
|
|
42982
|
+
type: "string",
|
|
42983
|
+
description: "The EUR investment account it feeds. Left out: the link's account, or a new one named Kraken."
|
|
42984
|
+
}
|
|
42985
|
+
],
|
|
42986
|
+
scopes: scopesOf(krakenLinkOperations.set),
|
|
42987
|
+
returns: "MoneyKrakenLinkStatus",
|
|
42988
|
+
notes: `${routeOf(krakenLinkOperations.set)}. Make the key at Kraken with only Query Funds ticked: a key that can see open orders is refused. The key and secret are read from the file and sealed; they are never a flag, never printed and never read back. A new Kraken account is "Only me", so making it needs finance:write on the key too (or pass --account). Replaces a key set before.`
|
|
42989
|
+
},
|
|
42990
|
+
{
|
|
42991
|
+
path: ["money", "kraken-link", "sync"],
|
|
42992
|
+
summary: "Read your Kraken holdings now and update the account's balance.",
|
|
42993
|
+
args: [],
|
|
42994
|
+
flags: [],
|
|
42995
|
+
scopes: scopesOf(krakenLinkOperations.sync),
|
|
42996
|
+
returns: "MoneyKrakenLinkStatus",
|
|
42997
|
+
notes: `${routeOf(krakenLinkOperations.sync)}. A failure keeps the last holdings and shows on the link as failing, with its reason.`
|
|
42998
|
+
},
|
|
42999
|
+
{
|
|
43000
|
+
path: ["money", "kraken-link", "remove"],
|
|
43001
|
+
summary: "Forget your Kraken key. The account keeps its last balance and holdings.",
|
|
43002
|
+
args: [],
|
|
43003
|
+
flags: [DESTRUCTIVE],
|
|
43004
|
+
scopes: scopesOf(krakenLinkOperations.remove),
|
|
43005
|
+
returns: "MoneyKrakenLinkStatus",
|
|
43006
|
+
notes: `${routeOf(krakenLinkOperations.remove)}. Gated behind --allow-destructive.`
|
|
43007
|
+
},
|
|
42708
43008
|
{
|
|
42709
43009
|
path: ["doccle", "connect"],
|
|
42710
43010
|
summary: "Give do your Doccle login so the nightly import can run. do checks it with Doccle first and keeps it sealed.",
|
|
@@ -42811,8 +43111,14 @@ var CONNECTION_HANDLERS = {
|
|
|
42811
43111
|
},
|
|
42812
43112
|
"money bank-link pending": async ({ args, requireClient }) => {
|
|
42813
43113
|
const id = args[0];
|
|
42814
|
-
if (!id)
|
|
42815
|
-
|
|
43114
|
+
if (!id) {
|
|
43115
|
+
return {
|
|
43116
|
+
data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pendings))),
|
|
43117
|
+
render: (data, mode) => Array.isArray(data) ? data.length === 0 ? "No bank links waiting to be mapped." : data.map((p) => renderPending(p, mode)).join(`
|
|
43118
|
+
|
|
43119
|
+
`) : renderRecord(data, mode)
|
|
43120
|
+
};
|
|
43121
|
+
}
|
|
42816
43122
|
return {
|
|
42817
43123
|
data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pending, { id }))),
|
|
42818
43124
|
render: renderPending
|
|
@@ -42827,6 +43133,32 @@ var CONNECTION_HANDLERS = {
|
|
|
42827
43133
|
`) || "Linked."
|
|
42828
43134
|
};
|
|
42829
43135
|
},
|
|
43136
|
+
"money kraken-link status": async ({ requireClient }) => ({
|
|
43137
|
+
data: await guarded([], () => requireClient().get(pathOf(krakenLinkOperations.status))),
|
|
43138
|
+
render: renderKraken
|
|
43139
|
+
}),
|
|
43140
|
+
"money kraken-link set": async ({ io, flags, requireClient }) => {
|
|
43141
|
+
refuseSecretFlags(flags, ["key", "api-key", "secret", "api-secret", "private-key"], "--key-file <path>");
|
|
43142
|
+
const text = await readSecret(io, flags, "key-file", "the Kraken key", "all");
|
|
43143
|
+
const { apiKey, apiSecret } = parseKrakenKey(text);
|
|
43144
|
+
const account = flagString(flags, "account")?.trim();
|
|
43145
|
+
const body = { apiKey, apiSecret, ...account ? { accountId: account } : {} };
|
|
43146
|
+
return {
|
|
43147
|
+
data: await guarded([apiKey, apiSecret], () => requireClient().put(pathOf(krakenLinkOperations.set), body)),
|
|
43148
|
+
render: renderKraken
|
|
43149
|
+
};
|
|
43150
|
+
},
|
|
43151
|
+
"money kraken-link sync": async ({ requireClient }) => ({
|
|
43152
|
+
data: await guarded([], () => requireClient().post(pathOf(krakenLinkOperations.sync), {})),
|
|
43153
|
+
render: renderKraken
|
|
43154
|
+
}),
|
|
43155
|
+
"money kraken-link remove": async ({ flags, requireClient }) => {
|
|
43156
|
+
assertDestructive(flags, "forget the Kraken key");
|
|
43157
|
+
return {
|
|
43158
|
+
data: await guarded([], () => requireClient().delete(pathOf(krakenLinkOperations.remove))),
|
|
43159
|
+
render: renderKraken
|
|
43160
|
+
};
|
|
43161
|
+
},
|
|
42830
43162
|
"doccle connect": (ctx) => connectDoccle(ctx),
|
|
42831
43163
|
"steam link": async ({ io, flags, requireClient }) => {
|
|
42832
43164
|
refuseSecretFlags(flags, ["key", "steam-key", "web-api-key"], "--key-file <path>");
|
|
@@ -43003,7 +43335,8 @@ function renderPending(data, mode) {
|
|
|
43003
43335
|
const p = data;
|
|
43004
43336
|
if (!Array.isArray(p?.accounts))
|
|
43005
43337
|
return renderRecord(data, mode);
|
|
43006
|
-
const
|
|
43338
|
+
const until = p.expiresAt ? ` \xB7 map before ${p.expiresAt}` : "";
|
|
43339
|
+
const lines = [`${p.bank ?? "Bank"} \xB7 pending ${p.pendingId}${until}`];
|
|
43007
43340
|
for (const a of p.accounts) {
|
|
43008
43341
|
const label = [a.name, a.ibanMasked].filter(Boolean).join(" ") || "account";
|
|
43009
43342
|
const hint = a.suggestedAccountId ? ` \u2192 ${a.suggestedAccountId}` : "";
|
|
@@ -43012,6 +43345,48 @@ function renderPending(data, mode) {
|
|
|
43012
43345
|
return lines.join(`
|
|
43013
43346
|
`);
|
|
43014
43347
|
}
|
|
43348
|
+
function parseKrakenKey(text) {
|
|
43349
|
+
const hint = "the key file should hold the API key and its private key: two lines, or JSON with apiKey and apiSecret";
|
|
43350
|
+
if (text.startsWith("{")) {
|
|
43351
|
+
let parsed;
|
|
43352
|
+
try {
|
|
43353
|
+
parsed = JSON.parse(text);
|
|
43354
|
+
} catch {
|
|
43355
|
+
throw usageError(hint);
|
|
43356
|
+
}
|
|
43357
|
+
const o = parsed ?? {};
|
|
43358
|
+
const apiKey = o.apiKey ?? o.key;
|
|
43359
|
+
const apiSecret = o.apiSecret ?? o.secret ?? o.privateKey;
|
|
43360
|
+
if (typeof apiKey !== "string" || typeof apiSecret !== "string")
|
|
43361
|
+
throw usageError(hint);
|
|
43362
|
+
return { apiKey: apiKey.trim(), apiSecret: apiSecret.trim() };
|
|
43363
|
+
}
|
|
43364
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
43365
|
+
if (lines.length !== 2)
|
|
43366
|
+
throw usageError(hint);
|
|
43367
|
+
return { apiKey: lines[0], apiSecret: lines[1] };
|
|
43368
|
+
}
|
|
43369
|
+
function renderKraken(data, mode) {
|
|
43370
|
+
const k = data;
|
|
43371
|
+
if (typeof k?.configured !== "boolean")
|
|
43372
|
+
return renderRecord(data, mode);
|
|
43373
|
+
if (!k.link)
|
|
43374
|
+
return k.configured ? "Kraken key set, not linked yet." : "No Kraken key set.";
|
|
43375
|
+
const synced = k.link.lastSyncAt ? ` \xB7 synced ${k.link.lastSyncAt}` : " \xB7 not synced yet";
|
|
43376
|
+
const lines = [`Kraken \u2192 ${k.link.accountTitle} \xB7 ${k.link.status}${synced}`];
|
|
43377
|
+
if (k.link.lastError)
|
|
43378
|
+
lines.push(` ${k.link.lastError}`);
|
|
43379
|
+
if (k.holdings) {
|
|
43380
|
+
lines.push(`Holdings at ${k.holdings.at} \xB7 \u20AC ${k.holdings.totalEur}`);
|
|
43381
|
+
for (const h of k.holdings.holdings) {
|
|
43382
|
+
const value = h.valueEur === null ? "no EUR price" : `\u20AC ${h.valueEur}`;
|
|
43383
|
+
const share = h.share === null ? "" : ` \xB7 ${(h.share * 100).toFixed(1)}%`;
|
|
43384
|
+
lines.push(` ${h.asset.padEnd(6)} ${h.amount} ${value}${share}`);
|
|
43385
|
+
}
|
|
43386
|
+
}
|
|
43387
|
+
return lines.join(`
|
|
43388
|
+
`);
|
|
43389
|
+
}
|
|
43015
43390
|
var commands10 = pairCommands(CONNECTION_COMMAND_SPECS, CONNECTION_HANDLERS);
|
|
43016
43391
|
|
|
43017
43392
|
// src/commands/contacts.ts
|
|
@@ -47811,6 +48186,14 @@ var commands30 = [
|
|
|
47811
48186
|
// src/commands/money.ts
|
|
47812
48187
|
import { readFileSync as readFileSync8 } from "fs";
|
|
47813
48188
|
import { basename as basename5 } from "path";
|
|
48189
|
+
function renderDedupe(data) {
|
|
48190
|
+
const r = data;
|
|
48191
|
+
const lines = r.pairs.map((p) => `${p.bookedOn} ${p.amount.padStart(10)} ${p.description ?? p.counterpartyName ?? ""} keep ${p.keepId} twin ${p.twinId} (${p.twinBookedOn})${p.ambiguous ? " ambiguous: sort by hand" : ""}`);
|
|
48192
|
+
const c = r.changed;
|
|
48193
|
+
lines.push(`${r.counts.pairs} pairs, ${r.counts.ambiguous} ambiguous`, r.committed ? `merged ${c.merged} \xB7 links ${c.links} \xB7 splits ${c.splits} \xB7 settlements ${c.settlements} \xB7 transfers ${c.transfers} \xB7 rules ${c.rules} \xB7 notes ${c.notes}` : "preview: nothing changed (--commit folds the unambiguous pairs)");
|
|
48194
|
+
return lines.join(`
|
|
48195
|
+
`);
|
|
48196
|
+
}
|
|
47814
48197
|
function partyFlag(flags) {
|
|
47815
48198
|
const userId = flagString(flags, "with-user");
|
|
47816
48199
|
const contactId = flagString(flags, "with-contact");
|
|
@@ -47871,13 +48254,34 @@ var commands31 = [
|
|
|
47871
48254
|
defineCommand({
|
|
47872
48255
|
operation: moneyOperations.updateAccount,
|
|
47873
48256
|
path: ["money", "account-update"],
|
|
47874
|
-
summary: "Edit a money account.",
|
|
48257
|
+
summary: "Edit a money account, or state its balance (for an export with no balance column).",
|
|
47875
48258
|
args: [{ name: "id", required: true, description: "Account ULID." }],
|
|
47876
|
-
flags: [
|
|
48259
|
+
flags: [
|
|
48260
|
+
{ name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." },
|
|
48261
|
+
{
|
|
48262
|
+
name: "balance",
|
|
48263
|
+
type: "string",
|
|
48264
|
+
description: "What the account holds, e.g. 1234.56 (sets balance)."
|
|
48265
|
+
},
|
|
48266
|
+
{
|
|
48267
|
+
name: "balance-at",
|
|
48268
|
+
type: "string",
|
|
48269
|
+
description: "The day (YYYY-MM-DD, its end) or moment that balance is from (default today)."
|
|
48270
|
+
}
|
|
48271
|
+
],
|
|
47877
48272
|
returns: "MoneyAccountView",
|
|
47878
|
-
|
|
48273
|
+
notes: ". `--balance 1234.56 --balance-at 2026-10-27` anchors an account whose bank export has no running balance (ING's): net worth counts from it, back and forward through the ledger. A negative one rides as `--balance=-20`. A day ahead is refused, and so is an account a bank link keeps. `--data` takes the rest (title, kind, iban, includeInNetWorth, \u2026).",
|
|
48274
|
+
run: async ({ args, flags, io, requireClient }) => {
|
|
47879
48275
|
const id = requireArg(args, 0, "account id");
|
|
47880
|
-
const
|
|
48276
|
+
const balance = flagString(flags, "balance");
|
|
48277
|
+
const balanceAt = flagString(flags, "balance-at");
|
|
48278
|
+
if (balanceAt !== undefined && balance === undefined) {
|
|
48279
|
+
throw usageError("--balance-at goes with --balance");
|
|
48280
|
+
}
|
|
48281
|
+
const body = parseInput(updateMoneyAccountInputSchema, {
|
|
48282
|
+
...readDataFlag(flags),
|
|
48283
|
+
...balance !== undefined ? { balance, balanceAt: balanceAt ?? isoDay(io.now()) } : {}
|
|
48284
|
+
});
|
|
47881
48285
|
return {
|
|
47882
48286
|
data: await requireClient().patch(pathOf(moneyOperations.updateAccount, { id }), body),
|
|
47883
48287
|
render: renderRecord
|
|
@@ -47930,6 +48334,32 @@ var commands31 = [
|
|
|
47930
48334
|
};
|
|
47931
48335
|
}
|
|
47932
48336
|
}),
|
|
48337
|
+
defineCommand({
|
|
48338
|
+
operation: moneyOperations.dedupe,
|
|
48339
|
+
path: ["money", "dedupe"],
|
|
48340
|
+
summary: "Find movements booked twice on an account (a statement's row and a bank link's twin); --commit folds them.",
|
|
48341
|
+
args: [],
|
|
48342
|
+
flags: [
|
|
48343
|
+
{ name: "account", type: "string", description: "The account ULID (required)." },
|
|
48344
|
+
{
|
|
48345
|
+
name: "commit",
|
|
48346
|
+
type: "boolean",
|
|
48347
|
+
description: "Fold the pairs; without it, a preview that writes nothing."
|
|
48348
|
+
}
|
|
48349
|
+
],
|
|
48350
|
+
returns: "MoneyDedupeResult",
|
|
48351
|
+
notes: ". Pairs by the bank sync's rule: same amount, booked a day apart at most, the most alike when several fit. A commit keeps the statement's row (category, review status, notes, links, splits, transfer), gives it the bank's entry id, moves what pointed at the twin onto it and deletes the twin. An `ambiguous` pair is never committed. Running it twice changes nothing the second time.",
|
|
48352
|
+
run: async ({ flags, requireClient }) => {
|
|
48353
|
+
const body = parseInput(moneyDedupeInputSchema, {
|
|
48354
|
+
accountId: requireFlag(flags, "account", "say which account: --account <id>"),
|
|
48355
|
+
...flagBool(flags, "commit") ? { commit: true } : {}
|
|
48356
|
+
});
|
|
48357
|
+
return {
|
|
48358
|
+
data: await requireClient().post(pathOf(moneyOperations.dedupe), body),
|
|
48359
|
+
render: renderDedupe
|
|
48360
|
+
};
|
|
48361
|
+
}
|
|
48362
|
+
}),
|
|
47933
48363
|
defineCommand({
|
|
47934
48364
|
operation: moneyOperations.listConnectionSyncs,
|
|
47935
48365
|
path: ["money", "connection-syncs"],
|
|
@@ -47990,9 +48420,9 @@ var commands31 = [
|
|
|
47990
48420
|
{ name: "cursor", type: "string", description: "The previous page's nextCursor." }
|
|
47991
48421
|
],
|
|
47992
48422
|
returns: "{ data: MoneyTransactionView[], nextCursor }",
|
|
47993
|
-
notes: ". Amounts are signed decimal strings. `--business` keeps rows with a business share; `--business-status` and `--tax-tag` narrow further.",
|
|
48423
|
+
notes: ". Amounts are signed decimal strings. `--business` keeps rows with a business share; `--business-status` and `--tax-tag` narrow further. A page is 100 rows at most: while `nextCursor` isn't null, pass it back as `--cursor` for the next.",
|
|
47994
48424
|
run: async ({ flags, requireClient }) => ({
|
|
47995
|
-
data: await requireClient().
|
|
48425
|
+
data: await requireClient().getPage(pathOf(moneyOperations.listTransactions), {
|
|
47996
48426
|
query: {
|
|
47997
48427
|
accountId: flagString(flags, "account"),
|
|
47998
48428
|
status: flagString(flags, "status"),
|
|
@@ -48456,12 +48886,22 @@ var commands31 = [
|
|
|
48456
48886
|
flags: [
|
|
48457
48887
|
{ name: "account", type: "string", description: "The account's ULID." },
|
|
48458
48888
|
{ name: "since", type: "string", description: "First day to take (default 2025-01-01)." },
|
|
48459
|
-
{ name: "commit", type: "boolean", description: "Book the new lines (else a preview)." }
|
|
48889
|
+
{ name: "commit", type: "boolean", description: "Book the new lines (else a preview)." },
|
|
48890
|
+
{
|
|
48891
|
+
name: "balance",
|
|
48892
|
+
type: "string",
|
|
48893
|
+
description: "What the account holds, for an export with no balance column (anchors it with --commit)."
|
|
48894
|
+
},
|
|
48895
|
+
{
|
|
48896
|
+
name: "balance-at",
|
|
48897
|
+
type: "string",
|
|
48898
|
+
description: "The day (YYYY-MM-DD, its end) or moment --balance is from (default today)."
|
|
48899
|
+
}
|
|
48460
48900
|
],
|
|
48461
48901
|
scopes: [...moneyOperations.previewImport.scopes, ...moneyOperations.commitImport.scopes],
|
|
48462
48902
|
returns: "MoneyImportPreview | MoneyImportResult",
|
|
48463
|
-
notes: `${routeOf(moneyOperations.previewImport)}, or ${routeOf(moneyOperations.commitImport)} with --commit (\`money_import_file\`). A KBC Touch export reads as is (\`;\`, comma decimals, UTF-8 or Windows-1252), and so does a CODA file. Each line is new, duplicate, before_window, other_account or other_currency; a line at exactly 0,00 (nothing moved, e.g. ING's empty SEPA lines) is left out and listed under \`skipped\`, counted as \`counts.nothing_moved\`, never a problem; re-importing books nothing twice. On a credit_card account the file is the card's statement: its CSV export (KBC's credit card CSV reads as is; a transaction cost is its own row) or the statement PDF's text (\`cardStatement\` says which, the total and the debit day); paying it off is a transfer, never spending twice.`,
|
|
48464
|
-
run: async ({ args, flags, requireClient }) => {
|
|
48903
|
+
notes: `${routeOf(moneyOperations.previewImport)}, or ${routeOf(moneyOperations.commitImport)} with --commit (\`money_import_file\`). A KBC Touch export reads as is (\`;\`, comma decimals, UTF-8 or Windows-1252), and so does a CODA file. Each line is new, duplicate, before_window, other_account or other_currency; a line at exactly 0,00 (nothing moved, e.g. ING's empty SEPA lines) is left out and listed under \`skipped\`, counted as \`counts.nothing_moved\`, never a problem; re-importing books nothing twice. On a credit_card account the file is the card's statement: its CSV export (KBC's credit card CSV reads as is; a transaction cost is its own row) or the statement PDF's text (\`cardStatement\` says which, the total and the debit day); paying it off is a transfer, never spending twice. A bank whose export has no running balance (ING's) leaves the account's balance unknown: \`--balance 1234.56 [--balance-at YYYY-MM-DD]\` with --commit states it, as \`do money account-update --balance\` does (a preview ignores it).`,
|
|
48904
|
+
run: async ({ args, flags, io, requireClient }) => {
|
|
48465
48905
|
const path = requireArg(args, 0, "path to the statement CSV");
|
|
48466
48906
|
const account = requireFlag(flags, "account", "money import needs --account <account id>");
|
|
48467
48907
|
let bytes;
|
|
@@ -48475,6 +48915,19 @@ var commands31 = [
|
|
|
48475
48915
|
const since = flagString(flags, "since");
|
|
48476
48916
|
if (since !== undefined)
|
|
48477
48917
|
form.append("since", since);
|
|
48918
|
+
const balance = flagString(flags, "balance");
|
|
48919
|
+
const balanceAt = flagString(flags, "balance-at");
|
|
48920
|
+
if (balanceAt !== undefined && balance === undefined) {
|
|
48921
|
+
throw usageError("--balance-at goes with --balance");
|
|
48922
|
+
}
|
|
48923
|
+
if (balance !== undefined) {
|
|
48924
|
+
const anchor = parseInput(updateMoneyAccountInputSchema, {
|
|
48925
|
+
balance,
|
|
48926
|
+
balanceAt: balanceAt ?? isoDay(io.now())
|
|
48927
|
+
});
|
|
48928
|
+
form.append("balance", anchor.balance);
|
|
48929
|
+
form.append("balanceAt", anchor.balanceAt);
|
|
48930
|
+
}
|
|
48478
48931
|
const name = basename5(path);
|
|
48479
48932
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/csv" }), name);
|
|
48480
48933
|
const op = flagBool(flags, "commit") ? moneyOperations.commitImport : moneyOperations.previewImport;
|
|
@@ -48623,7 +49076,7 @@ var commands31 = [
|
|
|
48623
49076
|
}
|
|
48624
49077
|
],
|
|
48625
49078
|
returns: "MoneyNetWorth",
|
|
48626
|
-
notes: ". total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed.",
|
|
49079
|
+
notes: ". total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed. `complete: false` when an account counts but its balance isn't known (a bank export with no running balance): it's listed under `unknown`, left out of the total, never counted as 0; `do money account-update <id> --balance \u2026 --balance-at \u2026` states it.",
|
|
48627
49080
|
run: async ({ flags, requireClient }) => ({
|
|
48628
49081
|
data: await requireClient().get(pathOf(moneyOperations.netWorthRead), {
|
|
48629
49082
|
query: { scope: flagString(flags, "scope"), months: flagString(flags, "months") }
|
package/package.json
CHANGED