@robbeverhelst/do 1.332.0 → 1.334.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 +977 -68
- package/package.json +1 -1
package/dist/do.js
CHANGED
|
@@ -983,6 +983,10 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
|
|
|
983
983
|
const positionals = [];
|
|
984
984
|
const flags = {};
|
|
985
985
|
let positionalOnly = false;
|
|
986
|
+
const setValue = (name, value) => {
|
|
987
|
+
const was = flags[name];
|
|
988
|
+
flags[name] = typeof was === "string" ? [was, value] : Array.isArray(was) ? [...was, value] : value;
|
|
989
|
+
};
|
|
986
990
|
for (let i = 0;i < argv.length; i++) {
|
|
987
991
|
const token = argv[i];
|
|
988
992
|
if (token === undefined)
|
|
@@ -999,7 +1003,7 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
|
|
|
999
1003
|
const body = token.slice(2);
|
|
1000
1004
|
const eq = body.indexOf("=");
|
|
1001
1005
|
if (eq !== -1) {
|
|
1002
|
-
|
|
1006
|
+
setValue(body.slice(0, eq), body.slice(eq + 1));
|
|
1003
1007
|
continue;
|
|
1004
1008
|
}
|
|
1005
1009
|
if (booleans.has(body)) {
|
|
@@ -1008,7 +1012,7 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
|
|
|
1008
1012
|
}
|
|
1009
1013
|
const next = argv[i + 1];
|
|
1010
1014
|
if (next !== undefined && (next === "-" || !next.startsWith("-"))) {
|
|
1011
|
-
|
|
1015
|
+
setValue(body, next);
|
|
1012
1016
|
i++;
|
|
1013
1017
|
} else {
|
|
1014
1018
|
flags[body] = true;
|
|
@@ -1030,7 +1034,11 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
|
|
|
1030
1034
|
}
|
|
1031
1035
|
function flagString(flags, name) {
|
|
1032
1036
|
const v = flags[name];
|
|
1033
|
-
return typeof v === "string" ? v : undefined;
|
|
1037
|
+
return typeof v === "string" ? v : Array.isArray(v) ? v.at(-1) : undefined;
|
|
1038
|
+
}
|
|
1039
|
+
function flagStrings(flags, name) {
|
|
1040
|
+
const v = flags[name];
|
|
1041
|
+
return typeof v === "string" ? [v] : Array.isArray(v) ? v : [];
|
|
1034
1042
|
}
|
|
1035
1043
|
function flagBool(flags, name) {
|
|
1036
1044
|
return flags[name] === true;
|
|
@@ -14410,7 +14418,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
|
|
|
14410
14418
|
|
|
14411
14419
|
// src/meta.ts
|
|
14412
14420
|
var CLI_NAME = "do";
|
|
14413
|
-
var VERSION = "1.
|
|
14421
|
+
var VERSION = "1.334.0";
|
|
14414
14422
|
var API_PREFIX = "/api/v1";
|
|
14415
14423
|
var DEFAULT_API_URL = "http://localhost:3001";
|
|
14416
14424
|
|
|
@@ -17867,6 +17875,7 @@ var automationRuns = pgTable("automation_runs", {
|
|
|
17867
17875
|
automationId: text("automation_id").notNull().references(() => automations.id),
|
|
17868
17876
|
eventType: text("event_type").notNull(),
|
|
17869
17877
|
eventEntityId: text("event_entity_id").notNull(),
|
|
17878
|
+
eventValue: jsonb("event_value"),
|
|
17870
17879
|
status: automationRunStatus("status").notNull(),
|
|
17871
17880
|
detail: text("detail"),
|
|
17872
17881
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
@@ -18355,6 +18364,7 @@ var userSettings = pgTable("user_settings", {
|
|
|
18355
18364
|
foodDiaryMode: boolean4("food_diary_mode").notNull().default(false),
|
|
18356
18365
|
calendarZoom: integer2("calendar_zoom"),
|
|
18357
18366
|
lifeBlockSharing: jsonb("life_block_sharing").$type(),
|
|
18367
|
+
recentSearches: jsonb("recent_searches").$type().notNull().default([]),
|
|
18358
18368
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
18359
18369
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
18360
18370
|
}, (t) => [unique("user_settings_tenant_user_uniq").on(t.tenantId, t.userId)]);
|
|
@@ -19955,9 +19965,29 @@ var animalLostCards = pgTable("animal_lost_cards", {
|
|
|
19955
19965
|
phone: text("phone"),
|
|
19956
19966
|
showChipped: boolean4("show_chipped").notNull().default(false),
|
|
19957
19967
|
note: text("note"),
|
|
19968
|
+
description: text("description"),
|
|
19969
|
+
answersTo: text("answers_to"),
|
|
19970
|
+
missingSince: timestamp("missing_since", { withTimezone: true }),
|
|
19971
|
+
missingWhere: text("missing_where"),
|
|
19958
19972
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
19959
19973
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
19960
19974
|
}, (t) => [index("animal_lost_cards_tenant_idx").on(t.tenantId)]);
|
|
19975
|
+
var animalLostReportFrom = pgEnum("animal_lost_report_from", ["owner", "finder"]);
|
|
19976
|
+
var animalLostReports = pgTable("animal_lost_reports", {
|
|
19977
|
+
id: text("id").primaryKey(),
|
|
19978
|
+
tenantId: text("tenant_id").notNull().references(() => tenants.id),
|
|
19979
|
+
animalId: text("animal_id").notNull().references(() => animals.id, { onDelete: "cascade" }),
|
|
19980
|
+
from: animalLostReportFrom("from").notNull(),
|
|
19981
|
+
at: timestamp("at", { withTimezone: true }).notNull(),
|
|
19982
|
+
title: text("title"),
|
|
19983
|
+
note: text("note"),
|
|
19984
|
+
place: text("place"),
|
|
19985
|
+
lat: doublePrecision("lat"),
|
|
19986
|
+
lng: doublePrecision("lng"),
|
|
19987
|
+
phone: text("phone"),
|
|
19988
|
+
photoFileId: text("photo_file_id"),
|
|
19989
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
19990
|
+
}, (t) => [index("animal_lost_reports_animal_idx").on(t.tenantId, t.animalId, t.at)]);
|
|
19961
19991
|
var animalHandoverLinks = pgTable("animal_handover_links", {
|
|
19962
19992
|
id: text("id").primaryKey(),
|
|
19963
19993
|
tenantId: text("tenant_id").notNull().references(() => tenants.id),
|
|
@@ -20403,7 +20433,8 @@ var animalDayCountSchema = object({ day: string2(), count: number2().int() });
|
|
|
20403
20433
|
var animalWeightPointSchema = object({
|
|
20404
20434
|
day: string2(),
|
|
20405
20435
|
grams: number2(),
|
|
20406
|
-
at: exports_coerce.date()
|
|
20436
|
+
at: exports_coerce.date(),
|
|
20437
|
+
note: string2().nullish()
|
|
20407
20438
|
});
|
|
20408
20439
|
var animalFileSchema = object({
|
|
20409
20440
|
id: ulidSchema,
|
|
@@ -20524,6 +20555,7 @@ var animalLogInputSchema = discriminatedUnion("kind", [
|
|
|
20524
20555
|
kind: literal("weight"),
|
|
20525
20556
|
value: number2().positive().max(200000),
|
|
20526
20557
|
unit: _enum2(["g", "kg"]).default("g"),
|
|
20558
|
+
note: string2().trim().min(1).max(120).optional(),
|
|
20527
20559
|
occurredAt: occurredAtSchema
|
|
20528
20560
|
}).strict(),
|
|
20529
20561
|
object({
|
|
@@ -20769,22 +20801,72 @@ var animalSitterNoteInputSchema = object({
|
|
|
20769
20801
|
body: string2().trim().max(1000).default("")
|
|
20770
20802
|
}).strict();
|
|
20771
20803
|
var animalSitterPhoneSchema = object({ phone: string2() });
|
|
20804
|
+
var lostCardPhoneSchema = string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -");
|
|
20772
20805
|
var animalLostCardInputSchema = object({
|
|
20773
20806
|
photoFileId: ulidSchema.nullish(),
|
|
20774
|
-
phone:
|
|
20807
|
+
phone: lostCardPhoneSchema.nullish(),
|
|
20775
20808
|
showChipped: boolean2().optional(),
|
|
20776
|
-
note: string2().trim().min(1).max(200).nullish()
|
|
20809
|
+
note: string2().trim().min(1).max(200).nullish(),
|
|
20810
|
+
description: string2().trim().min(1).max(400).nullish(),
|
|
20811
|
+
answersTo: string2().trim().min(1).max(120).nullish(),
|
|
20812
|
+
missingSince: exports_coerce.date().nullish(),
|
|
20813
|
+
missingWhere: string2().trim().min(1).max(200).nullish()
|
|
20777
20814
|
}).strict();
|
|
20778
20815
|
var animalLostCardPhotoSchema = object({ id: ulidSchema, name: string2() });
|
|
20816
|
+
var animalLostUpdateSchema = object({
|
|
20817
|
+
id: ulidSchema,
|
|
20818
|
+
at: exports_coerce.date(),
|
|
20819
|
+
title: string2(),
|
|
20820
|
+
note: string2().nullable(),
|
|
20821
|
+
place: string2().nullable()
|
|
20822
|
+
});
|
|
20779
20823
|
var animalLostCardPublicSchema = object({
|
|
20780
20824
|
name: string2(),
|
|
20781
20825
|
speciesKey: string2().nullable(),
|
|
20782
20826
|
kind: string2().nullable(),
|
|
20783
20827
|
missing: boolean2(),
|
|
20784
20828
|
hasPhoto: boolean2(),
|
|
20785
|
-
|
|
20829
|
+
phoneMasked: string2().nullable(),
|
|
20786
20830
|
chipped: boolean2(),
|
|
20787
|
-
note: string2().nullable()
|
|
20831
|
+
note: string2().nullable(),
|
|
20832
|
+
description: string2().nullable(),
|
|
20833
|
+
answersTo: string2().nullable(),
|
|
20834
|
+
missingSince: exports_coerce.date().nullable(),
|
|
20835
|
+
missingWhere: string2().nullable(),
|
|
20836
|
+
pronoun: _enum2(["he", "she", "they"]),
|
|
20837
|
+
sharedBy: string2().nullable(),
|
|
20838
|
+
owners: array(string2()),
|
|
20839
|
+
vet: string2().nullable(),
|
|
20840
|
+
updates: array(animalLostUpdateSchema)
|
|
20841
|
+
});
|
|
20842
|
+
var animalLostCardCallSchema = object({ phone: string2() });
|
|
20843
|
+
var blankless = (v) => typeof v === "string" && v.trim() === "" ? undefined : v;
|
|
20844
|
+
var animalLostFoundInputSchema = object({
|
|
20845
|
+
where: string2().trim().min(1).max(300),
|
|
20846
|
+
lat: preprocess(blankless, exports_coerce.number().min(-90).max(90).optional()),
|
|
20847
|
+
lng: preprocess(blankless, exports_coerce.number().min(-180).max(180).optional()),
|
|
20848
|
+
phone: preprocess(blankless, lostCardPhoneSchema.optional())
|
|
20849
|
+
}).strict().refine((v) => v.lat === undefined === (v.lng === undefined), {
|
|
20850
|
+
message: "Send both lat and lng, or neither."
|
|
20851
|
+
});
|
|
20852
|
+
var animalLostFoundSentSchema = object({ to: array(string2()) });
|
|
20853
|
+
var animalLostUpdateInputSchema = object({
|
|
20854
|
+
at: exports_coerce.date(),
|
|
20855
|
+
title: string2().trim().min(1).max(120),
|
|
20856
|
+
note: string2().trim().min(1).max(200).nullish(),
|
|
20857
|
+
place: string2().trim().min(1).max(200).nullish()
|
|
20858
|
+
}).strict();
|
|
20859
|
+
var animalLostReportSchema = object({
|
|
20860
|
+
id: ulidSchema,
|
|
20861
|
+
from: _enum2(["owner", "finder"]),
|
|
20862
|
+
at: exports_coerce.date(),
|
|
20863
|
+
title: string2().nullable(),
|
|
20864
|
+
note: string2().nullable(),
|
|
20865
|
+
place: string2().nullable(),
|
|
20866
|
+
lat: number2().nullable(),
|
|
20867
|
+
lng: number2().nullable(),
|
|
20868
|
+
phone: string2().nullable(),
|
|
20869
|
+
photoFileId: ulidSchema.nullable()
|
|
20788
20870
|
});
|
|
20789
20871
|
var animalLostCardStateSchema = object({
|
|
20790
20872
|
card: object({
|
|
@@ -20793,7 +20875,12 @@ var animalLostCardStateSchema = object({
|
|
|
20793
20875
|
phone: string2().nullable(),
|
|
20794
20876
|
showChipped: boolean2(),
|
|
20795
20877
|
note: string2().nullable(),
|
|
20878
|
+
description: string2().nullable(),
|
|
20879
|
+
answersTo: string2().nullable(),
|
|
20880
|
+
missingSince: exports_coerce.date().nullable(),
|
|
20881
|
+
missingWhere: string2().nullable(),
|
|
20796
20882
|
since: exports_coerce.date(),
|
|
20883
|
+
reports: array(animalLostReportSchema),
|
|
20797
20884
|
preview: animalLostCardPublicSchema
|
|
20798
20885
|
}).nullable(),
|
|
20799
20886
|
photos: array(animalLostCardPhotoSchema),
|
|
@@ -21044,7 +21131,7 @@ var animalOperations = defineOperations({
|
|
|
21044
21131
|
scopes: READ,
|
|
21045
21132
|
tag: "Animals",
|
|
21046
21133
|
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.",
|
|
21134
|
+
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
21135
|
status: 200,
|
|
21049
21136
|
response: { description: "The card's state." },
|
|
21050
21137
|
errors: { 404: true }
|
|
@@ -21736,6 +21823,8 @@ var taskOwnerSchema = actorRefSchema.extend({ desk: string2().nullable() });
|
|
|
21736
21823
|
var taskFilingSuggestionSchema = object({
|
|
21737
21824
|
areaId: ulidSchema.nullable(),
|
|
21738
21825
|
area: string2().nullable(),
|
|
21826
|
+
projectId: ulidSchema.nullable().default(null),
|
|
21827
|
+
project: string2().nullable().default(null),
|
|
21739
21828
|
owner: taskOwnerSchema.nullable(),
|
|
21740
21829
|
scheduledAt: exports_coerce.date().nullable(),
|
|
21741
21830
|
by: actorRefSchema
|
|
@@ -21952,13 +22041,25 @@ var occurrenceSchema = createSelectSchema(occurrences, {
|
|
|
21952
22041
|
completedAt: exports_coerce.date().nullable()
|
|
21953
22042
|
});
|
|
21954
22043
|
var byDayCodeSchema = _enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]);
|
|
22044
|
+
var byDayOrdinalSchema = _enum2(["1", "2", "3", "4", "-1"]);
|
|
22045
|
+
var byDaySchema = union([
|
|
22046
|
+
byDayCodeSchema,
|
|
22047
|
+
templateLiteral([byDayOrdinalSchema, byDayCodeSchema])
|
|
22048
|
+
]);
|
|
22049
|
+
var BY_DAY_RE = /^(-1|[1-4])?(MO|TU|WE|TH|FR|SA|SU)$/;
|
|
22050
|
+
function parseByDay(code) {
|
|
22051
|
+
const m = BY_DAY_RE.exec(code);
|
|
22052
|
+
if (!m)
|
|
22053
|
+
return null;
|
|
22054
|
+
return { ordinal: m[1] ? Number(m[1]) : null, weekday: m[2] };
|
|
22055
|
+
}
|
|
21955
22056
|
var recurrenceRuleParams = {
|
|
21956
22057
|
timezone: string2().min(1).default("UTC"),
|
|
21957
22058
|
interval: number2().int().positive().default(1),
|
|
21958
22059
|
mode: recurrenceModeSchema,
|
|
21959
22060
|
timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "time-of-day must be HH:MM (24h)").nullish(),
|
|
21960
22061
|
freq: recurrenceFreqSchema.optional(),
|
|
21961
|
-
byDay: array(
|
|
22062
|
+
byDay: array(byDaySchema).optional(),
|
|
21962
22063
|
until: exports_coerce.date().optional(),
|
|
21963
22064
|
count: number2().int().positive().optional(),
|
|
21964
22065
|
unit: recurrenceUnitSchema.optional(),
|
|
@@ -21968,6 +22069,14 @@ function refineRecurrenceRule(v, ctx) {
|
|
|
21968
22069
|
if (v.mode === "rigid" && !v.freq) {
|
|
21969
22070
|
ctx.addIssue({ code: "custom", path: ["freq"], message: "rigid rules require a freq" });
|
|
21970
22071
|
}
|
|
22072
|
+
const ordinal = v.byDay?.findIndex((code) => parseByDay(code)?.ordinal != null) ?? -1;
|
|
22073
|
+
if (ordinal >= 0 && v.freq !== "monthly" && v.freq !== "yearly") {
|
|
22074
|
+
ctx.addIssue({
|
|
22075
|
+
code: "custom",
|
|
22076
|
+
path: ["byDay", ordinal],
|
|
22077
|
+
message: "an ordinal weekday (1SU, -1FR) needs a monthly or yearly freq"
|
|
22078
|
+
});
|
|
22079
|
+
}
|
|
21971
22080
|
if (v.mode === "flexible") {
|
|
21972
22081
|
if (!v.unit) {
|
|
21973
22082
|
ctx.addIssue({ code: "custom", path: ["unit"], message: "flexible rules require a unit" });
|
|
@@ -22359,6 +22468,8 @@ var collectionEntries = pgTable("collection_entries", {
|
|
|
22359
22468
|
fileIds: text("file_ids").array().notNull().default([]),
|
|
22360
22469
|
minutes: integer2("minutes"),
|
|
22361
22470
|
ops: jsonb("ops").$type().notNull().default([]),
|
|
22471
|
+
chapter: text("chapter"),
|
|
22472
|
+
linkedNoteId: text("linked_note_id").references(() => nodes.id, { onDelete: "set null" }),
|
|
22362
22473
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
22363
22474
|
}, (t) => [
|
|
22364
22475
|
index("collection_entries_item_occurred_idx").on(t.itemId, t.occurredOn),
|
|
@@ -22470,8 +22581,11 @@ var collectionEntrySchema = createSelectSchema(collectionEntries, {
|
|
|
22470
22581
|
taskId: string2().nullable().default(null),
|
|
22471
22582
|
fileIds: array(string2()).default([]),
|
|
22472
22583
|
minutes: number2().int().nullable().default(null),
|
|
22473
|
-
ops: array(unknown()).default([])
|
|
22584
|
+
ops: array(unknown()).default([]),
|
|
22585
|
+
chapter: string2().nullable().default(null),
|
|
22586
|
+
linkedNoteId: string2().nullable().default(null)
|
|
22474
22587
|
});
|
|
22588
|
+
var entryChapterSchema = string2().trim().min(1).max(60);
|
|
22475
22589
|
var entryCostSchema = number2().min(0).max(1e7).multipleOf(0.01);
|
|
22476
22590
|
var entryFileIdsSchema = array(ulidSchema).max(20);
|
|
22477
22591
|
var createCollectionEntryInputSchema = createInsertSchema(collectionEntries, {
|
|
@@ -22483,7 +22597,9 @@ var createCollectionEntryInputSchema = createInsertSchema(collectionEntries, {
|
|
|
22483
22597
|
cost: entryCostSchema.nullish(),
|
|
22484
22598
|
contactId: ulidSchema.nullish(),
|
|
22485
22599
|
taskId: ulidSchema.nullish(),
|
|
22486
|
-
fileIds: entryFileIdsSchema.optional()
|
|
22600
|
+
fileIds: entryFileIdsSchema.optional(),
|
|
22601
|
+
chapter: entryChapterSchema.nullish(),
|
|
22602
|
+
linkedNoteId: ulidSchema.nullish()
|
|
22487
22603
|
}).omit({ tenantId: true, itemId: true, createdAt: true, minutes: true, ops: true });
|
|
22488
22604
|
var updateCollectionEntryInputSchema = object({
|
|
22489
22605
|
occurredOn: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "must be an ISO date (YYYY-MM-DD)").optional(),
|
|
@@ -22492,7 +22608,9 @@ var updateCollectionEntryInputSchema = object({
|
|
|
22492
22608
|
kind: collectionEntryKindSchema.optional(),
|
|
22493
22609
|
cost: entryCostSchema.nullish(),
|
|
22494
22610
|
contactId: ulidSchema.nullish(),
|
|
22495
|
-
fileIds: entryFileIdsSchema.optional()
|
|
22611
|
+
fileIds: entryFileIdsSchema.optional(),
|
|
22612
|
+
chapter: entryChapterSchema.nullish(),
|
|
22613
|
+
linkedNoteId: ulidSchema.nullish()
|
|
22496
22614
|
}).strict();
|
|
22497
22615
|
var collectionItemViewSchema = collectionItemSchema.extend({
|
|
22498
22616
|
title: string2(),
|
|
@@ -22565,6 +22683,7 @@ var collectionLogInputSchema = object({
|
|
|
22565
22683
|
status: string2().trim().min(1).max(40).optional(),
|
|
22566
22684
|
rating: collectionRatingSchema.nullish(),
|
|
22567
22685
|
note: string2().max(2000).nullish(),
|
|
22686
|
+
chapter: entryChapterSchema.nullish(),
|
|
22568
22687
|
said: string2().trim().min(1).max(500).optional(),
|
|
22569
22688
|
occurredOn: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "must be an ISO date (YYYY-MM-DD)").optional()
|
|
22570
22689
|
}).strict();
|
|
@@ -23146,6 +23265,8 @@ var moneyBankPendingSchema = object({
|
|
|
23146
23265
|
pendingId: ulidSchema,
|
|
23147
23266
|
bank: string2(),
|
|
23148
23267
|
consentValidUntil: exports_coerce.date(),
|
|
23268
|
+
createdAt: exports_coerce.date(),
|
|
23269
|
+
expiresAt: exports_coerce.date(),
|
|
23149
23270
|
accounts: array(object({
|
|
23150
23271
|
index: number2().int().min(0),
|
|
23151
23272
|
name: string2().nullable(),
|
|
@@ -23181,6 +23302,33 @@ var moneyBankSyncResultSchema = object({
|
|
|
23181
23302
|
error: true
|
|
23182
23303
|
}).extend({ accountTitle: string2() }))
|
|
23183
23304
|
});
|
|
23305
|
+
var moneyDedupeInputSchema = object({ accountId: ulidSchema, commit: boolean2().optional() }).strict();
|
|
23306
|
+
var moneyDedupePairSchema = object({
|
|
23307
|
+
keepId: string2(),
|
|
23308
|
+
twinId: string2(),
|
|
23309
|
+
bookedOn: string2(),
|
|
23310
|
+
twinBookedOn: string2(),
|
|
23311
|
+
amount: string2(),
|
|
23312
|
+
description: string2().nullable(),
|
|
23313
|
+
counterpartyName: string2().nullable(),
|
|
23314
|
+
ambiguous: boolean2()
|
|
23315
|
+
});
|
|
23316
|
+
var moneyDedupeChangesSchema = object({
|
|
23317
|
+
merged: number2().int(),
|
|
23318
|
+
links: number2().int(),
|
|
23319
|
+
splits: number2().int(),
|
|
23320
|
+
settlements: number2().int(),
|
|
23321
|
+
transfers: number2().int(),
|
|
23322
|
+
rules: number2().int(),
|
|
23323
|
+
notes: number2().int()
|
|
23324
|
+
});
|
|
23325
|
+
var moneyDedupeResultSchema = object({
|
|
23326
|
+
accountId: string2(),
|
|
23327
|
+
committed: boolean2(),
|
|
23328
|
+
pairs: array(moneyDedupePairSchema),
|
|
23329
|
+
counts: object({ pairs: number2().int(), ambiguous: number2().int() }),
|
|
23330
|
+
changed: moneyDedupeChangesSchema
|
|
23331
|
+
});
|
|
23184
23332
|
|
|
23185
23333
|
// ../../packages/schema/src/operations/bank-link.ts
|
|
23186
23334
|
var CONNECT = ["connections:write"];
|
|
@@ -23263,6 +23411,18 @@ var bankLinkOperations = defineOperations({
|
|
|
23263
23411
|
response: { description: "The bank accounts to map." },
|
|
23264
23412
|
errors: { 400: true, 403: true, 409: true }
|
|
23265
23413
|
},
|
|
23414
|
+
pendings: {
|
|
23415
|
+
method: "get",
|
|
23416
|
+
path: "/money/bank-link/pending",
|
|
23417
|
+
scopes: CONNECT,
|
|
23418
|
+
afterScopes: AFTER,
|
|
23419
|
+
tag: "Money",
|
|
23420
|
+
summary: "List the bank links waiting to be mapped",
|
|
23421
|
+
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.",
|
|
23422
|
+
status: 200,
|
|
23423
|
+
response: { description: "The bank links waiting to be mapped." },
|
|
23424
|
+
errors: { 403: true }
|
|
23425
|
+
},
|
|
23266
23426
|
pending: {
|
|
23267
23427
|
method: "get",
|
|
23268
23428
|
path: "/money/bank-link/pending/{id}",
|
|
@@ -25085,7 +25245,8 @@ var moneyAccountSchema = createSelectSchema(moneyAccounts, {
|
|
|
25085
25245
|
});
|
|
25086
25246
|
var moneyAccountViewSchema = moneyAccountSchema.omit({ ibanHash: true }).extend({
|
|
25087
25247
|
title: string2(),
|
|
25088
|
-
visibility: _enum2(NODE_VISIBILITY_VALUES)
|
|
25248
|
+
visibility: _enum2(NODE_VISIBILITY_VALUES),
|
|
25249
|
+
balanceKnown: boolean2()
|
|
25089
25250
|
});
|
|
25090
25251
|
var moneyCategorySchema = createSelectSchema(moneyCategories, {
|
|
25091
25252
|
archivedAt: exports_coerce.date().nullable(),
|
|
@@ -25145,7 +25306,14 @@ var createMoneyAccountInputSchema = object({
|
|
|
25145
25306
|
includeInNetWorth: boolean2().optional(),
|
|
25146
25307
|
visibility: _enum2(NODE_VISIBILITY_VALUES).optional()
|
|
25147
25308
|
}).strict();
|
|
25148
|
-
var
|
|
25309
|
+
var moneyBalanceAtSchema = union([keyDateDaySchema, exports_iso.datetime({ offset: true })]);
|
|
25310
|
+
var updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().extend({
|
|
25311
|
+
balance: moneyAmountSchema.optional(),
|
|
25312
|
+
balanceAt: moneyBalanceAtSchema.optional()
|
|
25313
|
+
}).strict().refine((u) => u.balance === undefined === (u.balanceAt === undefined), {
|
|
25314
|
+
message: "a balance and the day it's from go together",
|
|
25315
|
+
path: ["balanceAt"]
|
|
25316
|
+
});
|
|
25149
25317
|
var createMoneyCategoryInputSchema = object({
|
|
25150
25318
|
id: ulidSchema,
|
|
25151
25319
|
name: string2().trim().min(1).max(60),
|
|
@@ -25240,8 +25408,13 @@ var moneyTransactionPageSchema = object({
|
|
|
25240
25408
|
var MONEY_IMPORT_MAX_BYTES = 5 * 1024 * 1024;
|
|
25241
25409
|
var moneyImportFieldsSchema = object({
|
|
25242
25410
|
accountId: ulidSchema,
|
|
25243
|
-
since: keyDateDaySchema.optional()
|
|
25244
|
-
|
|
25411
|
+
since: keyDateDaySchema.optional(),
|
|
25412
|
+
balance: moneyAmountSchema.optional(),
|
|
25413
|
+
balanceAt: moneyBalanceAtSchema.optional()
|
|
25414
|
+
}).strict().refine((f) => f.balance === undefined === (f.balanceAt === undefined), {
|
|
25415
|
+
message: "a balance and the day it's from go together",
|
|
25416
|
+
path: ["balanceAt"]
|
|
25417
|
+
});
|
|
25245
25418
|
var moneyImportOutcomeSchema = _enum2([
|
|
25246
25419
|
"new",
|
|
25247
25420
|
"duplicate",
|
|
@@ -25434,13 +25607,19 @@ var moneyGoalLineSchema = createSelectSchema(moneyGoalLines, {
|
|
|
25434
25607
|
createdAt: exports_coerce.date(),
|
|
25435
25608
|
updatedAt: exports_coerce.date()
|
|
25436
25609
|
});
|
|
25610
|
+
var MONEY_GOAL_SAVED_FROM = ["account", "account_share", "contributions"];
|
|
25611
|
+
var moneyGoalSavedFromSchema = _enum2(MONEY_GOAL_SAVED_FROM);
|
|
25437
25612
|
var moneyGoalViewSchema = moneyGoalSchema.extend({
|
|
25438
25613
|
title: string2(),
|
|
25439
25614
|
visibility: _enum2(NODE_VISIBILITY_VALUES),
|
|
25440
25615
|
accountTitle: string2().nullable(),
|
|
25441
25616
|
accountBalance: amount.nullable(),
|
|
25617
|
+
accountBalanceKnown: boolean2().nullable(),
|
|
25442
25618
|
projectTitle: string2().nullable(),
|
|
25443
25619
|
saved: amount,
|
|
25620
|
+
savedFrom: moneyGoalSavedFromSchema,
|
|
25621
|
+
remaining: amount,
|
|
25622
|
+
monthlyNeed: amount.nullable(),
|
|
25444
25623
|
linesPlanned: amount,
|
|
25445
25624
|
linesPaid: amount
|
|
25446
25625
|
});
|
|
@@ -26945,6 +27124,8 @@ var healthShares = pgTable("health_shares", {
|
|
|
26945
27124
|
episodeId: text("episode_id"),
|
|
26946
27125
|
sections: text("sections").array().notNull().default(sql`'{}'::text[]`),
|
|
26947
27126
|
snapshot: jsonb("snapshot").notNull(),
|
|
27127
|
+
note: text("note"),
|
|
27128
|
+
attachments: jsonb("attachments").notNull().default(sql`'[]'::jsonb`),
|
|
26948
27129
|
tokenHash: text("token_hash").notNull().unique(),
|
|
26949
27130
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
26950
27131
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
@@ -31134,6 +31315,140 @@ var journalExportSchema = object({
|
|
|
31134
31315
|
nextSince: exports_coerce.date()
|
|
31135
31316
|
});
|
|
31136
31317
|
|
|
31318
|
+
// ../../packages/schema/src/contracts/journal-private.ts
|
|
31319
|
+
var JOURNAL_PRIVATE_TERMS = {
|
|
31320
|
+
alcohol: [
|
|
31321
|
+
"alcohol*",
|
|
31322
|
+
"drunk*",
|
|
31323
|
+
"drink*",
|
|
31324
|
+
"beer*",
|
|
31325
|
+
"wine*",
|
|
31326
|
+
"whisky",
|
|
31327
|
+
"whiskey",
|
|
31328
|
+
"vodka*",
|
|
31329
|
+
"gin",
|
|
31330
|
+
"rum",
|
|
31331
|
+
"tequila*",
|
|
31332
|
+
"cocktail*",
|
|
31333
|
+
"hangover*",
|
|
31334
|
+
"hungover",
|
|
31335
|
+
"tipsy",
|
|
31336
|
+
"booze*",
|
|
31337
|
+
"sober*",
|
|
31338
|
+
"sobriety",
|
|
31339
|
+
"bier*",
|
|
31340
|
+
"wijn*",
|
|
31341
|
+
"drank*",
|
|
31342
|
+
"dronken*",
|
|
31343
|
+
"borrel*",
|
|
31344
|
+
"pint",
|
|
31345
|
+
"pints",
|
|
31346
|
+
"pintje*",
|
|
31347
|
+
"kater*",
|
|
31348
|
+
"zuipen",
|
|
31349
|
+
"gezopen"
|
|
31350
|
+
],
|
|
31351
|
+
smoking: [
|
|
31352
|
+
"smok*",
|
|
31353
|
+
"cigar*",
|
|
31354
|
+
"tobacco",
|
|
31355
|
+
"nicotine",
|
|
31356
|
+
"vape*",
|
|
31357
|
+
"vaping",
|
|
31358
|
+
"roken",
|
|
31359
|
+
"rook",
|
|
31360
|
+
"rookte*",
|
|
31361
|
+
"gerookt",
|
|
31362
|
+
"roker*",
|
|
31363
|
+
"sigaar*",
|
|
31364
|
+
"sigaret*",
|
|
31365
|
+
"tabak*",
|
|
31366
|
+
"peuk*",
|
|
31367
|
+
"vapen"
|
|
31368
|
+
],
|
|
31369
|
+
cannabis: [
|
|
31370
|
+
"cannabis",
|
|
31371
|
+
"weed",
|
|
31372
|
+
"hash",
|
|
31373
|
+
"hashish",
|
|
31374
|
+
"marijuana",
|
|
31375
|
+
"joint",
|
|
31376
|
+
"joints",
|
|
31377
|
+
"jointje*",
|
|
31378
|
+
"stoned",
|
|
31379
|
+
"thc",
|
|
31380
|
+
"cbd",
|
|
31381
|
+
"edibles",
|
|
31382
|
+
"wiet*",
|
|
31383
|
+
"hasj*",
|
|
31384
|
+
"blowen",
|
|
31385
|
+
"blowt",
|
|
31386
|
+
"geblowd"
|
|
31387
|
+
],
|
|
31388
|
+
drugs: [
|
|
31389
|
+
"drug*",
|
|
31390
|
+
"cocaine",
|
|
31391
|
+
"coca\xEFne",
|
|
31392
|
+
"coke",
|
|
31393
|
+
"mdma",
|
|
31394
|
+
"ecstasy",
|
|
31395
|
+
"xtc",
|
|
31396
|
+
"ketamine",
|
|
31397
|
+
"lsd",
|
|
31398
|
+
"shrooms",
|
|
31399
|
+
"paddo*",
|
|
31400
|
+
"amphetamine*",
|
|
31401
|
+
"heroin*",
|
|
31402
|
+
"hero\xEFne",
|
|
31403
|
+
"opioid*"
|
|
31404
|
+
],
|
|
31405
|
+
relapse: [
|
|
31406
|
+
"relaps*",
|
|
31407
|
+
"craving*",
|
|
31408
|
+
"rehab",
|
|
31409
|
+
"aa meeting*",
|
|
31410
|
+
"terugval*",
|
|
31411
|
+
"hervallen",
|
|
31412
|
+
"afkick*",
|
|
31413
|
+
"ontwenning*"
|
|
31414
|
+
],
|
|
31415
|
+
fertility: [
|
|
31416
|
+
"fertil*",
|
|
31417
|
+
"ivf",
|
|
31418
|
+
"iui",
|
|
31419
|
+
"ovulat*",
|
|
31420
|
+
"conceiv*",
|
|
31421
|
+
"conception",
|
|
31422
|
+
"pregnan*",
|
|
31423
|
+
"sperm*",
|
|
31424
|
+
"embryo*",
|
|
31425
|
+
"miscarr*",
|
|
31426
|
+
"ttc",
|
|
31427
|
+
"vruchtbaar*",
|
|
31428
|
+
"ovulatie",
|
|
31429
|
+
"eisprong",
|
|
31430
|
+
"zwanger*",
|
|
31431
|
+
"sperma*",
|
|
31432
|
+
"miskraam",
|
|
31433
|
+
"kinderwens"
|
|
31434
|
+
]
|
|
31435
|
+
};
|
|
31436
|
+
var LETTER = "[\\p{L}\\p{N}_]";
|
|
31437
|
+
var PRIVATE = new RegExp(`(?<!${LETTER})(?:${Object.values(JOURNAL_PRIVATE_TERMS).flat().map((term) => {
|
|
31438
|
+
const stem = term.replace(/\*$/, "").replace(/\s+/g, "\\s+");
|
|
31439
|
+
return term.endsWith("*") ? `${stem}${LETTER}*` : stem;
|
|
31440
|
+
}).join("|")})(?!${LETTER})`, "iu");
|
|
31441
|
+
function isPrivateLine(line) {
|
|
31442
|
+
return PRIVATE.test(line);
|
|
31443
|
+
}
|
|
31444
|
+
function withoutPrivateLines(text) {
|
|
31445
|
+
const kept = text.split(/\r?\n/).filter((line) => !isPrivateLine(line)).join(`
|
|
31446
|
+
`).replace(/\n{3,}/g, `
|
|
31447
|
+
|
|
31448
|
+
`).trim();
|
|
31449
|
+
return kept || null;
|
|
31450
|
+
}
|
|
31451
|
+
|
|
31137
31452
|
// ../../packages/schema/src/contracts/journal-import.ts
|
|
31138
31453
|
var journalImportBatchSchema = string2().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "a batch is a month (YYYY-MM)");
|
|
31139
31454
|
var journalImportEntrySchema = _enum2(["summary", "retro", "log"]);
|
|
@@ -31144,6 +31459,7 @@ var journalImportFoundSchema = object({
|
|
|
31144
31459
|
documents: number2().int().nonnegative().optional(),
|
|
31145
31460
|
habits: number2().int().nonnegative().optional()
|
|
31146
31461
|
}).strict();
|
|
31462
|
+
var keptLine = (max) => string2().trim().min(1).max(max).transform((v) => isPrivateLine(v) ? null : v).nullish();
|
|
31147
31463
|
var JOURNAL_IMPORT_MAX_ITEMS = 400;
|
|
31148
31464
|
var journalImportItemInputSchema = object({
|
|
31149
31465
|
day: journalDateSchema,
|
|
@@ -31151,12 +31467,12 @@ var journalImportItemInputSchema = object({
|
|
|
31151
31467
|
sourceHash: string2().trim().min(1).max(128),
|
|
31152
31468
|
entry: journalImportEntrySchema.default("log"),
|
|
31153
31469
|
hasCheck: boolean2().default(false),
|
|
31154
|
-
title:
|
|
31155
|
-
body: string2().trim().min(1).max(20000).nullish(),
|
|
31470
|
+
title: keptLine(200),
|
|
31471
|
+
body: string2().trim().min(1).max(20000).transform(withoutPrivateLines).nullish(),
|
|
31156
31472
|
rating: number2().min(1).max(10).nullish(),
|
|
31157
|
-
ratingQuestion:
|
|
31473
|
+
ratingQuestion: keptLine(80),
|
|
31158
31474
|
sleep: number2().min(0).max(24).nullish(),
|
|
31159
|
-
highlight:
|
|
31475
|
+
highlight: keptLine(200),
|
|
31160
31476
|
milestoneCandidate: boolean2().default(false),
|
|
31161
31477
|
health: boolean2().default(false),
|
|
31162
31478
|
found: journalImportFoundSchema.default({})
|
|
@@ -31222,8 +31538,25 @@ var journalImportAnswerInputSchema = object({
|
|
|
31222
31538
|
rating: number2().min(1).max(10).nullable().optional(),
|
|
31223
31539
|
skip: boolean2().optional()
|
|
31224
31540
|
}).strict().refine((v) => Object.keys(v).length > 0, { message: "nothing to answer" });
|
|
31541
|
+
var journalImportRatingAnswerSchema = object({ day: journalDateSchema, rating: number2().min(1).max(10) }).strict();
|
|
31542
|
+
var journalImportApproveInputSchema = object({
|
|
31543
|
+
batch: journalImportBatchSchema.optional(),
|
|
31544
|
+
all: literal(true).optional(),
|
|
31545
|
+
ratings: array(journalImportRatingAnswerSchema).max(JOURNAL_IMPORT_MAX_ITEMS).default([])
|
|
31546
|
+
}).strict().refine((v) => v.batch !== undefined !== (v.all === true), {
|
|
31547
|
+
message: "approve one batch or all: send batch or all, not both"
|
|
31548
|
+
}).refine((v) => new Set(v.ratings.map((r) => r.day)).size === v.ratings.length, {
|
|
31549
|
+
message: "one rating per day",
|
|
31550
|
+
path: ["ratings"]
|
|
31551
|
+
});
|
|
31552
|
+
var journalImportOpenDaySchema = object({
|
|
31553
|
+
day: journalDateSchema,
|
|
31554
|
+
ratingQuestion: string2()
|
|
31555
|
+
});
|
|
31225
31556
|
var journalImportApproveSchema = object({
|
|
31226
31557
|
imported: number2().int(),
|
|
31558
|
+
batches: array(journalImportBatchSchema).default([]),
|
|
31559
|
+
open: array(journalImportOpenDaySchema).default([]),
|
|
31227
31560
|
overview: journalImportOverviewSchema
|
|
31228
31561
|
});
|
|
31229
31562
|
|
|
@@ -31611,12 +31944,25 @@ var journalOperations = defineOperations({
|
|
|
31611
31944
|
scopes: ["journal:write"],
|
|
31612
31945
|
tag: "Journal",
|
|
31613
31946
|
summary: "Stage daily notes for the backfill",
|
|
31614
|
-
description: "Stage parsed daily notes (`do journal import push <dir>`): one item per day (at most 400 a request) with its source file and hash, what becomes its entry (`summary`, `retro` or `log`), a one-liner, the summary's text (health lines left out), a clear `rating` or a `ratingQuestion` (\"~5/10\"), a highlight, whether it may be a milestone or had health lines, and the links counted. Nothing is written into the Journal
|
|
31947
|
+
description: "Stage parsed daily notes (`do journal import push <dir>`): one item per day (at most 400 a request) with its source file and hash, what becomes its entry (`summary`, `retro` or `log`), a one-liner, the summary's text (health lines left out), a clear `rating` or a `ratingQuestion` (\"~5/10\"), a highlight, whether it may be a milestone or had health lines, and the links counted. A line about drinking, smoking, cannabis or other drugs, relapse or fertility is stripped from the title, text, highlight and rating question here, whatever parsed the day. Nothing is written into the Journal until a batch is approved (`journal_import_approve`, or the member's Approve in the app). A day still waiting is replaced; one already imported is left alone (`alreadyImported`). Staged days stand behind the Journal's wall.",
|
|
31615
31948
|
body: journalImportUploadInputSchema,
|
|
31616
31949
|
status: 200,
|
|
31617
31950
|
response: { description: "How the upload went.", view: "JournalImportUpload" },
|
|
31618
31951
|
errors: { 400: true }
|
|
31619
31952
|
},
|
|
31953
|
+
approveImport: {
|
|
31954
|
+
method: "post",
|
|
31955
|
+
path: "/journal/imports/approve",
|
|
31956
|
+
scopes: ["journal:write"],
|
|
31957
|
+
tag: "Journal",
|
|
31958
|
+
summary: "Approve staged days into the Journal",
|
|
31959
|
+
description: 'Write staged days into the Journal (`do journal import approve <YYYY-MM|--all>`): one batch (`batch`, a month `YYYY-MM`) or every staged batch (`all: true`), never both. `ratings` answers the rating questions inline, one per day (`[{"day":"2026-10-03","rating":7}]`, 1 to 10). A day whose question has no answer stays staged and comes back in `open` with its question; the rest are written as imports (one-liner, summary, highlight, the `rating` and `sleep` Metrics), and a day the Journal already has keeps what\'s there and only gains a missing rating or sleep. An agent\'s approve goes through the same writes as `journal_write_day`: it shows in the member\'s change list with Undo, and past the bulk limit it waits for the member\'s confirmation (428). Lines about drinking, smoking, drugs, relapse or fertility are never written. 400 when a rating names a day that isn\'t waiting in the batch, or nothing is ready and nothing is open.',
|
|
31960
|
+
body: journalImportApproveInputSchema,
|
|
31961
|
+
status: 200,
|
|
31962
|
+
response: { description: "How the approve went.", view: "JournalImportApprove" },
|
|
31963
|
+
errors: { 400: true },
|
|
31964
|
+
operationId: "journal_import_approve"
|
|
31965
|
+
},
|
|
31620
31966
|
importOverview: {
|
|
31621
31967
|
method: "get",
|
|
31622
31968
|
path: "/journal/imports",
|
|
@@ -32057,6 +32403,98 @@ var keyOperations = defineOperations({
|
|
|
32057
32403
|
}
|
|
32058
32404
|
});
|
|
32059
32405
|
|
|
32406
|
+
// ../../packages/schema/src/contracts/money-kraken.ts
|
|
32407
|
+
var KRAKEN_SECRET = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
32408
|
+
var moneyKrakenKeyInputSchema = object({
|
|
32409
|
+
apiKey: string2().trim().min(16).max(200).regex(/^\S+$/, "The API key has no spaces."),
|
|
32410
|
+
apiSecret: string2().trim().min(40).max(200).regex(KRAKEN_SECRET, { message: "Paste the private key as Kraken shows it (base64)." }),
|
|
32411
|
+
accountId: ulidSchema.optional()
|
|
32412
|
+
}).strict();
|
|
32413
|
+
var moneyHoldingSchema = object({
|
|
32414
|
+
asset: string2(),
|
|
32415
|
+
amount: string2(),
|
|
32416
|
+
priceEur: string2().nullable(),
|
|
32417
|
+
valueEur: string2().nullable(),
|
|
32418
|
+
share: number2().nullable()
|
|
32419
|
+
});
|
|
32420
|
+
var moneyHoldingsSchema = object({
|
|
32421
|
+
accountId: ulidSchema,
|
|
32422
|
+
at: exports_coerce.date(),
|
|
32423
|
+
totalEur: string2(),
|
|
32424
|
+
dustEur: string2(),
|
|
32425
|
+
holdings: array(moneyHoldingSchema)
|
|
32426
|
+
});
|
|
32427
|
+
var moneyAccountHoldingsSchema = object({ holdings: moneyHoldingsSchema.nullable() });
|
|
32428
|
+
var moneyKrakenLinkStatusSchema = object({
|
|
32429
|
+
configured: boolean2(),
|
|
32430
|
+
link: object({
|
|
32431
|
+
id: ulidSchema,
|
|
32432
|
+
accountId: ulidSchema,
|
|
32433
|
+
accountTitle: string2(),
|
|
32434
|
+
status: _enum2(moneyConnectionStatus.enumValues),
|
|
32435
|
+
lastSyncAt: exports_coerce.date().nullable(),
|
|
32436
|
+
lastError: string2().nullable()
|
|
32437
|
+
}).nullable(),
|
|
32438
|
+
holdings: moneyHoldingsSchema.nullable()
|
|
32439
|
+
});
|
|
32440
|
+
|
|
32441
|
+
// ../../packages/schema/src/operations/kraken-link.ts
|
|
32442
|
+
var CONNECT2 = ["connections:write"];
|
|
32443
|
+
var AFTER2 = "`*` and `private:*` never grant `connections:write`: a key holds it by name, and a guest's key never does.";
|
|
32444
|
+
var krakenLinkOperations = defineOperations({
|
|
32445
|
+
status: {
|
|
32446
|
+
method: "get",
|
|
32447
|
+
path: "/money/kraken-link",
|
|
32448
|
+
scopes: CONNECT2,
|
|
32449
|
+
afterScopes: AFTER2,
|
|
32450
|
+
tag: "Money",
|
|
32451
|
+
summary: "Get the Kraken link",
|
|
32452
|
+
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.",
|
|
32453
|
+
status: 200,
|
|
32454
|
+
response: { description: "The Kraken link." },
|
|
32455
|
+
errors: { 403: true }
|
|
32456
|
+
},
|
|
32457
|
+
set: {
|
|
32458
|
+
method: "put",
|
|
32459
|
+
path: "/money/kraken-link",
|
|
32460
|
+
scopes: CONNECT2,
|
|
32461
|
+
afterScopes: AFTER2,
|
|
32462
|
+
tag: "Money",
|
|
32463
|
+
summary: "Set the Kraken key",
|
|
32464
|
+
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.",
|
|
32465
|
+
body: moneyKrakenKeyInputSchema,
|
|
32466
|
+
status: 200,
|
|
32467
|
+
response: { description: "The Kraken link, without the key." },
|
|
32468
|
+
errors: { 400: true, 403: true, 404: true, 503: true },
|
|
32469
|
+
throttle: "strict",
|
|
32470
|
+
mcp: false
|
|
32471
|
+
},
|
|
32472
|
+
sync: {
|
|
32473
|
+
method: "post",
|
|
32474
|
+
path: "/money/kraken-link/sync",
|
|
32475
|
+
scopes: CONNECT2,
|
|
32476
|
+
afterScopes: AFTER2,
|
|
32477
|
+
tag: "Money",
|
|
32478
|
+
summary: "Sync Kraken now",
|
|
32479
|
+
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.",
|
|
32480
|
+
status: 200,
|
|
32481
|
+
response: { description: "The Kraken link after the sync." },
|
|
32482
|
+
errors: { 400: true, 403: true }
|
|
32483
|
+
},
|
|
32484
|
+
remove: {
|
|
32485
|
+
method: "delete",
|
|
32486
|
+
path: "/money/kraken-link",
|
|
32487
|
+
scopes: CONNECT2,
|
|
32488
|
+
afterScopes: AFTER2,
|
|
32489
|
+
tag: "Money",
|
|
32490
|
+
summary: "Remove the Kraken link",
|
|
32491
|
+
description: "Forget the key's member's Kraken key and stop syncing. The account stays, with its last balance and holdings.",
|
|
32492
|
+
status: 200,
|
|
32493
|
+
response: { description: "The Kraken link, not configured." },
|
|
32494
|
+
errors: { 403: true }
|
|
32495
|
+
}
|
|
32496
|
+
});
|
|
32497
|
+
|
|
32060
32498
|
// ../../packages/schema/src/db/mail.ts
|
|
32061
32499
|
var mailCampaignStatus = pgEnum("mail_campaign_status", [
|
|
32062
32500
|
"draft",
|
|
@@ -32602,6 +33040,13 @@ var moneyNetWorthAccountSchema = object({
|
|
|
32602
33040
|
source: moneySnapshotSourceSchema,
|
|
32603
33041
|
month: moneyMonthSchema
|
|
32604
33042
|
});
|
|
33043
|
+
var moneyNetWorthUnknownSchema = moneyNetWorthAccountSchema.pick({
|
|
33044
|
+
accountId: true,
|
|
33045
|
+
title: true,
|
|
33046
|
+
kind: true,
|
|
33047
|
+
visibility: true,
|
|
33048
|
+
currency: true
|
|
33049
|
+
});
|
|
32605
33050
|
var moneyNetWorthSchema = object({
|
|
32606
33051
|
scope: moneyBudgetScopeSchema,
|
|
32607
33052
|
month: moneyMonthSchema,
|
|
@@ -32609,7 +33054,9 @@ var moneyNetWorthSchema = object({
|
|
|
32609
33054
|
have: amount5,
|
|
32610
33055
|
owe: amount5,
|
|
32611
33056
|
points: array(moneyNetWorthPointSchema),
|
|
32612
|
-
accounts: array(moneyNetWorthAccountSchema)
|
|
33057
|
+
accounts: array(moneyNetWorthAccountSchema),
|
|
33058
|
+
complete: boolean2(),
|
|
33059
|
+
unknown: array(moneyNetWorthUnknownSchema)
|
|
32613
33060
|
});
|
|
32614
33061
|
var setMoneyNetWorthValueInputSchema = object({
|
|
32615
33062
|
accountId: ulidSchema,
|
|
@@ -32791,7 +33238,7 @@ var moneyOperations = defineOperations({
|
|
|
32791
33238
|
scopes: ["finance:write"],
|
|
32792
33239
|
tag: "Money",
|
|
32793
33240
|
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.",
|
|
33241
|
+
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
33242
|
body: moneyImportFieldsSchema,
|
|
32796
33243
|
multipart: STATEMENT,
|
|
32797
33244
|
status: 201,
|
|
@@ -32838,7 +33285,7 @@ var moneyOperations = defineOperations({
|
|
|
32838
33285
|
scopes: ["finance:write"],
|
|
32839
33286
|
tag: "Money",
|
|
32840
33287
|
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
|
|
33288
|
+
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
33289
|
body: moneyBankSyncInputSchema,
|
|
32843
33290
|
status: 200,
|
|
32844
33291
|
response: { description: "What each sync did." },
|
|
@@ -32860,6 +33307,18 @@ var moneyOperations = defineOperations({
|
|
|
32860
33307
|
response: { description: "The sync log." },
|
|
32861
33308
|
errors: { 404: `No such account or transaction, or it is another member's "Only me" one.` }
|
|
32862
33309
|
},
|
|
33310
|
+
dedupe: {
|
|
33311
|
+
method: "post",
|
|
33312
|
+
path: "/money/dedupe",
|
|
33313
|
+
scopes: ["finance:write"],
|
|
33314
|
+
tag: "Money",
|
|
33315
|
+
summary: "Fold a bank link's twins into the statement's rows",
|
|
33316
|
+
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.",
|
|
33317
|
+
body: moneyDedupeInputSchema,
|
|
33318
|
+
status: 200,
|
|
33319
|
+
response: { description: "The pairs, their counts and what changed." },
|
|
33320
|
+
errors: { 400: true, 404: NO_ACCOUNT }
|
|
33321
|
+
},
|
|
32863
33322
|
getAccount: {
|
|
32864
33323
|
method: "get",
|
|
32865
33324
|
path: "/money/accounts/{id}",
|
|
@@ -32879,7 +33338,7 @@ var moneyOperations = defineOperations({
|
|
|
32879
33338
|
scopes: ["finance:write"],
|
|
32880
33339
|
tag: "Money",
|
|
32881
33340
|
summary: "Update a money account",
|
|
32882
|
-
description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it.",
|
|
33341
|
+
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
33342
|
pathParams: { id: "The account's ULID." },
|
|
32884
33343
|
pathParamSchemas: { id: ulidSchema },
|
|
32885
33344
|
body: updateMoneyAccountInputSchema,
|
|
@@ -33399,7 +33858,7 @@ var moneyOperations = defineOperations({
|
|
|
33399
33858
|
scopes: ["finance:read"],
|
|
33400
33859
|
tag: "Money",
|
|
33401
33860
|
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
|
|
33861
|
+
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
33862
|
query: [
|
|
33404
33863
|
{ name: "scope", description: "all, space or owner." },
|
|
33405
33864
|
{ name: "months", description: "How many months the line covers (2\u201360, default 12)." }
|
|
@@ -33427,7 +33886,7 @@ var moneyOperations = defineOperations({
|
|
|
33427
33886
|
scopes: ["finance:read"],
|
|
33428
33887
|
tag: "Money",
|
|
33429
33888
|
summary: "List money goals",
|
|
33430
|
-
description: "The goals this key may see (the household's, and its member's Only me ones), soonest target first: `targetAmount`, `targetOn`, `monthlyPlan`, the pot's account and Project by name, `saved` (`savedBefore` plus the transactions counted toward it) and the lines' `linesPlanned`/`linesPaid`.",
|
|
33889
|
+
description: "The goals this key may see (the household's, and its member's Only me ones), soonest target first: `targetAmount`, `targetOn`, `monthlyPlan`, the pot's account and Project by name, `saved` and where it comes from (`savedFrom`: `account`, the balance of the account it's alone in; `account_share`, its own capped by what a shared account has left, soonest target first; `contributions`, `savedBefore` plus the transactions counted toward it, also when the account's balance is unknown, `accountBalanceKnown: false`), `remaining`, `monthlyNeed` (to `targetOn`) and the lines' `linesPlanned`/`linesPaid`.",
|
|
33431
33890
|
status: 200,
|
|
33432
33891
|
response: { description: "The goals." }
|
|
33433
33892
|
},
|
|
@@ -34567,7 +35026,8 @@ var planUnplaceInputSchema = object({ taskId: ulidSchema }).strict();
|
|
|
34567
35026
|
var planListQuerySchema = object({
|
|
34568
35027
|
kind: planKindSchema.optional(),
|
|
34569
35028
|
status: planStatusSchema.optional(),
|
|
34570
|
-
startsOn: civilDaySchema.optional()
|
|
35029
|
+
startsOn: civilDaySchema.optional(),
|
|
35030
|
+
taskId: ulidSchema.optional()
|
|
34571
35031
|
});
|
|
34572
35032
|
var planTaskSchema = object({
|
|
34573
35033
|
id: ulidSchema,
|
|
@@ -34787,6 +35247,7 @@ var automationListItemSchema = automationViewSchema.extend({
|
|
|
34787
35247
|
madeBy: object({ userId: string2(), name: string2(), you: boolean2() }).nullable(),
|
|
34788
35248
|
lastRun: automationRunSummarySchema.nullable()
|
|
34789
35249
|
});
|
|
35250
|
+
var automationRunNowSchema = object({ queued: literal(true) });
|
|
34790
35251
|
|
|
34791
35252
|
// ../../packages/schema/src/db/shopping.ts
|
|
34792
35253
|
var shoppingLists = pgTable("shopping_lists", {
|
|
@@ -38540,6 +39001,18 @@ var WEEKDAY_ALT = "monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|
|
|
|
38540
39001
|
var EVERY_N_RE = /\bevery\s+(\d+)\s+(day|days|week|weeks|month|months|year|years)\b/i;
|
|
38541
39002
|
var EVERY_WEEKDAYS_RE = /\bevery\s+(?:weekday|weekdays|work\s?day|work\s?days)\b/i;
|
|
38542
39003
|
var EVERY_WEEKDAY_RE = new RegExp(`\\bevery\\s+(${WEEKDAY_ALT})\\b`, "i");
|
|
39004
|
+
var EVERY_NTH_WEEKDAY_RE = new RegExp(`\\bevery\\s+(first|1st|second|2nd|third|3rd|fourth|4th|last)\\s+(${WEEKDAY_ALT}|sun)(?:\\s+of\\s+(?:the|each|every)\\s+month)?\\b`, "i");
|
|
39005
|
+
var NTH = {
|
|
39006
|
+
first: "1",
|
|
39007
|
+
"1st": "1",
|
|
39008
|
+
second: "2",
|
|
39009
|
+
"2nd": "2",
|
|
39010
|
+
third: "3",
|
|
39011
|
+
"3rd": "3",
|
|
39012
|
+
fourth: "4",
|
|
39013
|
+
"4th": "4",
|
|
39014
|
+
last: "-1"
|
|
39015
|
+
};
|
|
38543
39016
|
var EVERY_UNIT_RE = /\bevery\s+(day|week|month|year)\b/i;
|
|
38544
39017
|
var ADVERB_RE = /\b(daily|weekly|monthly|yearly|annually)\b/i;
|
|
38545
39018
|
var RELATIVE_RE = /\b(today|tonight|tomorrow|tmrw|tmr|yesterday)\b/i;
|
|
@@ -38671,6 +39144,12 @@ function takeEstimate(scan, take) {
|
|
|
38671
39144
|
return;
|
|
38672
39145
|
}
|
|
38673
39146
|
function takeRecurrence(scan, take) {
|
|
39147
|
+
const nth = scan.match(EVERY_NTH_WEEKDAY_RE);
|
|
39148
|
+
if (nth && take(nth, "recurrence")) {
|
|
39149
|
+
const ordinal = NTH[(nth[1] ?? "").toLowerCase()] ?? "1";
|
|
39150
|
+
const byDay = `${ordinal}${weekdayByName(nth[2]).code}`;
|
|
39151
|
+
return { mode: "rigid", freq: "monthly", interval: 1, byDay: [byDay] };
|
|
39152
|
+
}
|
|
38674
39153
|
const everyN = scan.match(EVERY_N_RE);
|
|
38675
39154
|
if (everyN && take(everyN, "recurrence")) {
|
|
38676
39155
|
const [, nStr = "1", unitRaw = "day"] = everyN;
|
|
@@ -38806,8 +39285,28 @@ function firstOccurrence(rule, now) {
|
|
|
38806
39285
|
});
|
|
38807
39286
|
return addDays(today, Math.min(...deltas));
|
|
38808
39287
|
}
|
|
39288
|
+
const nth = rule.freq === "monthly" ? rule.byDay?.map(parseByDay) : undefined;
|
|
39289
|
+
if (nth?.length) {
|
|
39290
|
+
const days = [0, 1].flatMap((ahead) => nth.flatMap((p) => {
|
|
39291
|
+
if (!p)
|
|
39292
|
+
return [];
|
|
39293
|
+
const dow = WEEKDAYS.find((w) => w.code === p.weekday)?.dow ?? 0;
|
|
39294
|
+
const day = nthWeekdayOfMonth(today.getFullYear(), today.getMonth() + ahead, dow, p.ordinal ?? 1);
|
|
39295
|
+
return day >= today ? [day] : [];
|
|
39296
|
+
}));
|
|
39297
|
+
if (days.length > 0)
|
|
39298
|
+
return new Date(Math.min(...days.map((d) => d.getTime())));
|
|
39299
|
+
}
|
|
38809
39300
|
return today;
|
|
38810
39301
|
}
|
|
39302
|
+
function nthWeekdayOfMonth(year, month, dow, ordinal) {
|
|
39303
|
+
if (ordinal < 0) {
|
|
39304
|
+
const last = new Date(year, month + 1, 0);
|
|
39305
|
+
return addDays(last, -((last.getDay() - dow + 7) % 7));
|
|
39306
|
+
}
|
|
39307
|
+
const first = new Date(year, month, 1);
|
|
39308
|
+
return addDays(first, (dow - first.getDay() + 7) % 7 + (ordinal - 1) * 7);
|
|
39309
|
+
}
|
|
38811
39310
|
function addMonths(d, months) {
|
|
38812
39311
|
const next = new Date(d);
|
|
38813
39312
|
next.setMonth(next.getMonth() + months);
|
|
@@ -38878,6 +39377,7 @@ var bookFacetSchema = object({
|
|
|
38878
39377
|
coverSources: coverSourcesSchema.nullish(),
|
|
38879
39378
|
pageCount: number2().int().min(1).max(1e5).nullish(),
|
|
38880
39379
|
currentPage: number2().int().min(0).max(1e5).nullish(),
|
|
39380
|
+
chapter: string2().trim().min(1).max(60).nullish(),
|
|
38881
39381
|
isbn: string2().regex(/^[0-9Xx][0-9Xx\s-]{8,20}$/, "must be an ISBN").nullish()
|
|
38882
39382
|
}).strict();
|
|
38883
39383
|
var filmFacetSchema = object({
|
|
@@ -39215,7 +39715,7 @@ var STATUS_MARK = {
|
|
|
39215
39715
|
};
|
|
39216
39716
|
function suggestionText(s) {
|
|
39217
39717
|
const parts = [
|
|
39218
|
-
s.area,
|
|
39718
|
+
[s.area, s.project].filter(Boolean).join(" \u203A ") || null,
|
|
39219
39719
|
s.owner ? `owner ${ownerText(s.owner)}` : null,
|
|
39220
39720
|
s.scheduledAt ? shortDate(s.scheduledAt) : null
|
|
39221
39721
|
].filter(Boolean);
|
|
@@ -40095,7 +40595,17 @@ function renderJournalImportScan(data, mode) {
|
|
|
40095
40595
|
}
|
|
40096
40596
|
out.push(` milestone candidates ${count((i) => Boolean(i.milestoneCandidate))}`);
|
|
40097
40597
|
out.push(` health lines (left for Health) ${count((i) => Boolean(i.health))}`);
|
|
40098
|
-
out.push("", d.dryRun ? paint(mode, "dim", "Dry run: nothing uploaded.") : `Staged ${d.staged ?? 0}${d.alreadyImported ? ` (${d.alreadyImported} already in the Journal, left as they are)` : ""}. Approve each month in the app: Journal \u203A Import.`);
|
|
40598
|
+
out.push("", d.dryRun ? paint(mode, "dim", "Dry run: nothing uploaded.") : `Staged ${d.staged ?? 0}${d.alreadyImported ? ` (${d.alreadyImported} already in the Journal, left as they are)` : ""}. Approve each month with do journal import approve <YYYY-MM>, or in the app: Journal \u203A Import.`);
|
|
40599
|
+
return out.join(`
|
|
40600
|
+
`);
|
|
40601
|
+
}
|
|
40602
|
+
function renderJournalImportApprove(data, mode) {
|
|
40603
|
+
const d = data;
|
|
40604
|
+
const batches = d.batches?.length ? paint(mode, "dim", ` (${d.batches.join(", ")})`) : "";
|
|
40605
|
+
const out = [`Imported ${d.imported ?? 0} days into the Journal${batches}`];
|
|
40606
|
+
if (d.open?.length) {
|
|
40607
|
+
out.push("", "Still asking for a rating, left staged (answer with --rating YYYY-MM-DD=N):", ...d.open.map((o) => ` ${o.day} \u201C${o.ratingQuestion}\u201D`));
|
|
40608
|
+
}
|
|
40099
40609
|
return out.join(`
|
|
40100
40610
|
`);
|
|
40101
40611
|
}
|
|
@@ -41224,11 +41734,16 @@ var commands2 = [
|
|
|
41224
41734
|
],
|
|
41225
41735
|
flags: [
|
|
41226
41736
|
{ name: "unit", type: "string", description: "For a weight: g (default) or kg." },
|
|
41737
|
+
{
|
|
41738
|
+
name: "note",
|
|
41739
|
+
type: "string",
|
|
41740
|
+
description: 'For a weight: a short note drawn under its date ("vet check done").'
|
|
41741
|
+
},
|
|
41227
41742
|
{ name: "at", type: "string", description: "When (ISO 8601); default now." }
|
|
41228
41743
|
],
|
|
41229
41744
|
scopes: [...animalOperations.log.scopes],
|
|
41230
41745
|
returns: "AnimalTimelineEntry",
|
|
41231
|
-
notes: `${routeOf(animalOperations.log)} (\`animal_log\`). \`do animals log <hens> eggs 2\`, \`do animals log <dog> weight 37.8 --unit kg\`, \`do animals log <hen> note "eating again"\`. Eggs are the day's count: logging again that day replaces it.`
|
|
41746
|
+
notes: `${routeOf(animalOperations.log)} (\`animal_log\`). \`do animals log <hens> eggs 2\`, \`do animals log <dog> weight 37.8 --unit kg\`, \`do animals log <tortoise> weight 1409 --note "vet check done"\`, \`do animals log <hen> note "eating again"\`. Eggs are the day's count: logging again that day replaces it.`
|
|
41232
41747
|
},
|
|
41233
41748
|
run: async ({ args, flags, requireClient }) => {
|
|
41234
41749
|
const id = requireArg(args, 0, "animal id");
|
|
@@ -41238,8 +41753,11 @@ var commands2 = [
|
|
|
41238
41753
|
throw usageError("animals log needs a value (a number, or the note's text)");
|
|
41239
41754
|
const at = flagString(flags, "at");
|
|
41240
41755
|
const unit = flagString(flags, "unit");
|
|
41756
|
+
const note = flagString(flags, "note");
|
|
41757
|
+
if (note && kind !== "weight")
|
|
41758
|
+
throw usageError("--note goes with a weight");
|
|
41241
41759
|
const n = Number(raw);
|
|
41242
|
-
const fact = kind === "note" ? { kind, text: raw } : kind === "eggs" ? { kind, count: n } : kind === "weight" ? { kind, value: n, ...unit ? { unit } : {} } : { kind, value: n };
|
|
41760
|
+
const fact = kind === "note" ? { kind, text: raw } : kind === "eggs" ? { kind, count: n } : kind === "weight" ? { kind, value: n, ...unit ? { unit } : {}, ...note ? { note } : {} } : { kind, value: n };
|
|
41243
41761
|
if (kind !== "note" && Number.isNaN(n))
|
|
41244
41762
|
throw usageError("the value must be a number");
|
|
41245
41763
|
const body = parseInput(animalLogInputSchema, { ...fact, ...at ? { occurredAt: at } : {} });
|
|
@@ -42167,6 +42685,20 @@ var commands7 = [
|
|
|
42167
42685
|
];
|
|
42168
42686
|
|
|
42169
42687
|
// src/commands/collection.ts
|
|
42688
|
+
var CHAPTER_FLAG = {
|
|
42689
|
+
name: "chapter",
|
|
42690
|
+
type: "string",
|
|
42691
|
+
description: 'Where in the work: "9" (a chapter), "chapter 9", "book 9 and on".'
|
|
42692
|
+
};
|
|
42693
|
+
var LINKED_NOTE_FLAG = {
|
|
42694
|
+
name: "linked-note",
|
|
42695
|
+
type: "string",
|
|
42696
|
+
description: "Note ULID this Entry is tied to."
|
|
42697
|
+
};
|
|
42698
|
+
var whereFlags = (flags) => ({
|
|
42699
|
+
...flagString(flags, "chapter") ? { chapter: flagString(flags, "chapter") } : {},
|
|
42700
|
+
...flagString(flags, "linked-note") ? { linkedNoteId: flagString(flags, "linked-note") } : {}
|
|
42701
|
+
});
|
|
42170
42702
|
var commands8 = [
|
|
42171
42703
|
{
|
|
42172
42704
|
spec: {
|
|
@@ -42290,6 +42822,53 @@ var commands8 = [
|
|
|
42290
42822
|
};
|
|
42291
42823
|
}
|
|
42292
42824
|
},
|
|
42825
|
+
{
|
|
42826
|
+
spec: {
|
|
42827
|
+
path: ["collection", "reading"],
|
|
42828
|
+
summary: "Say where you are in a book: its chapter and/or page.",
|
|
42829
|
+
args: [{ name: "id", required: true, description: "Item ULID (a book)." }],
|
|
42830
|
+
flags: [
|
|
42831
|
+
{
|
|
42832
|
+
name: "chapter",
|
|
42833
|
+
type: "string",
|
|
42834
|
+
description: 'Where you are: "9" (a chapter), "chapter 9", "book 9".'
|
|
42835
|
+
},
|
|
42836
|
+
{ name: "page", type: "string", description: "The page you're on." },
|
|
42837
|
+
{ name: "clear-chapter", type: "boolean", description: "Forget the chapter." }
|
|
42838
|
+
],
|
|
42839
|
+
scopes: [...collectionOperations.update.scopes],
|
|
42840
|
+
returns: "CollectionItemView",
|
|
42841
|
+
notes: `Reads the book's facet and patches it back with the new place (${routeOf(collectionOperations.update)}). A first page moves a book you want to doing (Progress); a chapter alone moves nothing.`
|
|
42842
|
+
},
|
|
42843
|
+
run: async ({ args, flags, requireClient }) => {
|
|
42844
|
+
const id = requireArg(args, 0, "item id");
|
|
42845
|
+
const chapter = flagString(flags, "chapter");
|
|
42846
|
+
const clear = flagBool(flags, "clear-chapter");
|
|
42847
|
+
const page = flagString(flags, "page");
|
|
42848
|
+
if (page !== undefined && !/^\d+$/.test(page)) {
|
|
42849
|
+
throw usageError("--page must be a whole number, e.g. 142");
|
|
42850
|
+
}
|
|
42851
|
+
if (chapter === undefined && page === undefined && !clear) {
|
|
42852
|
+
throw usageError('collection reading needs --chapter or --page, e.g. --chapter "9"');
|
|
42853
|
+
}
|
|
42854
|
+
const client = requireClient();
|
|
42855
|
+
const item = await client.get(pathOf(collectionOperations.get, { id }));
|
|
42856
|
+
const { chapter: kept, ...rest } = item.facet ?? {};
|
|
42857
|
+
const facet = {
|
|
42858
|
+
...rest,
|
|
42859
|
+
...clear || kept === undefined ? {} : { chapter: kept }
|
|
42860
|
+
};
|
|
42861
|
+
if (chapter !== undefined)
|
|
42862
|
+
facet.chapter = chapter;
|
|
42863
|
+
if (page !== undefined)
|
|
42864
|
+
facet.currentPage = Number(page);
|
|
42865
|
+
const body = parseInput(updateCollectionItemInputSchema, { facet });
|
|
42866
|
+
return {
|
|
42867
|
+
data: await client.patch(pathOf(collectionOperations.update, { id }), body),
|
|
42868
|
+
render: renderRecord
|
|
42869
|
+
};
|
|
42870
|
+
}
|
|
42871
|
+
},
|
|
42293
42872
|
{
|
|
42294
42873
|
spec: {
|
|
42295
42874
|
path: ["collection", "add-entry"],
|
|
@@ -42309,7 +42888,9 @@ var commands8 = [
|
|
|
42309
42888
|
description: "What happened: use (default) \xB7 service \xB7 repair \xB7 fault \xB7 lent \xB7 returned \xB7 valued \xB7 moved."
|
|
42310
42889
|
},
|
|
42311
42890
|
{ name: "cost", type: "string", description: "What it cost (a sweep, a repair)." },
|
|
42312
|
-
{ name: "contact", type: "string", description: "Contact ULID of who did it." }
|
|
42891
|
+
{ name: "contact", type: "string", description: "Contact ULID of who did it." },
|
|
42892
|
+
CHAPTER_FLAG,
|
|
42893
|
+
LINKED_NOTE_FLAG
|
|
42313
42894
|
],
|
|
42314
42895
|
scopes: [...collectionOperations.addEntry.scopes],
|
|
42315
42896
|
returns: "CollectionItemDetail"
|
|
@@ -42331,7 +42912,8 @@ var commands8 = [
|
|
|
42331
42912
|
...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
|
|
42332
42913
|
...flagString(flags, "kind") ? { kind: flagString(flags, "kind") } : {},
|
|
42333
42914
|
...cost !== undefined ? { cost: Number(cost) } : {},
|
|
42334
|
-
...flagString(flags, "contact") ? { contactId: flagString(flags, "contact") } : {}
|
|
42915
|
+
...flagString(flags, "contact") ? { contactId: flagString(flags, "contact") } : {},
|
|
42916
|
+
...whereFlags(flags)
|
|
42335
42917
|
});
|
|
42336
42918
|
return {
|
|
42337
42919
|
data: await requireClient().post(pathOf(collectionOperations.addEntry, { id }), body),
|
|
@@ -42420,7 +43002,8 @@ var commands8 = [
|
|
|
42420
43002
|
name: "on",
|
|
42421
43003
|
type: "string",
|
|
42422
43004
|
description: "Day it occurred (YYYY-MM-DD); defaults to today."
|
|
42423
|
-
}
|
|
43005
|
+
},
|
|
43006
|
+
CHAPTER_FLAG
|
|
42424
43007
|
],
|
|
42425
43008
|
scopes: [...collectionOperations.log.scopes],
|
|
42426
43009
|
returns: "CollectionLogResult",
|
|
@@ -42441,7 +43024,8 @@ var commands8 = [
|
|
|
42441
43024
|
...flagString(flags, "status") ? { status: flagString(flags, "status") } : {},
|
|
42442
43025
|
...rating !== undefined ? { rating: Number(rating) } : {},
|
|
42443
43026
|
...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
|
|
42444
|
-
...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {}
|
|
43027
|
+
...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {},
|
|
43028
|
+
...flagString(flags, "chapter") ? { chapter: flagString(flags, "chapter") } : {}
|
|
42445
43029
|
});
|
|
42446
43030
|
return {
|
|
42447
43031
|
data: await requireClient().post(pathOf(collectionOperations.log), body),
|
|
@@ -42452,7 +43036,7 @@ var commands8 = [
|
|
|
42452
43036
|
{
|
|
42453
43037
|
spec: {
|
|
42454
43038
|
path: ["collection", "edit-entry"],
|
|
42455
|
-
summary: "Fix an Entry's day, rating or note.",
|
|
43039
|
+
summary: "Fix an Entry's day, rating, note, chapter or linked note.",
|
|
42456
43040
|
args: [
|
|
42457
43041
|
{ name: "id", required: true, description: "Item ULID." },
|
|
42458
43042
|
{ name: "entryId", required: true, description: "Entry ULID." }
|
|
@@ -42460,7 +43044,9 @@ var commands8 = [
|
|
|
42460
43044
|
flags: [
|
|
42461
43045
|
{ name: "rating", type: "string", description: "New rating 0.5\u20135." },
|
|
42462
43046
|
{ name: "note", type: "string", description: "New note." },
|
|
42463
|
-
{ name: "on", type: "string", description: "New day (YYYY-MM-DD)." }
|
|
43047
|
+
{ name: "on", type: "string", description: "New day (YYYY-MM-DD)." },
|
|
43048
|
+
CHAPTER_FLAG,
|
|
43049
|
+
LINKED_NOTE_FLAG
|
|
42464
43050
|
],
|
|
42465
43051
|
scopes: [...collectionOperations.editEntry.scopes],
|
|
42466
43052
|
returns: "CollectionItemDetail"
|
|
@@ -42475,7 +43061,8 @@ var commands8 = [
|
|
|
42475
43061
|
const body = parseInput(updateCollectionEntryInputSchema, {
|
|
42476
43062
|
...rating !== undefined ? { rating: Number(rating) } : {},
|
|
42477
43063
|
...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
|
|
42478
|
-
...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {}
|
|
43064
|
+
...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {},
|
|
43065
|
+
...whereFlags(flags)
|
|
42479
43066
|
});
|
|
42480
43067
|
return {
|
|
42481
43068
|
data: await requireClient().patch(pathOf(collectionOperations.editEntry, { id, entryId }), body),
|
|
@@ -42682,12 +43269,18 @@ var CONNECTION_COMMAND_SPECS = [
|
|
|
42682
43269
|
},
|
|
42683
43270
|
{
|
|
42684
43271
|
path: ["money", "bank-link", "pending"],
|
|
42685
|
-
summary: "
|
|
42686
|
-
args: [
|
|
43272
|
+
summary: "The bank authorisations waiting to be mapped, or one by its id: bank accounts and suggestions.",
|
|
43273
|
+
args: [
|
|
43274
|
+
{
|
|
43275
|
+
name: "id",
|
|
43276
|
+
required: false,
|
|
43277
|
+
description: "The pendingId from `complete`; without it, every one still waiting."
|
|
43278
|
+
}
|
|
43279
|
+
],
|
|
42687
43280
|
flags: [],
|
|
42688
43281
|
scopes: scopesOf(bankLinkOperations.pending),
|
|
42689
|
-
returns: "MoneyBankPending",
|
|
42690
|
-
notes: `${routeOf(bankLinkOperations.pending)}.`
|
|
43282
|
+
returns: "MoneyBankPending (one) or MoneyBankPending[] (all)",
|
|
43283
|
+
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
43284
|
},
|
|
42692
43285
|
{
|
|
42693
43286
|
path: ["money", "bank-link", "link"],
|
|
@@ -42705,6 +43298,53 @@ var CONNECTION_COMMAND_SPECS = [
|
|
|
42705
43298
|
returns: "MoneyConnectionView[]",
|
|
42706
43299
|
notes: `${routeOf(bankLinkOperations.link)}. Each do account is fed by one bank account.`
|
|
42707
43300
|
},
|
|
43301
|
+
{
|
|
43302
|
+
path: ["money", "kraken-link", "status"],
|
|
43303
|
+
summary: "Your Kraken link: the account it feeds, how its last sync went, and the holdings it saw.",
|
|
43304
|
+
args: [],
|
|
43305
|
+
flags: [],
|
|
43306
|
+
scopes: scopesOf(krakenLinkOperations.status),
|
|
43307
|
+
returns: "MoneyKrakenLinkStatus",
|
|
43308
|
+
notes: `${routeOf(krakenLinkOperations.status)}. The key is never returned.`
|
|
43309
|
+
},
|
|
43310
|
+
{
|
|
43311
|
+
path: ["money", "kraken-link", "set"],
|
|
43312
|
+
summary: "Give do your read-only Kraken API key, read from a file, and sync.",
|
|
43313
|
+
args: [],
|
|
43314
|
+
flags: [
|
|
43315
|
+
{
|
|
43316
|
+
name: "key-file",
|
|
43317
|
+
type: "string",
|
|
43318
|
+
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."
|
|
43319
|
+
},
|
|
43320
|
+
{
|
|
43321
|
+
name: "account",
|
|
43322
|
+
type: "string",
|
|
43323
|
+
description: "The EUR investment account it feeds. Left out: the link's account, or a new one named Kraken."
|
|
43324
|
+
}
|
|
43325
|
+
],
|
|
43326
|
+
scopes: scopesOf(krakenLinkOperations.set),
|
|
43327
|
+
returns: "MoneyKrakenLinkStatus",
|
|
43328
|
+
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.`
|
|
43329
|
+
},
|
|
43330
|
+
{
|
|
43331
|
+
path: ["money", "kraken-link", "sync"],
|
|
43332
|
+
summary: "Read your Kraken holdings now and update the account's balance.",
|
|
43333
|
+
args: [],
|
|
43334
|
+
flags: [],
|
|
43335
|
+
scopes: scopesOf(krakenLinkOperations.sync),
|
|
43336
|
+
returns: "MoneyKrakenLinkStatus",
|
|
43337
|
+
notes: `${routeOf(krakenLinkOperations.sync)}. A failure keeps the last holdings and shows on the link as failing, with its reason.`
|
|
43338
|
+
},
|
|
43339
|
+
{
|
|
43340
|
+
path: ["money", "kraken-link", "remove"],
|
|
43341
|
+
summary: "Forget your Kraken key. The account keeps its last balance and holdings.",
|
|
43342
|
+
args: [],
|
|
43343
|
+
flags: [DESTRUCTIVE],
|
|
43344
|
+
scopes: scopesOf(krakenLinkOperations.remove),
|
|
43345
|
+
returns: "MoneyKrakenLinkStatus",
|
|
43346
|
+
notes: `${routeOf(krakenLinkOperations.remove)}. Gated behind --allow-destructive.`
|
|
43347
|
+
},
|
|
42708
43348
|
{
|
|
42709
43349
|
path: ["doccle", "connect"],
|
|
42710
43350
|
summary: "Give do your Doccle login so the nightly import can run. do checks it with Doccle first and keeps it sealed.",
|
|
@@ -42811,8 +43451,14 @@ var CONNECTION_HANDLERS = {
|
|
|
42811
43451
|
},
|
|
42812
43452
|
"money bank-link pending": async ({ args, requireClient }) => {
|
|
42813
43453
|
const id = args[0];
|
|
42814
|
-
if (!id)
|
|
42815
|
-
|
|
43454
|
+
if (!id) {
|
|
43455
|
+
return {
|
|
43456
|
+
data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pendings))),
|
|
43457
|
+
render: (data, mode) => Array.isArray(data) ? data.length === 0 ? "No bank links waiting to be mapped." : data.map((p) => renderPending(p, mode)).join(`
|
|
43458
|
+
|
|
43459
|
+
`) : renderRecord(data, mode)
|
|
43460
|
+
};
|
|
43461
|
+
}
|
|
42816
43462
|
return {
|
|
42817
43463
|
data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pending, { id }))),
|
|
42818
43464
|
render: renderPending
|
|
@@ -42827,6 +43473,32 @@ var CONNECTION_HANDLERS = {
|
|
|
42827
43473
|
`) || "Linked."
|
|
42828
43474
|
};
|
|
42829
43475
|
},
|
|
43476
|
+
"money kraken-link status": async ({ requireClient }) => ({
|
|
43477
|
+
data: await guarded([], () => requireClient().get(pathOf(krakenLinkOperations.status))),
|
|
43478
|
+
render: renderKraken
|
|
43479
|
+
}),
|
|
43480
|
+
"money kraken-link set": async ({ io, flags, requireClient }) => {
|
|
43481
|
+
refuseSecretFlags(flags, ["key", "api-key", "secret", "api-secret", "private-key"], "--key-file <path>");
|
|
43482
|
+
const text = await readSecret(io, flags, "key-file", "the Kraken key", "all");
|
|
43483
|
+
const { apiKey, apiSecret } = parseKrakenKey(text);
|
|
43484
|
+
const account = flagString(flags, "account")?.trim();
|
|
43485
|
+
const body = { apiKey, apiSecret, ...account ? { accountId: account } : {} };
|
|
43486
|
+
return {
|
|
43487
|
+
data: await guarded([apiKey, apiSecret], () => requireClient().put(pathOf(krakenLinkOperations.set), body)),
|
|
43488
|
+
render: renderKraken
|
|
43489
|
+
};
|
|
43490
|
+
},
|
|
43491
|
+
"money kraken-link sync": async ({ requireClient }) => ({
|
|
43492
|
+
data: await guarded([], () => requireClient().post(pathOf(krakenLinkOperations.sync), {})),
|
|
43493
|
+
render: renderKraken
|
|
43494
|
+
}),
|
|
43495
|
+
"money kraken-link remove": async ({ flags, requireClient }) => {
|
|
43496
|
+
assertDestructive(flags, "forget the Kraken key");
|
|
43497
|
+
return {
|
|
43498
|
+
data: await guarded([], () => requireClient().delete(pathOf(krakenLinkOperations.remove))),
|
|
43499
|
+
render: renderKraken
|
|
43500
|
+
};
|
|
43501
|
+
},
|
|
42830
43502
|
"doccle connect": (ctx) => connectDoccle(ctx),
|
|
42831
43503
|
"steam link": async ({ io, flags, requireClient }) => {
|
|
42832
43504
|
refuseSecretFlags(flags, ["key", "steam-key", "web-api-key"], "--key-file <path>");
|
|
@@ -43003,7 +43675,8 @@ function renderPending(data, mode) {
|
|
|
43003
43675
|
const p = data;
|
|
43004
43676
|
if (!Array.isArray(p?.accounts))
|
|
43005
43677
|
return renderRecord(data, mode);
|
|
43006
|
-
const
|
|
43678
|
+
const until = p.expiresAt ? ` \xB7 map before ${p.expiresAt}` : "";
|
|
43679
|
+
const lines = [`${p.bank ?? "Bank"} \xB7 pending ${p.pendingId}${until}`];
|
|
43007
43680
|
for (const a of p.accounts) {
|
|
43008
43681
|
const label = [a.name, a.ibanMasked].filter(Boolean).join(" ") || "account";
|
|
43009
43682
|
const hint = a.suggestedAccountId ? ` \u2192 ${a.suggestedAccountId}` : "";
|
|
@@ -43012,6 +43685,48 @@ function renderPending(data, mode) {
|
|
|
43012
43685
|
return lines.join(`
|
|
43013
43686
|
`);
|
|
43014
43687
|
}
|
|
43688
|
+
function parseKrakenKey(text) {
|
|
43689
|
+
const hint = "the key file should hold the API key and its private key: two lines, or JSON with apiKey and apiSecret";
|
|
43690
|
+
if (text.startsWith("{")) {
|
|
43691
|
+
let parsed;
|
|
43692
|
+
try {
|
|
43693
|
+
parsed = JSON.parse(text);
|
|
43694
|
+
} catch {
|
|
43695
|
+
throw usageError(hint);
|
|
43696
|
+
}
|
|
43697
|
+
const o = parsed ?? {};
|
|
43698
|
+
const apiKey = o.apiKey ?? o.key;
|
|
43699
|
+
const apiSecret = o.apiSecret ?? o.secret ?? o.privateKey;
|
|
43700
|
+
if (typeof apiKey !== "string" || typeof apiSecret !== "string")
|
|
43701
|
+
throw usageError(hint);
|
|
43702
|
+
return { apiKey: apiKey.trim(), apiSecret: apiSecret.trim() };
|
|
43703
|
+
}
|
|
43704
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
43705
|
+
if (lines.length !== 2)
|
|
43706
|
+
throw usageError(hint);
|
|
43707
|
+
return { apiKey: lines[0], apiSecret: lines[1] };
|
|
43708
|
+
}
|
|
43709
|
+
function renderKraken(data, mode) {
|
|
43710
|
+
const k = data;
|
|
43711
|
+
if (typeof k?.configured !== "boolean")
|
|
43712
|
+
return renderRecord(data, mode);
|
|
43713
|
+
if (!k.link)
|
|
43714
|
+
return k.configured ? "Kraken key set, not linked yet." : "No Kraken key set.";
|
|
43715
|
+
const synced = k.link.lastSyncAt ? ` \xB7 synced ${k.link.lastSyncAt}` : " \xB7 not synced yet";
|
|
43716
|
+
const lines = [`Kraken \u2192 ${k.link.accountTitle} \xB7 ${k.link.status}${synced}`];
|
|
43717
|
+
if (k.link.lastError)
|
|
43718
|
+
lines.push(` ${k.link.lastError}`);
|
|
43719
|
+
if (k.holdings) {
|
|
43720
|
+
lines.push(`Holdings at ${k.holdings.at} \xB7 \u20AC ${k.holdings.totalEur}`);
|
|
43721
|
+
for (const h of k.holdings.holdings) {
|
|
43722
|
+
const value = h.valueEur === null ? "no EUR price" : `\u20AC ${h.valueEur}`;
|
|
43723
|
+
const share = h.share === null ? "" : ` \xB7 ${(h.share * 100).toFixed(1)}%`;
|
|
43724
|
+
lines.push(` ${h.asset.padEnd(6)} ${h.amount} ${value}${share}`);
|
|
43725
|
+
}
|
|
43726
|
+
}
|
|
43727
|
+
return lines.join(`
|
|
43728
|
+
`);
|
|
43729
|
+
}
|
|
43015
43730
|
var commands10 = pairCommands(CONNECTION_COMMAND_SPECS, CONNECTION_HANDLERS);
|
|
43016
43731
|
|
|
43017
43732
|
// src/commands/contacts.ts
|
|
@@ -46416,7 +47131,8 @@ import { createHash } from "crypto";
|
|
|
46416
47131
|
import { readdirSync, readFileSync as readFileSync7 } from "fs";
|
|
46417
47132
|
import { join as join2 } from "path";
|
|
46418
47133
|
var DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.md$/;
|
|
46419
|
-
var
|
|
47134
|
+
var SCORE = "(?:10|[1-9])";
|
|
47135
|
+
var RATING = new RegExp(`(?:\\p{L}+\\s*\\()?(?<![\\p{L}\\p{N}_./~-])(~)?\\s*(${SCORE}(?:\\.\\d)?)(?:\\s*[-\u2013]\\s*(${SCORE}))?\\s*/\\s*10(?![\\p{L}\\p{N}_/-]|[.,]\\d)\\)?`, "u");
|
|
46420
47136
|
var HEALTH = /\b(symptoms?|stomach|nausea|headaches?|migraine|doctor|GP|medication|meds|pills?|blood|fever|pain|clinic|hospital)\b/i;
|
|
46421
47137
|
var NOISE = /\b(PR monitor|GitHub daily|deploy(?:ment)? watch\w*|cron|heartbeat|sales prospects?)\b/i;
|
|
46422
47138
|
var MILESTONE = /\b(first|went live|is live|win:|moved in|arrived|wedding|engaged)\b/i;
|
|
@@ -46485,8 +47201,8 @@ function parseDailyNote(path, day, text) {
|
|
|
46485
47201
|
const ratingLine = all.find((l) => /rating/i.test(l) && RATING.test(l)) ?? all.find((l) => RATING.test(l));
|
|
46486
47202
|
const match = ratingLine ? RATING.exec(ratingLine) : null;
|
|
46487
47203
|
if (match) {
|
|
46488
|
-
const [whole, approx, low, high] = match;
|
|
46489
|
-
if (approx || high)
|
|
47204
|
+
const [whole, approx, low = "", high] = match;
|
|
47205
|
+
if (approx || high || low.includes("."))
|
|
46490
47206
|
ratingQuestion = whole.trim();
|
|
46491
47207
|
else
|
|
46492
47208
|
rating = Number(low);
|
|
@@ -46508,7 +47224,7 @@ function parseDailyNote(path, day, text) {
|
|
|
46508
47224
|
inCheck = false;
|
|
46509
47225
|
if (line === ratingLine || /how was (the|your) day/i.test(line))
|
|
46510
47226
|
continue;
|
|
46511
|
-
if (HEALTH.test(line) || NOISE.test(line))
|
|
47227
|
+
if (HEALTH.test(line) || NOISE.test(line) || isPrivateLine(line))
|
|
46512
47228
|
continue;
|
|
46513
47229
|
kept.push(line);
|
|
46514
47230
|
}
|
|
@@ -46518,8 +47234,13 @@ function parseDailyNote(path, day, text) {
|
|
|
46518
47234
|
`).trim() || null;
|
|
46519
47235
|
const firstProse = (body ?? "").split(/\n\s*\n/).map((p) => plain(p)).find((p) => p.length > 0);
|
|
46520
47236
|
title = firstProse ? firstSentence(firstProse.replace(/^The day\s*/i, "")) : null;
|
|
47237
|
+
} else {
|
|
47238
|
+
body = all.filter((l) => !/^#\s/.test(l) && l !== ratingLine && !/how was (the|your) day/i.test(l) && !HEALTH.test(l) && !NOISE.test(l) && !isPrivateLine(l)).join(`
|
|
47239
|
+
`).replace(/\n{3,}/g, `
|
|
47240
|
+
|
|
47241
|
+
`).trim() || null;
|
|
46521
47242
|
}
|
|
46522
|
-
const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && plain(l).length > 0);
|
|
47243
|
+
const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && !isPrivateLine(l) && plain(l).length > 0);
|
|
46523
47244
|
const highlight = milestoneLine ? firstSentence(plain(milestoneLine), 80) : null;
|
|
46524
47245
|
const sleep = sleepOf(all);
|
|
46525
47246
|
const prs = (text.match(PR_REF) ?? []).length;
|
|
@@ -46900,7 +47621,7 @@ var commands24 = [
|
|
|
46900
47621
|
description: "How the days name their folder (default memory/daily)."
|
|
46901
47622
|
}
|
|
46902
47623
|
],
|
|
46903
|
-
notes: ", 400 days a request. The day's '## Day summary' becomes its entry; a clear 'n/10' rating is kept, '~5/10' or '3-4/10' becomes a question; health and automation lines never reach the text. Nothing is written into the Journal until
|
|
47624
|
+
notes: ", 400 days a request. The day's '## Day summary' becomes its entry; a clear 'n/10' rating (n 1 to 10, standing on its own) is kept, '~5/10' or '3-4/10' becomes a question; health and automation lines never reach the text, and lines about drinking, smoking, drugs, relapse or fertility never reach the title, text or highlight (the API strips them too). A day without a summary gets its kept lines as its text. Nothing is written into the Journal until a month is approved (`do journal import approve`, or the member in the app). Re-running replaces what's still waiting.",
|
|
46904
47625
|
run: async ({ args, flags, requireClient }) => {
|
|
46905
47626
|
const dir = requireArg(args, 0, "dir");
|
|
46906
47627
|
const items = scanDailyNotes(dir, flagString(flags, "label") ?? "memory/daily");
|
|
@@ -46925,6 +47646,42 @@ var commands24 = [
|
|
|
46925
47646
|
};
|
|
46926
47647
|
}
|
|
46927
47648
|
}),
|
|
47649
|
+
defineCommand({
|
|
47650
|
+
operation: journalOperations.approveImport,
|
|
47651
|
+
path: ["journal", "import", "approve"],
|
|
47652
|
+
summary: "Approve staged days into the Journal: one month's batch, or --all; --rating answers a fuzzy rating.",
|
|
47653
|
+
args: [{ name: "batch", required: false, description: "The month to approve, YYYY-MM." }],
|
|
47654
|
+
flags: [
|
|
47655
|
+
{ name: "all", type: "boolean", description: "Approve every staged batch." },
|
|
47656
|
+
{
|
|
47657
|
+
name: "rating",
|
|
47658
|
+
type: "string",
|
|
47659
|
+
description: "Answer a day's rating question, YYYY-MM-DD=N (1 to 10); repeat per day."
|
|
47660
|
+
}
|
|
47661
|
+
],
|
|
47662
|
+
notes: ". A day whose rating question has no --rating stays staged and is listed as open; the rest are written as imports. An agent's approve shows in the member's change list with Undo. Lines about drinking, smoking, drugs, relapse or fertility are never written.",
|
|
47663
|
+
run: async ({ args, flags, requireClient }) => {
|
|
47664
|
+
const batch = args[0];
|
|
47665
|
+
const all = flagBool(flags, "all");
|
|
47666
|
+
if (batch === undefined === !all) {
|
|
47667
|
+
throw usageError("approve one month (YYYY-MM) or --all");
|
|
47668
|
+
}
|
|
47669
|
+
const ratings = flagStrings(flags, "rating").map((answer) => {
|
|
47670
|
+
const m = /^(\d{4}-\d{2}-\d{2})=(\d+(?:\.\d+)?)$/.exec(answer.trim());
|
|
47671
|
+
if (!m)
|
|
47672
|
+
throw usageError(`--rating takes YYYY-MM-DD=N, got "${answer}"`);
|
|
47673
|
+
return { day: m[1], rating: Number(m[2]) };
|
|
47674
|
+
});
|
|
47675
|
+
const input = parseInput(journalImportApproveInputSchema, {
|
|
47676
|
+
...all ? { all: true } : { batch },
|
|
47677
|
+
ratings
|
|
47678
|
+
});
|
|
47679
|
+
return {
|
|
47680
|
+
data: await requireClient().post(pathOf(journalOperations.approveImport), input),
|
|
47681
|
+
render: renderJournalImportApprove
|
|
47682
|
+
};
|
|
47683
|
+
}
|
|
47684
|
+
}),
|
|
46928
47685
|
defineCommand({
|
|
46929
47686
|
operation: journalOperations.importOverview,
|
|
46930
47687
|
path: ["journal", "import", "status"],
|
|
@@ -47811,6 +48568,87 @@ var commands30 = [
|
|
|
47811
48568
|
// src/commands/money.ts
|
|
47812
48569
|
import { readFileSync as readFileSync8 } from "fs";
|
|
47813
48570
|
import { basename as basename5 } from "path";
|
|
48571
|
+
|
|
48572
|
+
// ../../packages/domain/src/money.ts
|
|
48573
|
+
var toNumber = (cents) => Number(cents) / 100;
|
|
48574
|
+
function formatMoney(amount, {
|
|
48575
|
+
currency = "EUR",
|
|
48576
|
+
signed = false,
|
|
48577
|
+
whole = false,
|
|
48578
|
+
cents: always = false,
|
|
48579
|
+
locale
|
|
48580
|
+
} = {}) {
|
|
48581
|
+
const exact = typeof amount === "bigint" ? amount : amountToCents(amount);
|
|
48582
|
+
const cents = whole ? roundToEuro(exact) : exact;
|
|
48583
|
+
const abs = cents < 0n ? -cents : cents;
|
|
48584
|
+
const text = new Intl.NumberFormat(locale ?? "en-IE", {
|
|
48585
|
+
style: "currency",
|
|
48586
|
+
currency,
|
|
48587
|
+
minimumFractionDigits: always || abs % 100n !== 0n ? 2 : 0,
|
|
48588
|
+
maximumFractionDigits: 2
|
|
48589
|
+
}).format(toNumber(abs));
|
|
48590
|
+
if (cents < 0n)
|
|
48591
|
+
return `\u2212${text}`;
|
|
48592
|
+
return signed && cents > 0n ? `+${text}` : text;
|
|
48593
|
+
}
|
|
48594
|
+
function roundToEuro(cents) {
|
|
48595
|
+
const abs = cents < 0n ? -cents : cents;
|
|
48596
|
+
const rounded = (abs + 50n) / 100n * 100n;
|
|
48597
|
+
return cents < 0n ? -rounded : rounded;
|
|
48598
|
+
}
|
|
48599
|
+
function goalSavedSource(goal) {
|
|
48600
|
+
const account = goal.accountTitle ?? "the account";
|
|
48601
|
+
if (goal.savedFrom === "account")
|
|
48602
|
+
return `all of ${account}`;
|
|
48603
|
+
if (goal.savedFrom === "account_share")
|
|
48604
|
+
return `its share of ${account}`;
|
|
48605
|
+
if (goal.accountTitle && goal.accountBalanceKnown === false) {
|
|
48606
|
+
return `counted toward it; the balance of ${goal.accountTitle} is unknown`;
|
|
48607
|
+
}
|
|
48608
|
+
return "counted toward it";
|
|
48609
|
+
}
|
|
48610
|
+
|
|
48611
|
+
// src/commands/money.ts
|
|
48612
|
+
function renderDedupe(data) {
|
|
48613
|
+
const r = data;
|
|
48614
|
+
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" : ""}`);
|
|
48615
|
+
const c = r.changed;
|
|
48616
|
+
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)");
|
|
48617
|
+
return lines.join(`
|
|
48618
|
+
`);
|
|
48619
|
+
}
|
|
48620
|
+
function renderGoals(data) {
|
|
48621
|
+
const goals = Array.isArray(data) ? data : [];
|
|
48622
|
+
if (goals.length === 0)
|
|
48623
|
+
return "(no goals yet)";
|
|
48624
|
+
return goals.map((g) => {
|
|
48625
|
+
const whole = (a) => formatMoney(a, { whole: true });
|
|
48626
|
+
const parts = [
|
|
48627
|
+
`${whole(g.saved)} of ${whole(g.targetAmount)} (${goalSavedSource(g)})`,
|
|
48628
|
+
Number(g.remaining) > 0 ? `${whole(g.remaining)} to go` : "reached",
|
|
48629
|
+
g.monthlyNeed && g.targetOn ? `${formatMoney(g.monthlyNeed)} a month by ${g.targetOn}` : null
|
|
48630
|
+
];
|
|
48631
|
+
return `${g.title} ${parts.filter(Boolean).join(" \xB7 ")} ${g.id}`;
|
|
48632
|
+
}).join(`
|
|
48633
|
+
`);
|
|
48634
|
+
}
|
|
48635
|
+
function syncLine(r) {
|
|
48636
|
+
const when = r.startedAt ? `${String(r.startedAt).slice(0, 16).replace("T", " ")} ` : "";
|
|
48637
|
+
const what = r.accountTitle ? `${r.accountTitle} ` : "";
|
|
48638
|
+
const counts = `fetched ${r.fetched ?? 0} \xB7 imported ${r.imported ?? 0} \xB7 matched ${r.matched ?? 0} \xB7 duplicates ${r.duplicates ?? 0}`;
|
|
48639
|
+
return `${when}${what}${r.outcome ?? "?"} ${counts}${r.error ? `
|
|
48640
|
+
${r.error}` : ""}`;
|
|
48641
|
+
}
|
|
48642
|
+
function renderSyncLog(data) {
|
|
48643
|
+
const rows = Array.isArray(data) ? data : data.data ?? [];
|
|
48644
|
+
return rows.length === 0 ? "(no syncs yet)" : rows.map(syncLine).join(`
|
|
48645
|
+
`);
|
|
48646
|
+
}
|
|
48647
|
+
function renderSyncResults(data) {
|
|
48648
|
+
const { results } = data;
|
|
48649
|
+
return results.length === 0 ? "(no live bank links)" : results.map(syncLine).join(`
|
|
48650
|
+
`);
|
|
48651
|
+
}
|
|
47814
48652
|
function partyFlag(flags) {
|
|
47815
48653
|
const userId = flagString(flags, "with-user");
|
|
47816
48654
|
const contactId = flagString(flags, "with-contact");
|
|
@@ -47871,13 +48709,34 @@ var commands31 = [
|
|
|
47871
48709
|
defineCommand({
|
|
47872
48710
|
operation: moneyOperations.updateAccount,
|
|
47873
48711
|
path: ["money", "account-update"],
|
|
47874
|
-
summary: "Edit a money account.",
|
|
48712
|
+
summary: "Edit a money account, or state its balance (for an export with no balance column).",
|
|
47875
48713
|
args: [{ name: "id", required: true, description: "Account ULID." }],
|
|
47876
|
-
flags: [
|
|
48714
|
+
flags: [
|
|
48715
|
+
{ name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." },
|
|
48716
|
+
{
|
|
48717
|
+
name: "balance",
|
|
48718
|
+
type: "string",
|
|
48719
|
+
description: "What the account holds, e.g. 1234.56 (sets balance)."
|
|
48720
|
+
},
|
|
48721
|
+
{
|
|
48722
|
+
name: "balance-at",
|
|
48723
|
+
type: "string",
|
|
48724
|
+
description: "The day (YYYY-MM-DD, its end) or moment that balance is from (default today)."
|
|
48725
|
+
}
|
|
48726
|
+
],
|
|
47877
48727
|
returns: "MoneyAccountView",
|
|
47878
|
-
|
|
48728
|
+
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).",
|
|
48729
|
+
run: async ({ args, flags, io, requireClient }) => {
|
|
47879
48730
|
const id = requireArg(args, 0, "account id");
|
|
47880
|
-
const
|
|
48731
|
+
const balance = flagString(flags, "balance");
|
|
48732
|
+
const balanceAt = flagString(flags, "balance-at");
|
|
48733
|
+
if (balanceAt !== undefined && balance === undefined) {
|
|
48734
|
+
throw usageError("--balance-at goes with --balance");
|
|
48735
|
+
}
|
|
48736
|
+
const body = parseInput(updateMoneyAccountInputSchema, {
|
|
48737
|
+
...readDataFlag(flags),
|
|
48738
|
+
...balance !== undefined ? { balance, balanceAt: balanceAt ?? isoDay(io.now()) } : {}
|
|
48739
|
+
});
|
|
47881
48740
|
return {
|
|
47882
48741
|
data: await requireClient().patch(pathOf(moneyOperations.updateAccount, { id }), body),
|
|
47883
48742
|
render: renderRecord
|
|
@@ -47926,7 +48785,33 @@ var commands31 = [
|
|
|
47926
48785
|
});
|
|
47927
48786
|
return {
|
|
47928
48787
|
data: await requireClient().post(pathOf(moneyOperations.syncConnections), body),
|
|
47929
|
-
render:
|
|
48788
|
+
render: renderSyncResults
|
|
48789
|
+
};
|
|
48790
|
+
}
|
|
48791
|
+
}),
|
|
48792
|
+
defineCommand({
|
|
48793
|
+
operation: moneyOperations.dedupe,
|
|
48794
|
+
path: ["money", "dedupe"],
|
|
48795
|
+
summary: "Find movements booked twice on an account (a statement's row and a bank link's twin); --commit folds them.",
|
|
48796
|
+
args: [],
|
|
48797
|
+
flags: [
|
|
48798
|
+
{ name: "account", type: "string", description: "The account ULID (required)." },
|
|
48799
|
+
{
|
|
48800
|
+
name: "commit",
|
|
48801
|
+
type: "boolean",
|
|
48802
|
+
description: "Fold the pairs; without it, a preview that writes nothing."
|
|
48803
|
+
}
|
|
48804
|
+
],
|
|
48805
|
+
returns: "MoneyDedupeResult",
|
|
48806
|
+
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.",
|
|
48807
|
+
run: async ({ flags, requireClient }) => {
|
|
48808
|
+
const body = parseInput(moneyDedupeInputSchema, {
|
|
48809
|
+
accountId: requireFlag(flags, "account", "say which account: --account <id>"),
|
|
48810
|
+
...flagBool(flags, "commit") ? { commit: true } : {}
|
|
48811
|
+
});
|
|
48812
|
+
return {
|
|
48813
|
+
data: await requireClient().post(pathOf(moneyOperations.dedupe), body),
|
|
48814
|
+
render: renderDedupe
|
|
47930
48815
|
};
|
|
47931
48816
|
}
|
|
47932
48817
|
}),
|
|
@@ -47937,7 +48822,7 @@ var commands31 = [
|
|
|
47937
48822
|
args: [{ name: "id", required: true, description: "Bank link (connection) ULID." }],
|
|
47938
48823
|
flags: [],
|
|
47939
48824
|
returns: "MoneyConnectionSync[]",
|
|
47940
|
-
run: readOperation(moneyOperations.listConnectionSyncs,
|
|
48825
|
+
run: readOperation(moneyOperations.listConnectionSyncs, renderSyncLog, { id: "id" })
|
|
47941
48826
|
}),
|
|
47942
48827
|
defineCommand({
|
|
47943
48828
|
operation: moneyOperations.listCategories,
|
|
@@ -47990,9 +48875,9 @@ var commands31 = [
|
|
|
47990
48875
|
{ name: "cursor", type: "string", description: "The previous page's nextCursor." }
|
|
47991
48876
|
],
|
|
47992
48877
|
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.",
|
|
48878
|
+
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
48879
|
run: async ({ flags, requireClient }) => ({
|
|
47995
|
-
data: await requireClient().
|
|
48880
|
+
data: await requireClient().getPage(pathOf(moneyOperations.listTransactions), {
|
|
47996
48881
|
query: {
|
|
47997
48882
|
accountId: flagString(flags, "account"),
|
|
47998
48883
|
status: flagString(flags, "status"),
|
|
@@ -48456,12 +49341,22 @@ var commands31 = [
|
|
|
48456
49341
|
flags: [
|
|
48457
49342
|
{ name: "account", type: "string", description: "The account's ULID." },
|
|
48458
49343
|
{ 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)." }
|
|
49344
|
+
{ name: "commit", type: "boolean", description: "Book the new lines (else a preview)." },
|
|
49345
|
+
{
|
|
49346
|
+
name: "balance",
|
|
49347
|
+
type: "string",
|
|
49348
|
+
description: "What the account holds, for an export with no balance column (anchors it with --commit)."
|
|
49349
|
+
},
|
|
49350
|
+
{
|
|
49351
|
+
name: "balance-at",
|
|
49352
|
+
type: "string",
|
|
49353
|
+
description: "The day (YYYY-MM-DD, its end) or moment --balance is from (default today)."
|
|
49354
|
+
}
|
|
48460
49355
|
],
|
|
48461
49356
|
scopes: [...moneyOperations.previewImport.scopes, ...moneyOperations.commitImport.scopes],
|
|
48462
49357
|
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 }) => {
|
|
49358
|
+
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).`,
|
|
49359
|
+
run: async ({ args, flags, io, requireClient }) => {
|
|
48465
49360
|
const path = requireArg(args, 0, "path to the statement CSV");
|
|
48466
49361
|
const account = requireFlag(flags, "account", "money import needs --account <account id>");
|
|
48467
49362
|
let bytes;
|
|
@@ -48475,6 +49370,19 @@ var commands31 = [
|
|
|
48475
49370
|
const since = flagString(flags, "since");
|
|
48476
49371
|
if (since !== undefined)
|
|
48477
49372
|
form.append("since", since);
|
|
49373
|
+
const balance = flagString(flags, "balance");
|
|
49374
|
+
const balanceAt = flagString(flags, "balance-at");
|
|
49375
|
+
if (balanceAt !== undefined && balance === undefined) {
|
|
49376
|
+
throw usageError("--balance-at goes with --balance");
|
|
49377
|
+
}
|
|
49378
|
+
if (balance !== undefined) {
|
|
49379
|
+
const anchor = parseInput(updateMoneyAccountInputSchema, {
|
|
49380
|
+
balance,
|
|
49381
|
+
balanceAt: balanceAt ?? isoDay(io.now())
|
|
49382
|
+
});
|
|
49383
|
+
form.append("balance", anchor.balance);
|
|
49384
|
+
form.append("balanceAt", anchor.balanceAt);
|
|
49385
|
+
}
|
|
48478
49386
|
const name = basename5(path);
|
|
48479
49387
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/csv" }), name);
|
|
48480
49388
|
const op = flagBool(flags, "commit") ? moneyOperations.commitImport : moneyOperations.previewImport;
|
|
@@ -48623,7 +49531,7 @@ var commands31 = [
|
|
|
48623
49531
|
}
|
|
48624
49532
|
],
|
|
48625
49533
|
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.",
|
|
49534
|
+
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
49535
|
run: async ({ flags, requireClient }) => ({
|
|
48628
49536
|
data: await requireClient().get(pathOf(moneyOperations.netWorthRead), {
|
|
48629
49537
|
query: { scope: flagString(flags, "scope"), months: flagString(flags, "months") }
|
|
@@ -48662,7 +49570,8 @@ var commands31 = [
|
|
|
48662
49570
|
args: [],
|
|
48663
49571
|
flags: [],
|
|
48664
49572
|
returns: "MoneyGoalView[]",
|
|
48665
|
-
|
|
49573
|
+
notes: ". `saved` reads `savedFrom`: `account` (the only goal in its account: the balance), `account_share` (several share one: each its own, capped by what's left, soonest target first) or `contributions` (savedBefore plus what's counted toward it; also when the account's balance is unknown, `accountBalanceKnown: false`). `remaining` is the target less saved; `monthlyNeed` spreads it over the months to `targetOn`.",
|
|
49574
|
+
run: readOperation(moneyOperations.listGoals, renderGoals)
|
|
48666
49575
|
}),
|
|
48667
49576
|
defineCommand({
|
|
48668
49577
|
operation: moneyOperations.getGoal,
|
|
@@ -51694,7 +52603,7 @@ var commands49 = [
|
|
|
51694
52603
|
"#tag": "Adds a tag. Repeatable: '#home #urgent'.",
|
|
51695
52604
|
"!": "Stars the task \u2014 the 1\u20132 things that matter (aliases: !star/!s/!flag/!f). Drives Today's focus and the starred filter.",
|
|
51696
52605
|
"~area": "Files it into an existing Area by its title, case-insensitive: '~garden' for 'Garden'. One word (letters, digits, - and _), so an Area named with spaces can't be named here \u2014 file it after with `do tasks update <id> --data '{\"areaId\":\"<id>\"}'`. An unknown name files nothing (never creates an Area); the first ~area wins. Valid names: `do areas list --json` (their `title`).",
|
|
51697
|
-
when: "The planned day and time: 'today', 'tomorrow 3pm', 'fri', 'next monday', 'in 3 days', 'at 16:00', 'tonight' (18:00); the past for back-filling: 'yesterday', 'last friday'. A time makes it a timed task (allDay false). 'every monday' / 'every 2 weeks' / 'daily' attaches a recurrence.",
|
|
52606
|
+
when: "The planned day and time: 'today', 'tomorrow 3pm', 'fri', 'next monday', 'in 3 days', 'at 16:00', 'tonight' (18:00); the past for back-filling: 'yesterday', 'last friday'. A time makes it a timed task (allDay false). 'every monday' / 'every 2 weeks' / 'daily' / 'every first sunday' / 'every last friday' attaches a recurrence.",
|
|
51698
52607
|
"by / due": "The Deadline, never the plan: 'by friday', 'due fri', 'due tomorrow 5pm', 'by 17:00' (today). 'by' with no date after it ('pay by card') stays in the title.",
|
|
51699
52608
|
estimate: "How long it takes (durationMinutes): '30m', '45 min', '1h', '1.5h', '1h30', '2 hours', 'for 20 mins'. Not 'in 2 hours'."
|
|
51700
52609
|
}
|