@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.
Files changed (2) hide show
  1. package/dist/do.js +977 -68
  2. 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
- flags[body.slice(0, eq)] = body.slice(eq + 1);
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
- flags[body] = next;
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.332.0";
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: string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -").nullish(),
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
- phone: string2().nullable(),
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(byDayCodeSchema).optional(),
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 updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().strict();
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
- }).strict();
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: string2().trim().min(1).max(200).nullish(),
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: string2().trim().min(1).max(80).nullish(),
31473
+ ratingQuestion: keptLine(80),
31158
31474
  sleep: number2().min(0).max(24).nullish(),
31159
- highlight: string2().trim().min(1).max(200).nullish(),
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: the member reviews and approves each month in the app, and a key can't approve. A day still waiting is replaced; one already imported is left alone (`alreadyImported`). Staged days stand behind the Journal's wall.",
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, amount and counterparty or reference, is matched instead of 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.",
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; 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. `scope`: `all` (default), `space` or `owner`; `months` 2\u201360 (12 by default).",
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: "A bank authorisation waiting to be mapped: its bank accounts and the suggestions.",
42686
- args: [{ name: "id", required: true, description: "The pendingId from `complete`." }],
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
- throw usageError("money bank-link pending needs the pendingId");
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 lines = [`${p.bank ?? "Bank"} \xB7 pending ${p.pendingId}`];
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 RATING = /(?:[A-Za-z]+\s*\()?(~)?\s*(\d{1,2}(?:\.\d)?)(?:\s*[-\u2013]\s*(\d{1,2}))?\s*\/\s*10\)?/;
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 the member approves the month (the app only; a key can't approve). Re-running replaces what's still waiting.",
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: [{ name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." }],
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
- run: async ({ args, flags, requireClient }) => {
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 body = parseInput(updateMoneyAccountInputSchema, readDataFlag(flags));
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: renderRecord
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, renderList, { id: "id" })
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().get(pathOf(moneyOperations.listTransactions), {
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
- run: readOperation(moneyOperations.listGoals, renderList)
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
  }