@robbeverhelst/do 1.333.0 → 1.335.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 +1292 -85
  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.333.0";
14421
+ var VERSION = "1.335.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()
@@ -20424,7 +20433,8 @@ var animalDayCountSchema = object({ day: string2(), count: number2().int() });
20424
20433
  var animalWeightPointSchema = object({
20425
20434
  day: string2(),
20426
20435
  grams: number2(),
20427
- at: exports_coerce.date()
20436
+ at: exports_coerce.date(),
20437
+ note: string2().nullish()
20428
20438
  });
20429
20439
  var animalFileSchema = object({
20430
20440
  id: ulidSchema,
@@ -20545,6 +20555,7 @@ var animalLogInputSchema = discriminatedUnion("kind", [
20545
20555
  kind: literal("weight"),
20546
20556
  value: number2().positive().max(200000),
20547
20557
  unit: _enum2(["g", "kg"]).default("g"),
20558
+ note: string2().trim().min(1).max(120).optional(),
20548
20559
  occurredAt: occurredAtSchema
20549
20560
  }).strict(),
20550
20561
  object({
@@ -22030,13 +22041,25 @@ var occurrenceSchema = createSelectSchema(occurrences, {
22030
22041
  completedAt: exports_coerce.date().nullable()
22031
22042
  });
22032
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
+ }
22033
22056
  var recurrenceRuleParams = {
22034
22057
  timezone: string2().min(1).default("UTC"),
22035
22058
  interval: number2().int().positive().default(1),
22036
22059
  mode: recurrenceModeSchema,
22037
22060
  timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "time-of-day must be HH:MM (24h)").nullish(),
22038
22061
  freq: recurrenceFreqSchema.optional(),
22039
- byDay: array(byDayCodeSchema).optional(),
22062
+ byDay: array(byDaySchema).optional(),
22040
22063
  until: exports_coerce.date().optional(),
22041
22064
  count: number2().int().positive().optional(),
22042
22065
  unit: recurrenceUnitSchema.optional(),
@@ -22046,6 +22069,14 @@ function refineRecurrenceRule(v, ctx) {
22046
22069
  if (v.mode === "rigid" && !v.freq) {
22047
22070
  ctx.addIssue({ code: "custom", path: ["freq"], message: "rigid rules require a freq" });
22048
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
+ }
22049
22080
  if (v.mode === "flexible") {
22050
22081
  if (!v.unit) {
22051
22082
  ctx.addIssue({ code: "custom", path: ["unit"], message: "flexible rules require a unit" });
@@ -22437,6 +22468,8 @@ var collectionEntries = pgTable("collection_entries", {
22437
22468
  fileIds: text("file_ids").array().notNull().default([]),
22438
22469
  minutes: integer2("minutes"),
22439
22470
  ops: jsonb("ops").$type().notNull().default([]),
22471
+ chapter: text("chapter"),
22472
+ linkedNoteId: text("linked_note_id").references(() => nodes.id, { onDelete: "set null" }),
22440
22473
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
22441
22474
  }, (t) => [
22442
22475
  index("collection_entries_item_occurred_idx").on(t.itemId, t.occurredOn),
@@ -22548,8 +22581,11 @@ var collectionEntrySchema = createSelectSchema(collectionEntries, {
22548
22581
  taskId: string2().nullable().default(null),
22549
22582
  fileIds: array(string2()).default([]),
22550
22583
  minutes: number2().int().nullable().default(null),
22551
- ops: array(unknown()).default([])
22584
+ ops: array(unknown()).default([]),
22585
+ chapter: string2().nullable().default(null),
22586
+ linkedNoteId: string2().nullable().default(null)
22552
22587
  });
22588
+ var entryChapterSchema = string2().trim().min(1).max(60);
22553
22589
  var entryCostSchema = number2().min(0).max(1e7).multipleOf(0.01);
22554
22590
  var entryFileIdsSchema = array(ulidSchema).max(20);
22555
22591
  var createCollectionEntryInputSchema = createInsertSchema(collectionEntries, {
@@ -22561,7 +22597,9 @@ var createCollectionEntryInputSchema = createInsertSchema(collectionEntries, {
22561
22597
  cost: entryCostSchema.nullish(),
22562
22598
  contactId: ulidSchema.nullish(),
22563
22599
  taskId: ulidSchema.nullish(),
22564
- fileIds: entryFileIdsSchema.optional()
22600
+ fileIds: entryFileIdsSchema.optional(),
22601
+ chapter: entryChapterSchema.nullish(),
22602
+ linkedNoteId: ulidSchema.nullish()
22565
22603
  }).omit({ tenantId: true, itemId: true, createdAt: true, minutes: true, ops: true });
22566
22604
  var updateCollectionEntryInputSchema = object({
22567
22605
  occurredOn: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "must be an ISO date (YYYY-MM-DD)").optional(),
@@ -22570,7 +22608,9 @@ var updateCollectionEntryInputSchema = object({
22570
22608
  kind: collectionEntryKindSchema.optional(),
22571
22609
  cost: entryCostSchema.nullish(),
22572
22610
  contactId: ulidSchema.nullish(),
22573
- fileIds: entryFileIdsSchema.optional()
22611
+ fileIds: entryFileIdsSchema.optional(),
22612
+ chapter: entryChapterSchema.nullish(),
22613
+ linkedNoteId: ulidSchema.nullish()
22574
22614
  }).strict();
22575
22615
  var collectionItemViewSchema = collectionItemSchema.extend({
22576
22616
  title: string2(),
@@ -22643,6 +22683,7 @@ var collectionLogInputSchema = object({
22643
22683
  status: string2().trim().min(1).max(40).optional(),
22644
22684
  rating: collectionRatingSchema.nullish(),
22645
22685
  note: string2().max(2000).nullish(),
22686
+ chapter: entryChapterSchema.nullish(),
22646
22687
  said: string2().trim().min(1).max(500).optional(),
22647
22688
  occurredOn: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "must be an ISO date (YYYY-MM-DD)").optional()
22648
22689
  }).strict();
@@ -22874,6 +22915,12 @@ var moneyAccountKind = pgEnum("money_account_kind", [
22874
22915
  "pension",
22875
22916
  "asset_manual"
22876
22917
  ]);
22918
+ var moneyPensionSubtype = pgEnum("money_pension_subtype", [
22919
+ "group_insurance",
22920
+ "pension_savings_fund",
22921
+ "pension_savings_insurance",
22922
+ "vapz"
22923
+ ]);
22877
22924
  var moneyAccountHolder = pgEnum("money_account_holder", ["me", "joint", "other"]);
22878
22925
  var moneyAccountEntity = pgEnum("money_account_entity", ["personal", "business"]);
22879
22926
  var moneyImportSource = pgEnum("money_import_source", [
@@ -22925,6 +22972,8 @@ var moneyTaxTag = pgEnum("money_tax_tag", [
22925
22972
  var moneyAccounts = pgTable("money_accounts", {
22926
22973
  ...leafColumns(),
22927
22974
  kind: moneyAccountKind("kind").notNull().default("current"),
22975
+ pensionSubtype: moneyPensionSubtype("pension_subtype"),
22976
+ lockedUntil: date5("locked_until"),
22928
22977
  holder: moneyAccountHolder("holder").notNull().default("me"),
22929
22978
  entity: moneyAccountEntity("entity").notNull().default("personal"),
22930
22979
  institutionContactId: text("institution_contact_id").references(() => contacts.id, {
@@ -25161,6 +25210,7 @@ var fileViewLinkSchema = object({
25161
25210
 
25162
25211
  // ../../packages/schema/src/contracts/money.ts
25163
25212
  var moneyAccountKindSchema = _enum2(moneyAccountKind.enumValues);
25213
+ var moneyPensionSubtypeSchema = _enum2(moneyPensionSubtype.enumValues);
25164
25214
  var moneyAccountHolderSchema = _enum2(moneyAccountHolder.enumValues);
25165
25215
  var moneyAccountEntitySchema = _enum2(moneyAccountEntity.enumValues);
25166
25216
  var moneyImportSourceSchema = _enum2(moneyImportSource.enumValues);
@@ -25256,6 +25306,8 @@ var createMoneyAccountInputSchema = object({
25256
25306
  id: ulidSchema,
25257
25307
  title: string2().trim().min(1).max(120),
25258
25308
  kind: moneyAccountKindSchema.optional(),
25309
+ pensionSubtype: moneyPensionSubtypeSchema.nullish(),
25310
+ lockedUntil: keyDateDaySchema.nullish(),
25259
25311
  holder: moneyAccountHolderSchema.optional(),
25260
25312
  institutionContactId: ulidSchema.nullish(),
25261
25313
  iban: string2().trim().transform(compactIban).pipe(string2().regex(/^[A-Z]{2}\d{2}[A-Z0-9]{8,30}$/, "expected an IBAN")).nullish(),
@@ -25269,7 +25321,9 @@ var moneyBalanceAtSchema = union([keyDateDaySchema, exports_iso.datetime({ offse
25269
25321
  var updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().extend({
25270
25322
  balance: moneyAmountSchema.optional(),
25271
25323
  balanceAt: moneyBalanceAtSchema.optional()
25272
- }).strict().refine((u) => u.balance === undefined === (u.balanceAt === undefined), {
25324
+ }).strict().refine((u) => Object.values(u).some((v) => v !== undefined), {
25325
+ message: "nothing to change: give a field to set (or a balance with its balanceAt)"
25326
+ }).refine((u) => u.balance === undefined === (u.balanceAt === undefined), {
25273
25327
  message: "a balance and the day it's from go together",
25274
25328
  path: ["balanceAt"]
25275
25329
  });
@@ -25566,13 +25620,19 @@ var moneyGoalLineSchema = createSelectSchema(moneyGoalLines, {
25566
25620
  createdAt: exports_coerce.date(),
25567
25621
  updatedAt: exports_coerce.date()
25568
25622
  });
25623
+ var MONEY_GOAL_SAVED_FROM = ["account", "account_share", "contributions"];
25624
+ var moneyGoalSavedFromSchema = _enum2(MONEY_GOAL_SAVED_FROM);
25569
25625
  var moneyGoalViewSchema = moneyGoalSchema.extend({
25570
25626
  title: string2(),
25571
25627
  visibility: _enum2(NODE_VISIBILITY_VALUES),
25572
25628
  accountTitle: string2().nullable(),
25573
25629
  accountBalance: amount.nullable(),
25630
+ accountBalanceKnown: boolean2().nullable(),
25574
25631
  projectTitle: string2().nullable(),
25575
25632
  saved: amount,
25633
+ savedFrom: moneyGoalSavedFromSchema,
25634
+ remaining: amount,
25635
+ monthlyNeed: amount.nullable(),
25576
25636
  linesPlanned: amount,
25577
25637
  linesPaid: amount
25578
25638
  });
@@ -26511,6 +26571,16 @@ var familyTreeUnionSchema = object({
26511
26571
  partners: array(familyTreeRefSchema),
26512
26572
  children: array(familyTreeRefSchema)
26513
26573
  });
26574
+ var FAMILY_TREE_MAX_GENERATIONS = 50;
26575
+ var familyTreeAncestorSchema = object({
26576
+ n: number2().int().min(1),
26577
+ generation: number2().int().min(0),
26578
+ ref: familyTreeRefSchema,
26579
+ repeatOf: number2().int().nullable()
26580
+ });
26581
+ function ahnentafelGeneration(n) {
26582
+ return Math.floor(Math.log2(n));
26583
+ }
26514
26584
  var familyTreePersonViewSchema = object({
26515
26585
  ref: familyTreeRefSchema,
26516
26586
  name: string2(),
@@ -26568,11 +26638,24 @@ var familyTreeViewSchema = object({
26568
26638
  birthdaysOffered: array(familyTreeRefSchema),
26569
26639
  branch: familyTreeBranchViewSchema.nullable(),
26570
26640
  matches: array(familyTreeMatchSchema),
26641
+ edits: object({ count: number2().int(), skipped: number2().int() }),
26571
26642
  updatedAt: exports_coerce.date()
26572
26643
  });
26573
26644
  var familyTreeResponseSchema = object({ tree: familyTreeViewSchema.nullable() });
26574
26645
  var sourceNameSchema = string2().trim().min(1).max(80);
26575
26646
  var sourceEmailSchema = string2().trim().email().max(200);
26647
+ var familyTreeAncestorsQuerySchema = object({
26648
+ ref: familyTreeRefSchema.optional(),
26649
+ depth: exports_coerce.number().int().min(1).max(FAMILY_TREE_MAX_GENERATIONS).optional()
26650
+ });
26651
+ var familyTreeAncestorsSchema = object({
26652
+ rootRef: familyTreeRefSchema,
26653
+ depth: number2().int().nullable(),
26654
+ generations: number2().int(),
26655
+ deeper: boolean2(),
26656
+ ancestors: array(familyTreeAncestorSchema),
26657
+ people: array(familyTreePersonViewSchema)
26658
+ });
26576
26659
  var familyTreeQuerySchema = object({ side: familyTreeSideSchema.optional() });
26577
26660
  var familyTreeUploadFieldsSchema = object({
26578
26661
  sourceName: preprocess((v) => v === "" ? undefined : v, sourceNameSchema.optional()),
@@ -26609,7 +26692,118 @@ var familyTreeMatchDecisionResultSchema = object({
26609
26692
  matches: array(familyTreeMatchViewSchema)
26610
26693
  });
26611
26694
 
26695
+ // ../../packages/schema/src/contracts/family-tree-edits.ts
26696
+ var familyTreeDateSchema = string2().trim().regex(/^\d{4}(-\d{2}-\d{2})?$/, "expected YYYY or YYYY-MM-DD").refine((v) => {
26697
+ if (v.length === 4)
26698
+ return true;
26699
+ const d = new Date(`${v}T00:00:00Z`);
26700
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
26701
+ }, "not a real day");
26702
+ var nameSchema = string2().trim().min(1).max(120);
26703
+ var partSchema = string2().trim().min(1).max(80).nullish();
26704
+ var placeSchema = string2().trim().min(1).max(200).nullish();
26705
+ var sexSchema = _enum2(["M", "F"]).nullish();
26706
+ var personFieldsShape = {
26707
+ given: partSchema,
26708
+ surname: partSchema,
26709
+ sex: sexSchema,
26710
+ birthDate: familyTreeDateSchema.nullish(),
26711
+ birthPlace: placeSchema,
26712
+ deathDate: familyTreeDateSchema.nullish(),
26713
+ deathPlace: placeSchema
26714
+ };
26715
+ var addFamilyTreePersonInputSchema = object({ name: nameSchema, ...personFieldsShape }).strict();
26716
+ var editFamilyTreePersonInputSchema = object({ name: nameSchema.optional(), ...personFieldsShape }).strict().refine((v) => Object.values(v).some((x) => x !== undefined), "nothing to change");
26717
+ var factTypeSchema = string2().trim().min(1).max(40).transform((v) => v.toLowerCase());
26718
+ var factFieldsShape = {
26719
+ date: string2().trim().min(1).max(60).nullish(),
26720
+ place: placeSchema,
26721
+ value: string2().trim().min(1).max(2000).nullish()
26722
+ };
26723
+ var addFamilyTreeFactInputSchema = object({ type: factTypeSchema, ...factFieldsShape }).strict();
26724
+ var editFamilyTreeFactInputSchema = object({ type: factTypeSchema.optional(), ...factFieldsShape }).strict().refine((v) => Object.values(v).some((x) => x !== undefined), "nothing to change");
26725
+ var familyTreeFactIdSchema = string2().regex(/^[A-Za-z0-9_-]{1,40}$/, "expected a fact id");
26726
+ var familyTreeLinkInputSchema = discriminatedUnion("kind", [
26727
+ object({ kind: literal("parent"), parent: familyTreeRefSchema, child: familyTreeRefSchema }).strict(),
26728
+ object({ kind: literal("partner"), a: familyTreeRefSchema, b: familyTreeRefSchema }).strict()
26729
+ ]);
26730
+ var familyTreeFactSchema = object({
26731
+ id: familyTreeFactIdSchema,
26732
+ type: string2(),
26733
+ date: string2().nullable(),
26734
+ place: string2().nullable(),
26735
+ value: string2().nullable()
26736
+ });
26737
+ var relativeSchema = object({ ref: familyTreeRefSchema, name: string2() });
26738
+ var familyTreePersonDetailSchema = object({
26739
+ ref: familyTreeRefSchema,
26740
+ name: string2(),
26741
+ given: string2().nullable(),
26742
+ surname: string2().nullable(),
26743
+ sex: _enum2(["M", "F"]).nullable(),
26744
+ birthDate: string2().nullable(),
26745
+ birthYear: number2().int().nullable(),
26746
+ birthPlace: string2().nullable(),
26747
+ deathDate: string2().nullable(),
26748
+ deathYear: number2().int().nullable(),
26749
+ deathPlace: string2().nullable(),
26750
+ living: boolean2(),
26751
+ facts: array(familyTreeFactSchema),
26752
+ parents: array(relativeSchema),
26753
+ partners: array(relativeSchema),
26754
+ children: array(relativeSchema),
26755
+ added: boolean2(),
26756
+ contact: object({ id: ulidSchema, title: string2() }).nullable()
26757
+ });
26758
+ var familyTreeEditKindSchema = _enum2([
26759
+ "person_add",
26760
+ "person_edit",
26761
+ "person_remove",
26762
+ "fact_add",
26763
+ "fact_edit",
26764
+ "fact_remove",
26765
+ "link_parent",
26766
+ "link_partner",
26767
+ "unlink_parent",
26768
+ "unlink_partner"
26769
+ ]);
26770
+ var familyTreeEditViewSchema = object({
26771
+ id: ulidSchema,
26772
+ kind: familyTreeEditKindSchema,
26773
+ line: string2(),
26774
+ at: exports_coerce.date(),
26775
+ applied: boolean2(),
26776
+ reason: string2().nullable()
26777
+ });
26778
+ var familyTreeEditsSchema = object({ edits: array(familyTreeEditViewSchema) });
26779
+ var familyTreeChangeSchema = object({
26780
+ ref: familyTreeRefSchema,
26781
+ op: _enum2(["created", "updated", "deleted"]),
26782
+ name: string2(),
26783
+ fields: array(string2())
26784
+ });
26785
+ var familyTreeEditReceiptSchema = object({
26786
+ edit: familyTreeEditViewSchema,
26787
+ changes: array(familyTreeChangeSchema),
26788
+ person: familyTreePersonDetailSchema.nullable()
26789
+ });
26790
+ var familyTreeUndoResultSchema = object({
26791
+ undone: ulidSchema,
26792
+ changes: array(familyTreeChangeSchema),
26793
+ skipped: array(familyTreeEditViewSchema)
26794
+ });
26795
+
26612
26796
  // ../../packages/schema/src/operations/family-tree.ts
26797
+ var REF = {
26798
+ ref: { description: "The person's ref in the tree (`I12`, or `P\u2026` for one added in do)." }
26799
+ };
26800
+ var REF_SCHEMA = { ref: familyTreeRefSchema };
26801
+ var FACT = {
26802
+ ...REF,
26803
+ factId: { description: "The fact's id (`OCCU-1` from the GEDCOM, `f-\u2026` for one added in do)." }
26804
+ };
26805
+ var FACT_SCHEMA = { ref: familyTreeRefSchema, factId: familyTreeFactIdSchema };
26806
+ var RECEIPT = "Returns the receipt `{ edit, changes, person }`: `edit.id` is what `POST /family-tree/edits/{id}/undo` takes, `changes` lists each person the edit changed (created \xB7 updated \xB7 deleted, with the fields). The edit is kept as an overlay over the import, so a new GEDCOM keeps it.";
26613
26807
  var NOT_IN_STAR = "A sensitive scope (ADR 0028): never part of `*`, and only the tree of the member who minted the key.";
26614
26808
  var familyTreeOperations = defineOperations({
26615
26809
  import: {
@@ -26646,6 +26840,31 @@ var familyTreeOperations = defineOperations({
26646
26840
  response: { description: "`{ tree }`." },
26647
26841
  errors: { 400: true, 403: "The key doesn't hold `people:family-tree`, or is no member's." }
26648
26842
  },
26843
+ ancestors: {
26844
+ method: "get",
26845
+ path: "/family-tree/ancestors",
26846
+ scopes: ["people:family-tree"],
26847
+ tag: "People",
26848
+ summary: "The direct line back",
26849
+ description: "A pedigree: every ancestor of `ref` (the member's own place in the tree when left out) as far as `depth` generations (1\u201350; all of them when left out). Each slot has its Ahnentafel number `n` (the person is 1, a father 2n, a mother 2n+1) and `generation`; someone who is an ancestor along two lines appears at each number, `repeatOf` naming the first. `people` holds each person once; `deeper` says the tree goes further back than `depth`. A 400 before the member said who they are and with no `ref`.",
26850
+ query: [
26851
+ {
26852
+ name: "ref",
26853
+ description: "Whose ancestors (default: the member's own place in the tree)."
26854
+ },
26855
+ {
26856
+ name: "depth",
26857
+ type: "integer",
26858
+ minimum: 1,
26859
+ maximum: 50,
26860
+ description: "How many generations back (default: all)."
26861
+ }
26862
+ ],
26863
+ querySchema: familyTreeAncestorsQuerySchema,
26864
+ status: 200,
26865
+ response: { description: "`{ rootRef, depth, generations, deeper, ancestors, people }`." },
26866
+ errors: { 400: true, 404: true }
26867
+ },
26649
26868
  people: {
26650
26869
  method: "get",
26651
26870
  path: "/family-tree/people",
@@ -26697,6 +26916,150 @@ var familyTreeOperations = defineOperations({
26697
26916
  status: 200,
26698
26917
  response: { description: "`{ id, did, matches }`." },
26699
26918
  errors: { 400: true, 404: true }
26919
+ },
26920
+ person: {
26921
+ method: "get",
26922
+ path: "/family-tree/people/{ref}",
26923
+ scopes: ["people:family-tree"],
26924
+ tag: "People",
26925
+ summary: "One person in the tree",
26926
+ description: "Their name, sex, birth and death (date and place), facts (each with the `id` an edit takes), parents, partners and children, whether the member added them, and the Contact they're linked to.",
26927
+ pathParams: REF,
26928
+ pathParamSchemas: REF_SCHEMA,
26929
+ status: 200,
26930
+ response: { description: "The person." },
26931
+ errors: { 400: true, 404: true }
26932
+ },
26933
+ addPerson: {
26934
+ method: "post",
26935
+ path: "/family-tree/people",
26936
+ scopes: ["people:family-tree"],
26937
+ tag: "People",
26938
+ summary: "Add a person to the tree",
26939
+ description: `\`{ name, given?, surname?, sex? (M \xB7 F), birthDate?, birthPlace?, deathDate?, deathPlace? }\`; dates are \`YYYY\` or \`YYYY-MM-DD\`, and a death date or place means not living. They get a new ref (\`P\u2026\`); link them to parents or a partner next. ${RECEIPT}`,
26940
+ body: addFamilyTreePersonInputSchema,
26941
+ status: 201,
26942
+ response: { description: "The receipt." },
26943
+ errors: { 400: true, 404: true }
26944
+ },
26945
+ editPerson: {
26946
+ method: "patch",
26947
+ path: "/family-tree/people/{ref}",
26948
+ scopes: ["people:family-tree"],
26949
+ tag: "People",
26950
+ summary: "Change a person in the tree",
26951
+ description: `Any of the add's fields; \`null\` clears one. A change to the death decides again whether they're living. ${RECEIPT}`,
26952
+ pathParams: REF,
26953
+ pathParamSchemas: REF_SCHEMA,
26954
+ body: editFamilyTreePersonInputSchema,
26955
+ status: 200,
26956
+ response: { description: "The receipt." },
26957
+ errors: { 400: true, 404: true }
26958
+ },
26959
+ removePerson: {
26960
+ method: "delete",
26961
+ path: "/family-tree/people/{ref}",
26962
+ scopes: ["people:family-tree"],
26963
+ tag: "People",
26964
+ summary: "Take a person out of the tree",
26965
+ description: `Out of every family they're in; their link to a Contact comes back with an Undo. ${RECEIPT}`,
26966
+ pathParams: REF,
26967
+ pathParamSchemas: REF_SCHEMA,
26968
+ status: 200,
26969
+ response: { description: "The receipt (`person`: null)." },
26970
+ errors: { 400: true, 404: true }
26971
+ },
26972
+ addFact: {
26973
+ method: "post",
26974
+ path: "/family-tree/people/{ref}/facts",
26975
+ scopes: ["people:family-tree"],
26976
+ tag: "People",
26977
+ summary: "Add a fact to a person",
26978
+ description: `\`{ type, date?, place?, value? }\`: an occupation, a residence, a baptism, a note (the type is a word of the member's own). The date is as written (\`ABT 1720\`). ${RECEIPT}`,
26979
+ pathParams: REF,
26980
+ pathParamSchemas: REF_SCHEMA,
26981
+ body: addFamilyTreeFactInputSchema,
26982
+ status: 201,
26983
+ response: { description: "The receipt." },
26984
+ errors: { 400: true, 404: true }
26985
+ },
26986
+ editFact: {
26987
+ method: "patch",
26988
+ path: "/family-tree/people/{ref}/facts/{factId}",
26989
+ scopes: ["people:family-tree"],
26990
+ tag: "People",
26991
+ summary: "Change a fact",
26992
+ description: `Any of \`type\`, \`date\`, \`place\`, \`value\`; \`null\` clears one. ${RECEIPT}`,
26993
+ pathParams: FACT,
26994
+ pathParamSchemas: FACT_SCHEMA,
26995
+ body: editFamilyTreeFactInputSchema,
26996
+ status: 200,
26997
+ response: { description: "The receipt." },
26998
+ errors: { 400: true, 404: true }
26999
+ },
27000
+ removeFact: {
27001
+ method: "delete",
27002
+ path: "/family-tree/people/{ref}/facts/{factId}",
27003
+ scopes: ["people:family-tree"],
27004
+ tag: "People",
27005
+ summary: "Remove a fact",
27006
+ description: RECEIPT,
27007
+ pathParams: FACT,
27008
+ pathParamSchemas: FACT_SCHEMA,
27009
+ status: 200,
27010
+ response: { description: "The receipt." },
27011
+ errors: { 400: true, 404: true }
27012
+ },
27013
+ link: {
27014
+ method: "post",
27015
+ path: "/family-tree/links",
27016
+ scopes: ["people:family-tree"],
27017
+ tag: "People",
27018
+ summary: "Link parent and child, or partners",
27019
+ description: `\`{ kind: "parent", parent, child }\` (a child has at most two parents: a third is a 409) or \`{ kind: "partner", a, b }\`. ${RECEIPT}`,
27020
+ body: familyTreeLinkInputSchema,
27021
+ status: 200,
27022
+ response: { description: "The receipt." },
27023
+ errors: { 400: true, 404: true, 409: true },
27024
+ operationId: "family_tree_link"
27025
+ },
27026
+ unlink: {
27027
+ method: "post",
27028
+ path: "/family-tree/unlink",
27029
+ scopes: ["people:family-tree"],
27030
+ tag: "People",
27031
+ summary: "Unlink parent and child, or partners",
27032
+ description: `The link's body. A child with siblings keeps them with the other parent; partners with children together are a 409 (unlink a parent first). ${RECEIPT}`,
27033
+ body: familyTreeLinkInputSchema,
27034
+ status: 200,
27035
+ response: { description: "The receipt." },
27036
+ errors: { 400: true, 404: true, 409: true },
27037
+ operationId: "family_tree_unlink"
27038
+ },
27039
+ edits: {
27040
+ method: "get",
27041
+ path: "/family-tree/edits",
27042
+ scopes: ["people:family-tree"],
27043
+ tag: "People",
27044
+ summary: "The member's edits to the tree",
27045
+ description: "Oldest first, each `{ id, kind, line, at, applied, reason }`. The edits are an overlay replayed over every import: one the current file has no room for (its person or fact isn't in it, or it no longer fits) is `applied: false` with the `reason`, kept, and applies again when a later file brings its person back.",
27046
+ status: 200,
27047
+ response: { description: "`{ edits }`." },
27048
+ errors: { 404: true }
27049
+ },
27050
+ undo: {
27051
+ method: "post",
27052
+ path: "/family-tree/edits/{id}/undo",
27053
+ scopes: ["people:family-tree"],
27054
+ tag: "People",
27055
+ summary: "Undo an edit to the tree",
27056
+ description: "The receipt's Undo: the edit leaves the overlay and the tree is replayed without it. Returns `{ undone, changes, skipped }`; `skipped` lists the edits that leaned on it (a fact on a person it added) and no longer apply. A POST, so it doesn't wait on a confirmation the way a delete does.",
27057
+ pathParams: { id: "The edit's id (a receipt's `edit.id`)." },
27058
+ pathParamSchemas: { id: ulidSchema },
27059
+ status: 200,
27060
+ response: { description: "`{ undone, changes, skipped }`." },
27061
+ errors: { 400: true, 404: true },
27062
+ operationId: "family_tree_undo"
26700
27063
  }
26701
27064
  });
26702
27065
 
@@ -27893,7 +28256,7 @@ var foodPatternsSchema = object({
27893
28256
  // ../../packages/schema/src/operations/food-log.ts
27894
28257
  var NO_MEMBER = "The key holds no health scope by name, or has no member behind it.";
27895
28258
  var HIDDEN2 = "No such meal, or it isn't the key's member's.";
27896
- var RECEIPT = "The meal with its `receipt`: a headline with the kcal range, a confidence word (close \xB7 estimate \xB7 rough), a line per item with its source, and the Undo / Edit / Save as my meal calls.";
28259
+ var RECEIPT2 = "The meal with its `receipt`: a headline with the kcal range, a confidence word (close \xB7 estimate \xB7 rough), a line per item with its source, and the Undo / Edit / Save as my meal calls.";
27897
28260
  var ITEM = "Each item: `label` and one of `foodId` (from `GET /api/v1/foods/search`), `productId` or `recipeId`, with `grams`, `quantity` + `unit` (`2 boterham`, `1 el`) or `servings`; optional `gramsLow`/`gramsHigh` and `matchConfidence`. With no reference, an `estimate` `{kcal, proteinG, carbsG, fatG}` (a labelled guess). The numbers are computed: table \xD7 grams.";
27898
28261
  var meal = { id: "The meal's ULID." };
27899
28262
  var mealId = { id: ulidSchema };
@@ -27923,7 +28286,7 @@ var foodLogOperations = defineOperations({
27923
28286
  description: `Log what the member ate in one call: \`id\`, \`title\`, \`eatenAt\`, \`mealType\` (breakfast \xB7 lunch \xB7 dinner \xB7 snack \xB7 drink), optional \`portionNote\`, \`place\`, \`flags\`, \`capture\`, \`photoFileId\`, and \`items\`. ${ITEM} Always private to the member.`,
27924
28287
  body: createMealLogInputSchema,
27925
28288
  status: 201,
27926
- response: { description: RECEIPT },
28289
+ response: { description: RECEIPT2 },
27927
28290
  errors: { 400: true, 403: NO_MEMBER },
27928
28291
  operationId: "food_log"
27929
28292
  },
@@ -27951,7 +28314,7 @@ var foodLogOperations = defineOperations({
27951
28314
  description: `Correct a meal: \`replace\` (\`[{itemId, item}]\`), \`remove\` (item ids) and \`add\` (items) add rows, never edit one; the meal's own fields update. ${ITEM}`,
27952
28315
  body: correctMealLogInputSchema,
27953
28316
  status: 200,
27954
- response: { description: RECEIPT },
28317
+ response: { description: RECEIPT2 },
27955
28318
  errors: { 400: true, 404: HIDDEN2 },
27956
28319
  operationId: "food_correct"
27957
28320
  },
@@ -27978,7 +28341,7 @@ var foodLogOperations = defineOperations({
27978
28341
  summary: "Undo a meal's last correction",
27979
28342
  description: "Take back the latest correction: the items it replaced or removed are current again.",
27980
28343
  status: 200,
27981
- response: { description: RECEIPT },
28344
+ response: { description: RECEIPT2 },
27982
28345
  errors: { 400: true, 404: HIDDEN2 }
27983
28346
  },
27984
28347
  saveMeal: {
@@ -31268,6 +31631,140 @@ var journalExportSchema = object({
31268
31631
  nextSince: exports_coerce.date()
31269
31632
  });
31270
31633
 
31634
+ // ../../packages/schema/src/contracts/journal-private.ts
31635
+ var JOURNAL_PRIVATE_TERMS = {
31636
+ alcohol: [
31637
+ "alcohol*",
31638
+ "drunk*",
31639
+ "drink*",
31640
+ "beer*",
31641
+ "wine*",
31642
+ "whisky",
31643
+ "whiskey",
31644
+ "vodka*",
31645
+ "gin",
31646
+ "rum",
31647
+ "tequila*",
31648
+ "cocktail*",
31649
+ "hangover*",
31650
+ "hungover",
31651
+ "tipsy",
31652
+ "booze*",
31653
+ "sober*",
31654
+ "sobriety",
31655
+ "bier*",
31656
+ "wijn*",
31657
+ "drank*",
31658
+ "dronken*",
31659
+ "borrel*",
31660
+ "pint",
31661
+ "pints",
31662
+ "pintje*",
31663
+ "kater*",
31664
+ "zuipen",
31665
+ "gezopen"
31666
+ ],
31667
+ smoking: [
31668
+ "smok*",
31669
+ "cigar*",
31670
+ "tobacco",
31671
+ "nicotine",
31672
+ "vape*",
31673
+ "vaping",
31674
+ "roken",
31675
+ "rook",
31676
+ "rookte*",
31677
+ "gerookt",
31678
+ "roker*",
31679
+ "sigaar*",
31680
+ "sigaret*",
31681
+ "tabak*",
31682
+ "peuk*",
31683
+ "vapen"
31684
+ ],
31685
+ cannabis: [
31686
+ "cannabis",
31687
+ "weed",
31688
+ "hash",
31689
+ "hashish",
31690
+ "marijuana",
31691
+ "joint",
31692
+ "joints",
31693
+ "jointje*",
31694
+ "stoned",
31695
+ "thc",
31696
+ "cbd",
31697
+ "edibles",
31698
+ "wiet*",
31699
+ "hasj*",
31700
+ "blowen",
31701
+ "blowt",
31702
+ "geblowd"
31703
+ ],
31704
+ drugs: [
31705
+ "drug*",
31706
+ "cocaine",
31707
+ "coca\xEFne",
31708
+ "coke",
31709
+ "mdma",
31710
+ "ecstasy",
31711
+ "xtc",
31712
+ "ketamine",
31713
+ "lsd",
31714
+ "shrooms",
31715
+ "paddo*",
31716
+ "amphetamine*",
31717
+ "heroin*",
31718
+ "hero\xEFne",
31719
+ "opioid*"
31720
+ ],
31721
+ relapse: [
31722
+ "relaps*",
31723
+ "craving*",
31724
+ "rehab",
31725
+ "aa meeting*",
31726
+ "terugval*",
31727
+ "hervallen",
31728
+ "afkick*",
31729
+ "ontwenning*"
31730
+ ],
31731
+ fertility: [
31732
+ "fertil*",
31733
+ "ivf",
31734
+ "iui",
31735
+ "ovulat*",
31736
+ "conceiv*",
31737
+ "conception",
31738
+ "pregnan*",
31739
+ "sperm*",
31740
+ "embryo*",
31741
+ "miscarr*",
31742
+ "ttc",
31743
+ "vruchtbaar*",
31744
+ "ovulatie",
31745
+ "eisprong",
31746
+ "zwanger*",
31747
+ "sperma*",
31748
+ "miskraam",
31749
+ "kinderwens"
31750
+ ]
31751
+ };
31752
+ var LETTER = "[\\p{L}\\p{N}_]";
31753
+ var PRIVATE = new RegExp(`(?<!${LETTER})(?:${Object.values(JOURNAL_PRIVATE_TERMS).flat().map((term) => {
31754
+ const stem = term.replace(/\*$/, "").replace(/\s+/g, "\\s+");
31755
+ return term.endsWith("*") ? `${stem}${LETTER}*` : stem;
31756
+ }).join("|")})(?!${LETTER})`, "iu");
31757
+ function isPrivateLine(line) {
31758
+ return PRIVATE.test(line);
31759
+ }
31760
+ function withoutPrivateLines(text) {
31761
+ const kept = text.split(/\r?\n/).filter((line) => !isPrivateLine(line)).join(`
31762
+ `).replace(/\n{3,}/g, `
31763
+
31764
+ `).trim();
31765
+ return kept || null;
31766
+ }
31767
+
31271
31768
  // ../../packages/schema/src/contracts/journal-import.ts
31272
31769
  var journalImportBatchSchema = string2().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "a batch is a month (YYYY-MM)");
31273
31770
  var journalImportEntrySchema = _enum2(["summary", "retro", "log"]);
@@ -31278,6 +31775,7 @@ var journalImportFoundSchema = object({
31278
31775
  documents: number2().int().nonnegative().optional(),
31279
31776
  habits: number2().int().nonnegative().optional()
31280
31777
  }).strict();
31778
+ var keptLine = (max) => string2().trim().min(1).max(max).transform((v) => isPrivateLine(v) ? null : v).nullish();
31281
31779
  var JOURNAL_IMPORT_MAX_ITEMS = 400;
31282
31780
  var journalImportItemInputSchema = object({
31283
31781
  day: journalDateSchema,
@@ -31285,12 +31783,12 @@ var journalImportItemInputSchema = object({
31285
31783
  sourceHash: string2().trim().min(1).max(128),
31286
31784
  entry: journalImportEntrySchema.default("log"),
31287
31785
  hasCheck: boolean2().default(false),
31288
- title: string2().trim().min(1).max(200).nullish(),
31289
- body: string2().trim().min(1).max(20000).nullish(),
31786
+ title: keptLine(200),
31787
+ body: string2().trim().min(1).max(20000).transform(withoutPrivateLines).nullish(),
31290
31788
  rating: number2().min(1).max(10).nullish(),
31291
- ratingQuestion: string2().trim().min(1).max(80).nullish(),
31789
+ ratingQuestion: keptLine(80),
31292
31790
  sleep: number2().min(0).max(24).nullish(),
31293
- highlight: string2().trim().min(1).max(200).nullish(),
31791
+ highlight: keptLine(200),
31294
31792
  milestoneCandidate: boolean2().default(false),
31295
31793
  health: boolean2().default(false),
31296
31794
  found: journalImportFoundSchema.default({})
@@ -31356,8 +31854,25 @@ var journalImportAnswerInputSchema = object({
31356
31854
  rating: number2().min(1).max(10).nullable().optional(),
31357
31855
  skip: boolean2().optional()
31358
31856
  }).strict().refine((v) => Object.keys(v).length > 0, { message: "nothing to answer" });
31857
+ var journalImportRatingAnswerSchema = object({ day: journalDateSchema, rating: number2().min(1).max(10) }).strict();
31858
+ var journalImportApproveInputSchema = object({
31859
+ batch: journalImportBatchSchema.optional(),
31860
+ all: literal(true).optional(),
31861
+ ratings: array(journalImportRatingAnswerSchema).max(JOURNAL_IMPORT_MAX_ITEMS).default([])
31862
+ }).strict().refine((v) => v.batch !== undefined !== (v.all === true), {
31863
+ message: "approve one batch or all: send batch or all, not both"
31864
+ }).refine((v) => new Set(v.ratings.map((r) => r.day)).size === v.ratings.length, {
31865
+ message: "one rating per day",
31866
+ path: ["ratings"]
31867
+ });
31868
+ var journalImportOpenDaySchema = object({
31869
+ day: journalDateSchema,
31870
+ ratingQuestion: string2()
31871
+ });
31359
31872
  var journalImportApproveSchema = object({
31360
31873
  imported: number2().int(),
31874
+ batches: array(journalImportBatchSchema).default([]),
31875
+ open: array(journalImportOpenDaySchema).default([]),
31361
31876
  overview: journalImportOverviewSchema
31362
31877
  });
31363
31878
 
@@ -31745,12 +32260,25 @@ var journalOperations = defineOperations({
31745
32260
  scopes: ["journal:write"],
31746
32261
  tag: "Journal",
31747
32262
  summary: "Stage daily notes for the backfill",
31748
- 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.",
32263
+ 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.",
31749
32264
  body: journalImportUploadInputSchema,
31750
32265
  status: 200,
31751
32266
  response: { description: "How the upload went.", view: "JournalImportUpload" },
31752
32267
  errors: { 400: true }
31753
32268
  },
32269
+ approveImport: {
32270
+ method: "post",
32271
+ path: "/journal/imports/approve",
32272
+ scopes: ["journal:write"],
32273
+ tag: "Journal",
32274
+ summary: "Approve staged days into the Journal",
32275
+ 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.',
32276
+ body: journalImportApproveInputSchema,
32277
+ status: 200,
32278
+ response: { description: "How the approve went.", view: "JournalImportApprove" },
32279
+ errors: { 400: true },
32280
+ operationId: "journal_import_approve"
32281
+ },
31754
32282
  importOverview: {
31755
32283
  method: "get",
31756
32284
  path: "/journal/imports",
@@ -32826,7 +33354,10 @@ var moneyNetWorthAccountSchema = object({
32826
33354
  currency: string2(),
32827
33355
  value: amount5,
32828
33356
  source: moneySnapshotSourceSchema,
32829
- month: moneyMonthSchema
33357
+ month: moneyMonthSchema,
33358
+ pensionSubtype: moneyPensionSubtypeSchema.nullable(),
33359
+ lockedUntil: string2().nullable(),
33360
+ locked: boolean2()
32830
33361
  });
32831
33362
  var moneyNetWorthUnknownSchema = moneyNetWorthAccountSchema.pick({
32832
33363
  accountId: true,
@@ -32841,6 +33372,8 @@ var moneyNetWorthSchema = object({
32841
33372
  total: amount5,
32842
33373
  have: amount5,
32843
33374
  owe: amount5,
33375
+ liquid: amount5,
33376
+ locked: amount5,
32844
33377
  points: array(moneyNetWorthPointSchema),
32845
33378
  accounts: array(moneyNetWorthAccountSchema),
32846
33379
  complete: boolean2(),
@@ -33051,7 +33584,7 @@ var moneyOperations = defineOperations({
33051
33584
  scopes: ["finance:write"],
33052
33585
  tag: "Money",
33053
33586
  summary: "Open a money account",
33054
- description: 'Open an account: `id`, `title`, optional `kind` (current \xB7 savings \xB7 credit_card \xB7 cash \xB7 voucher \xB7 loan \xB7 investment \xB7 pension \xB7 asset_manual), `holder`, `iban` (stored only masked and hashed), `currency`, `openingBalance` (a decimal string), `includeInBudget`, `includeInNetWorth` and `visibility`. 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.',
33587
+ description: 'Open an account: `id`, `title`, optional `kind` (current \xB7 savings \xB7 credit_card \xB7 cash \xB7 voucher \xB7 loan \xB7 investment \xB7 pension \xB7 asset_manual), the `pensionSubtype` of a pension (group_insurance \xB7 pension_savings_fund \xB7 pension_savings_insurance \xB7 vapz; on another kind a 400), `lockedUntil` (YYYY-MM-DD, any kind), `holder`, `iban` (stored only masked and hashed), `currency`, `openingBalance` (a decimal string), `includeInBudget` (always off for a pension, which never counts toward the budget), `includeInNetWorth` and `visibility`. 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.',
33055
33588
  body: createMoneyAccountInputSchema,
33056
33589
  status: 201,
33057
33590
  response: { description: "The account." },
@@ -33126,7 +33659,7 @@ var moneyOperations = defineOperations({
33126
33659
  scopes: ["finance:write"],
33127
33660
  tag: "Money",
33128
33661
  summary: "Update a money account",
33129
- description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it. `balance` with `balanceAt` (a day, read as its end, or a moment) states what it held then: the anchor net worth counts from, back and forward through the ledger, for a bank whose export has no running balance. The two go together; a day ahead, or an account a bank link keeps, is a 400.",
33662
+ description: "Rename it, change its kind (an investment account moved to `pension` keeps its balance and ledger; moved off `pension`, its `pensionSubtype` is cleared), its `pensionSubtype` or `lockedUntil` (null clears), 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, and so is a body that sets nothing.",
33130
33663
  pathParams: { id: "The account's ULID." },
33131
33664
  pathParamSchemas: { id: ulidSchema },
33132
33665
  body: updateMoneyAccountInputSchema,
@@ -33646,7 +34179,7 @@ var moneyOperations = defineOperations({
33646
34179
  scopes: ["finance:read"],
33647
34180
  tag: "Money",
33648
34181
  summary: "Get net worth and its monthly line",
33649
- description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `points` (one per month, oldest first, the current month last; `total` null before any account had a value) and `accounts` (biggest first, each with its `value`, `source` \u2014 balance, ledger or manual \u2014 and the `month` that value is from). A loan or a credit card always counts as owed. This month follows each account's balance, carried forward by what was booked after `balanceAt`; the months before an account was first read are worked back from its ledger; a value set by hand carries on until the next one. An account whose ledger came from a bank but has no balance yet (an export with no running balance) is never counted as 0: it is listed under `unknown` and left out of the total, and `complete` is false. `scope`: `all` (default), `space` or `owner`; `months` 2\u201360 (12 by default).",
34182
+ description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `liquid` and `locked` (they add up to `total`; locked is a pension, or an account whose `lockedUntil` day is ahead, never a debt), `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).",
33650
34183
  query: [
33651
34184
  { name: "scope", description: "all, space or owner." },
33652
34185
  { name: "months", description: "How many months the line covers (2\u201360, default 12)." }
@@ -33674,7 +34207,7 @@ var moneyOperations = defineOperations({
33674
34207
  scopes: ["finance:read"],
33675
34208
  tag: "Money",
33676
34209
  summary: "List money goals",
33677
- 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`.",
34210
+ 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`.",
33678
34211
  status: 200,
33679
34212
  response: { description: "The goals." }
33680
34213
  },
@@ -34354,12 +34887,12 @@ var periodOperations = defineOperations({
34354
34887
  var placePointSchema = tuple([number2().finite(), number2().finite()]);
34355
34888
  var placeOutlineSchema = array(placePointSchema).min(3).max(200);
34356
34889
  var placeKindSchema = _enum2(placeKindValues);
34357
- var placeSchema = createSelectSchema(places, {
34890
+ var placeSchema2 = createSelectSchema(places, {
34358
34891
  outline: placeOutlineSchema.nullable(),
34359
34892
  kind: string2().default("room"),
34360
34893
  gardenFeatureId: string2().nullable().default(null)
34361
34894
  });
34362
- var placeViewSchema = placeSchema.extend({
34895
+ var placeViewSchema = placeSchema2.extend({
34363
34896
  title: string2(),
34364
34897
  parentId: string2().nullable().default(null),
34365
34898
  createdAt: exports_coerce.date(),
@@ -34814,7 +35347,8 @@ var planUnplaceInputSchema = object({ taskId: ulidSchema }).strict();
34814
35347
  var planListQuerySchema = object({
34815
35348
  kind: planKindSchema.optional(),
34816
35349
  status: planStatusSchema.optional(),
34817
- startsOn: civilDaySchema.optional()
35350
+ startsOn: civilDaySchema.optional(),
35351
+ taskId: ulidSchema.optional()
34818
35352
  });
34819
35353
  var planTaskSchema = object({
34820
35354
  id: ulidSchema,
@@ -35034,6 +35568,7 @@ var automationListItemSchema = automationViewSchema.extend({
35034
35568
  madeBy: object({ userId: string2(), name: string2(), you: boolean2() }).nullable(),
35035
35569
  lastRun: automationRunSummarySchema.nullable()
35036
35570
  });
35571
+ var automationRunNowSchema = object({ queued: literal(true) });
35037
35572
 
35038
35573
  // ../../packages/schema/src/db/shopping.ts
35039
35574
  var shoppingLists = pgTable("shopping_lists", {
@@ -35795,7 +36330,8 @@ var projectOperations = defineOperations({
35795
36330
  var occurrenceViewSchema = occurrenceSchema.extend({
35796
36331
  title: string2(),
35797
36332
  templateKind: _enum2(["task", "habit", "routine"]),
35798
- measurementType: habitMeasurementTypeSchema.nullable()
36333
+ measurementType: habitMeasurementTypeSchema.nullable(),
36334
+ allDay: boolean2().optional()
35799
36335
  });
35800
36336
  var todayResponseSchema = object({
35801
36337
  from: exports_coerce.date(),
@@ -38787,6 +39323,18 @@ var WEEKDAY_ALT = "monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|
38787
39323
  var EVERY_N_RE = /\bevery\s+(\d+)\s+(day|days|week|weeks|month|months|year|years)\b/i;
38788
39324
  var EVERY_WEEKDAYS_RE = /\bevery\s+(?:weekday|weekdays|work\s?day|work\s?days)\b/i;
38789
39325
  var EVERY_WEEKDAY_RE = new RegExp(`\\bevery\\s+(${WEEKDAY_ALT})\\b`, "i");
39326
+ 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");
39327
+ var NTH = {
39328
+ first: "1",
39329
+ "1st": "1",
39330
+ second: "2",
39331
+ "2nd": "2",
39332
+ third: "3",
39333
+ "3rd": "3",
39334
+ fourth: "4",
39335
+ "4th": "4",
39336
+ last: "-1"
39337
+ };
38790
39338
  var EVERY_UNIT_RE = /\bevery\s+(day|week|month|year)\b/i;
38791
39339
  var ADVERB_RE = /\b(daily|weekly|monthly|yearly|annually)\b/i;
38792
39340
  var RELATIVE_RE = /\b(today|tonight|tomorrow|tmrw|tmr|yesterday)\b/i;
@@ -38918,6 +39466,12 @@ function takeEstimate(scan, take) {
38918
39466
  return;
38919
39467
  }
38920
39468
  function takeRecurrence(scan, take) {
39469
+ const nth = scan.match(EVERY_NTH_WEEKDAY_RE);
39470
+ if (nth && take(nth, "recurrence")) {
39471
+ const ordinal = NTH[(nth[1] ?? "").toLowerCase()] ?? "1";
39472
+ const byDay = `${ordinal}${weekdayByName(nth[2]).code}`;
39473
+ return { mode: "rigid", freq: "monthly", interval: 1, byDay: [byDay] };
39474
+ }
38921
39475
  const everyN = scan.match(EVERY_N_RE);
38922
39476
  if (everyN && take(everyN, "recurrence")) {
38923
39477
  const [, nStr = "1", unitRaw = "day"] = everyN;
@@ -39053,8 +39607,28 @@ function firstOccurrence(rule, now) {
39053
39607
  });
39054
39608
  return addDays(today, Math.min(...deltas));
39055
39609
  }
39610
+ const nth = rule.freq === "monthly" ? rule.byDay?.map(parseByDay) : undefined;
39611
+ if (nth?.length) {
39612
+ const days = [0, 1].flatMap((ahead) => nth.flatMap((p) => {
39613
+ if (!p)
39614
+ return [];
39615
+ const dow = WEEKDAYS.find((w) => w.code === p.weekday)?.dow ?? 0;
39616
+ const day = nthWeekdayOfMonth(today.getFullYear(), today.getMonth() + ahead, dow, p.ordinal ?? 1);
39617
+ return day >= today ? [day] : [];
39618
+ }));
39619
+ if (days.length > 0)
39620
+ return new Date(Math.min(...days.map((d) => d.getTime())));
39621
+ }
39056
39622
  return today;
39057
39623
  }
39624
+ function nthWeekdayOfMonth(year, month, dow, ordinal) {
39625
+ if (ordinal < 0) {
39626
+ const last = new Date(year, month + 1, 0);
39627
+ return addDays(last, -((last.getDay() - dow + 7) % 7));
39628
+ }
39629
+ const first = new Date(year, month, 1);
39630
+ return addDays(first, (dow - first.getDay() + 7) % 7 + (ordinal - 1) * 7);
39631
+ }
39058
39632
  function addMonths(d, months) {
39059
39633
  const next = new Date(d);
39060
39634
  next.setMonth(next.getMonth() + months);
@@ -39125,6 +39699,7 @@ var bookFacetSchema = object({
39125
39699
  coverSources: coverSourcesSchema.nullish(),
39126
39700
  pageCount: number2().int().min(1).max(1e5).nullish(),
39127
39701
  currentPage: number2().int().min(0).max(1e5).nullish(),
39702
+ chapter: string2().trim().min(1).max(60).nullish(),
39128
39703
  isbn: string2().regex(/^[0-9Xx][0-9Xx\s-]{8,20}$/, "must be an ISBN").nullish()
39129
39704
  }).strict();
39130
39705
  var filmFacetSchema = object({
@@ -40342,7 +40917,17 @@ function renderJournalImportScan(data, mode) {
40342
40917
  }
40343
40918
  out.push(` milestone candidates ${count((i) => Boolean(i.milestoneCandidate))}`);
40344
40919
  out.push(` health lines (left for Health) ${count((i) => Boolean(i.health))}`);
40345
- 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.`);
40920
+ 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.`);
40921
+ return out.join(`
40922
+ `);
40923
+ }
40924
+ function renderJournalImportApprove(data, mode) {
40925
+ const d = data;
40926
+ const batches = d.batches?.length ? paint(mode, "dim", ` (${d.batches.join(", ")})`) : "";
40927
+ const out = [`Imported ${d.imported ?? 0} days into the Journal${batches}`];
40928
+ if (d.open?.length) {
40929
+ 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`));
40930
+ }
40346
40931
  return out.join(`
40347
40932
  `);
40348
40933
  }
@@ -41471,11 +42056,16 @@ var commands2 = [
41471
42056
  ],
41472
42057
  flags: [
41473
42058
  { name: "unit", type: "string", description: "For a weight: g (default) or kg." },
42059
+ {
42060
+ name: "note",
42061
+ type: "string",
42062
+ description: 'For a weight: a short note drawn under its date ("vet check done").'
42063
+ },
41474
42064
  { name: "at", type: "string", description: "When (ISO 8601); default now." }
41475
42065
  ],
41476
42066
  scopes: [...animalOperations.log.scopes],
41477
42067
  returns: "AnimalTimelineEntry",
41478
- 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.`
42068
+ 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.`
41479
42069
  },
41480
42070
  run: async ({ args, flags, requireClient }) => {
41481
42071
  const id = requireArg(args, 0, "animal id");
@@ -41485,8 +42075,11 @@ var commands2 = [
41485
42075
  throw usageError("animals log needs a value (a number, or the note's text)");
41486
42076
  const at = flagString(flags, "at");
41487
42077
  const unit = flagString(flags, "unit");
42078
+ const note = flagString(flags, "note");
42079
+ if (note && kind !== "weight")
42080
+ throw usageError("--note goes with a weight");
41488
42081
  const n = Number(raw);
41489
- const fact = kind === "note" ? { kind, text: raw } : kind === "eggs" ? { kind, count: n } : kind === "weight" ? { kind, value: n, ...unit ? { unit } : {} } : { kind, value: n };
42082
+ const fact = kind === "note" ? { kind, text: raw } : kind === "eggs" ? { kind, count: n } : kind === "weight" ? { kind, value: n, ...unit ? { unit } : {}, ...note ? { note } : {} } : { kind, value: n };
41490
42083
  if (kind !== "note" && Number.isNaN(n))
41491
42084
  throw usageError("the value must be a number");
41492
42085
  const body = parseInput(animalLogInputSchema, { ...fact, ...at ? { occurredAt: at } : {} });
@@ -42414,6 +43007,20 @@ var commands7 = [
42414
43007
  ];
42415
43008
 
42416
43009
  // src/commands/collection.ts
43010
+ var CHAPTER_FLAG = {
43011
+ name: "chapter",
43012
+ type: "string",
43013
+ description: 'Where in the work: "9" (a chapter), "chapter 9", "book 9 and on".'
43014
+ };
43015
+ var LINKED_NOTE_FLAG = {
43016
+ name: "linked-note",
43017
+ type: "string",
43018
+ description: "Note ULID this Entry is tied to."
43019
+ };
43020
+ var whereFlags = (flags) => ({
43021
+ ...flagString(flags, "chapter") ? { chapter: flagString(flags, "chapter") } : {},
43022
+ ...flagString(flags, "linked-note") ? { linkedNoteId: flagString(flags, "linked-note") } : {}
43023
+ });
42417
43024
  var commands8 = [
42418
43025
  {
42419
43026
  spec: {
@@ -42537,6 +43144,53 @@ var commands8 = [
42537
43144
  };
42538
43145
  }
42539
43146
  },
43147
+ {
43148
+ spec: {
43149
+ path: ["collection", "reading"],
43150
+ summary: "Say where you are in a book: its chapter and/or page.",
43151
+ args: [{ name: "id", required: true, description: "Item ULID (a book)." }],
43152
+ flags: [
43153
+ {
43154
+ name: "chapter",
43155
+ type: "string",
43156
+ description: 'Where you are: "9" (a chapter), "chapter 9", "book 9".'
43157
+ },
43158
+ { name: "page", type: "string", description: "The page you're on." },
43159
+ { name: "clear-chapter", type: "boolean", description: "Forget the chapter." }
43160
+ ],
43161
+ scopes: [...collectionOperations.update.scopes],
43162
+ returns: "CollectionItemView",
43163
+ 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.`
43164
+ },
43165
+ run: async ({ args, flags, requireClient }) => {
43166
+ const id = requireArg(args, 0, "item id");
43167
+ const chapter = flagString(flags, "chapter");
43168
+ const clear = flagBool(flags, "clear-chapter");
43169
+ const page = flagString(flags, "page");
43170
+ if (page !== undefined && !/^\d+$/.test(page)) {
43171
+ throw usageError("--page must be a whole number, e.g. 142");
43172
+ }
43173
+ if (chapter === undefined && page === undefined && !clear) {
43174
+ throw usageError('collection reading needs --chapter or --page, e.g. --chapter "9"');
43175
+ }
43176
+ const client = requireClient();
43177
+ const item = await client.get(pathOf(collectionOperations.get, { id }));
43178
+ const { chapter: kept, ...rest } = item.facet ?? {};
43179
+ const facet = {
43180
+ ...rest,
43181
+ ...clear || kept === undefined ? {} : { chapter: kept }
43182
+ };
43183
+ if (chapter !== undefined)
43184
+ facet.chapter = chapter;
43185
+ if (page !== undefined)
43186
+ facet.currentPage = Number(page);
43187
+ const body = parseInput(updateCollectionItemInputSchema, { facet });
43188
+ return {
43189
+ data: await client.patch(pathOf(collectionOperations.update, { id }), body),
43190
+ render: renderRecord
43191
+ };
43192
+ }
43193
+ },
42540
43194
  {
42541
43195
  spec: {
42542
43196
  path: ["collection", "add-entry"],
@@ -42556,7 +43210,9 @@ var commands8 = [
42556
43210
  description: "What happened: use (default) \xB7 service \xB7 repair \xB7 fault \xB7 lent \xB7 returned \xB7 valued \xB7 moved."
42557
43211
  },
42558
43212
  { name: "cost", type: "string", description: "What it cost (a sweep, a repair)." },
42559
- { name: "contact", type: "string", description: "Contact ULID of who did it." }
43213
+ { name: "contact", type: "string", description: "Contact ULID of who did it." },
43214
+ CHAPTER_FLAG,
43215
+ LINKED_NOTE_FLAG
42560
43216
  ],
42561
43217
  scopes: [...collectionOperations.addEntry.scopes],
42562
43218
  returns: "CollectionItemDetail"
@@ -42578,7 +43234,8 @@ var commands8 = [
42578
43234
  ...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
42579
43235
  ...flagString(flags, "kind") ? { kind: flagString(flags, "kind") } : {},
42580
43236
  ...cost !== undefined ? { cost: Number(cost) } : {},
42581
- ...flagString(flags, "contact") ? { contactId: flagString(flags, "contact") } : {}
43237
+ ...flagString(flags, "contact") ? { contactId: flagString(flags, "contact") } : {},
43238
+ ...whereFlags(flags)
42582
43239
  });
42583
43240
  return {
42584
43241
  data: await requireClient().post(pathOf(collectionOperations.addEntry, { id }), body),
@@ -42667,7 +43324,8 @@ var commands8 = [
42667
43324
  name: "on",
42668
43325
  type: "string",
42669
43326
  description: "Day it occurred (YYYY-MM-DD); defaults to today."
42670
- }
43327
+ },
43328
+ CHAPTER_FLAG
42671
43329
  ],
42672
43330
  scopes: [...collectionOperations.log.scopes],
42673
43331
  returns: "CollectionLogResult",
@@ -42688,7 +43346,8 @@ var commands8 = [
42688
43346
  ...flagString(flags, "status") ? { status: flagString(flags, "status") } : {},
42689
43347
  ...rating !== undefined ? { rating: Number(rating) } : {},
42690
43348
  ...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
42691
- ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {}
43349
+ ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {},
43350
+ ...flagString(flags, "chapter") ? { chapter: flagString(flags, "chapter") } : {}
42692
43351
  });
42693
43352
  return {
42694
43353
  data: await requireClient().post(pathOf(collectionOperations.log), body),
@@ -42699,7 +43358,7 @@ var commands8 = [
42699
43358
  {
42700
43359
  spec: {
42701
43360
  path: ["collection", "edit-entry"],
42702
- summary: "Fix an Entry's day, rating or note.",
43361
+ summary: "Fix an Entry's day, rating, note, chapter or linked note.",
42703
43362
  args: [
42704
43363
  { name: "id", required: true, description: "Item ULID." },
42705
43364
  { name: "entryId", required: true, description: "Entry ULID." }
@@ -42707,7 +43366,9 @@ var commands8 = [
42707
43366
  flags: [
42708
43367
  { name: "rating", type: "string", description: "New rating 0.5\u20135." },
42709
43368
  { name: "note", type: "string", description: "New note." },
42710
- { name: "on", type: "string", description: "New day (YYYY-MM-DD)." }
43369
+ { name: "on", type: "string", description: "New day (YYYY-MM-DD)." },
43370
+ CHAPTER_FLAG,
43371
+ LINKED_NOTE_FLAG
42711
43372
  ],
42712
43373
  scopes: [...collectionOperations.editEntry.scopes],
42713
43374
  returns: "CollectionItemDetail"
@@ -42722,7 +43383,8 @@ var commands8 = [
42722
43383
  const body = parseInput(updateCollectionEntryInputSchema, {
42723
43384
  ...rating !== undefined ? { rating: Number(rating) } : {},
42724
43385
  ...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
42725
- ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {}
43386
+ ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {},
43387
+ ...whereFlags(flags)
42726
43388
  });
42727
43389
  return {
42728
43390
  data: await requireClient().patch(pathOf(collectionOperations.editEntry, { id, entryId }), body),
@@ -46791,7 +47453,8 @@ import { createHash } from "crypto";
46791
47453
  import { readdirSync, readFileSync as readFileSync7 } from "fs";
46792
47454
  import { join as join2 } from "path";
46793
47455
  var DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.md$/;
46794
- var RATING = /(?:[A-Za-z]+\s*\()?(~)?\s*(\d{1,2}(?:\.\d)?)(?:\s*[-\u2013]\s*(\d{1,2}))?\s*\/\s*10\)?/;
47456
+ var SCORE = "(?:10|[1-9])";
47457
+ 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");
46795
47458
  var HEALTH = /\b(symptoms?|stomach|nausea|headaches?|migraine|doctor|GP|medication|meds|pills?|blood|fever|pain|clinic|hospital)\b/i;
46796
47459
  var NOISE = /\b(PR monitor|GitHub daily|deploy(?:ment)? watch\w*|cron|heartbeat|sales prospects?)\b/i;
46797
47460
  var MILESTONE = /\b(first|went live|is live|win:|moved in|arrived|wedding|engaged)\b/i;
@@ -46860,8 +47523,8 @@ function parseDailyNote(path, day, text) {
46860
47523
  const ratingLine = all.find((l) => /rating/i.test(l) && RATING.test(l)) ?? all.find((l) => RATING.test(l));
46861
47524
  const match = ratingLine ? RATING.exec(ratingLine) : null;
46862
47525
  if (match) {
46863
- const [whole, approx, low, high] = match;
46864
- if (approx || high)
47526
+ const [whole, approx, low = "", high] = match;
47527
+ if (approx || high || low.includes("."))
46865
47528
  ratingQuestion = whole.trim();
46866
47529
  else
46867
47530
  rating = Number(low);
@@ -46883,7 +47546,7 @@ function parseDailyNote(path, day, text) {
46883
47546
  inCheck = false;
46884
47547
  if (line === ratingLine || /how was (the|your) day/i.test(line))
46885
47548
  continue;
46886
- if (HEALTH.test(line) || NOISE.test(line))
47549
+ if (HEALTH.test(line) || NOISE.test(line) || isPrivateLine(line))
46887
47550
  continue;
46888
47551
  kept.push(line);
46889
47552
  }
@@ -46893,8 +47556,13 @@ function parseDailyNote(path, day, text) {
46893
47556
  `).trim() || null;
46894
47557
  const firstProse = (body ?? "").split(/\n\s*\n/).map((p) => plain(p)).find((p) => p.length > 0);
46895
47558
  title = firstProse ? firstSentence(firstProse.replace(/^The day\s*/i, "")) : null;
47559
+ } else {
47560
+ 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(`
47561
+ `).replace(/\n{3,}/g, `
47562
+
47563
+ `).trim() || null;
46896
47564
  }
46897
- const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && plain(l).length > 0);
47565
+ const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && !isPrivateLine(l) && plain(l).length > 0);
46898
47566
  const highlight = milestoneLine ? firstSentence(plain(milestoneLine), 80) : null;
46899
47567
  const sleep = sleepOf(all);
46900
47568
  const prs = (text.match(PR_REF) ?? []).length;
@@ -47275,7 +47943,7 @@ var commands24 = [
47275
47943
  description: "How the days name their folder (default memory/daily)."
47276
47944
  }
47277
47945
  ],
47278
- 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.",
47946
+ 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.",
47279
47947
  run: async ({ args, flags, requireClient }) => {
47280
47948
  const dir = requireArg(args, 0, "dir");
47281
47949
  const items = scanDailyNotes(dir, flagString(flags, "label") ?? "memory/daily");
@@ -47300,6 +47968,42 @@ var commands24 = [
47300
47968
  };
47301
47969
  }
47302
47970
  }),
47971
+ defineCommand({
47972
+ operation: journalOperations.approveImport,
47973
+ path: ["journal", "import", "approve"],
47974
+ summary: "Approve staged days into the Journal: one month's batch, or --all; --rating answers a fuzzy rating.",
47975
+ args: [{ name: "batch", required: false, description: "The month to approve, YYYY-MM." }],
47976
+ flags: [
47977
+ { name: "all", type: "boolean", description: "Approve every staged batch." },
47978
+ {
47979
+ name: "rating",
47980
+ type: "string",
47981
+ description: "Answer a day's rating question, YYYY-MM-DD=N (1 to 10); repeat per day."
47982
+ }
47983
+ ],
47984
+ 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.",
47985
+ run: async ({ args, flags, requireClient }) => {
47986
+ const batch = args[0];
47987
+ const all = flagBool(flags, "all");
47988
+ if (batch === undefined === !all) {
47989
+ throw usageError("approve one month (YYYY-MM) or --all");
47990
+ }
47991
+ const ratings = flagStrings(flags, "rating").map((answer) => {
47992
+ const m = /^(\d{4}-\d{2}-\d{2})=(\d+(?:\.\d+)?)$/.exec(answer.trim());
47993
+ if (!m)
47994
+ throw usageError(`--rating takes YYYY-MM-DD=N, got "${answer}"`);
47995
+ return { day: m[1], rating: Number(m[2]) };
47996
+ });
47997
+ const input = parseInput(journalImportApproveInputSchema, {
47998
+ ...all ? { all: true } : { batch },
47999
+ ratings
48000
+ });
48001
+ return {
48002
+ data: await requireClient().post(pathOf(journalOperations.approveImport), input),
48003
+ render: renderJournalImportApprove
48004
+ };
48005
+ }
48006
+ }),
47303
48007
  defineCommand({
47304
48008
  operation: journalOperations.importOverview,
47305
48009
  path: ["journal", "import", "status"],
@@ -48186,6 +48890,118 @@ var commands30 = [
48186
48890
  // src/commands/money.ts
48187
48891
  import { readFileSync as readFileSync8 } from "fs";
48188
48892
  import { basename as basename5 } from "path";
48893
+
48894
+ // ../../packages/domain/src/money.ts
48895
+ var toNumber = (cents) => Number(cents) / 100;
48896
+ function formatMoney(amount, {
48897
+ currency = "EUR",
48898
+ signed = false,
48899
+ whole = false,
48900
+ cents: always = false,
48901
+ locale
48902
+ } = {}) {
48903
+ const exact = typeof amount === "bigint" ? amount : amountToCents(amount);
48904
+ const cents = whole ? roundToEuro(exact) : exact;
48905
+ const abs = cents < 0n ? -cents : cents;
48906
+ const text = new Intl.NumberFormat(locale ?? "en-IE", {
48907
+ style: "currency",
48908
+ currency,
48909
+ minimumFractionDigits: always || abs % 100n !== 0n ? 2 : 0,
48910
+ maximumFractionDigits: 2
48911
+ }).format(toNumber(abs));
48912
+ if (cents < 0n)
48913
+ return `\u2212${text}`;
48914
+ return signed && cents > 0n ? `+${text}` : text;
48915
+ }
48916
+ function roundToEuro(cents) {
48917
+ const abs = cents < 0n ? -cents : cents;
48918
+ const rounded = (abs + 50n) / 100n * 100n;
48919
+ return cents < 0n ? -rounded : rounded;
48920
+ }
48921
+ function goalSavedSource(goal) {
48922
+ const account = goal.accountTitle ?? "the account";
48923
+ if (goal.savedFrom === "account")
48924
+ return `all of ${account}`;
48925
+ if (goal.savedFrom === "account_share")
48926
+ return `its share of ${account}`;
48927
+ if (goal.accountTitle && goal.accountBalanceKnown === false) {
48928
+ return `counted toward it; the balance of ${goal.accountTitle} is unknown`;
48929
+ }
48930
+ return "counted toward it";
48931
+ }
48932
+
48933
+ // src/global-flags.ts
48934
+ var GLOBAL_FLAGS = [
48935
+ { name: "json", type: "boolean", description: "Emit JSON (automatic when stdout is piped)." },
48936
+ { name: "quiet", type: "boolean", description: "Suppress human output (errors still report)." },
48937
+ { name: "verbose", type: "boolean", description: "Extra diagnostics on stderr." },
48938
+ {
48939
+ name: "api-key",
48940
+ type: "string",
48941
+ description: "API key; overrides DO_API_KEY and the config file."
48942
+ },
48943
+ {
48944
+ name: "url",
48945
+ type: "string",
48946
+ description: "API origin; overrides DO_API_URL and the config file."
48947
+ },
48948
+ { name: "help", type: "boolean", description: "Show help for the command." },
48949
+ { name: "version", type: "boolean", description: "Print the CLI version." },
48950
+ {
48951
+ name: "data",
48952
+ type: "string",
48953
+ description: `JSON object body for create/update (e.g. --data '{"title":"Buy milk"}').`
48954
+ },
48955
+ {
48956
+ name: "allow-destructive",
48957
+ type: "boolean",
48958
+ description: "Permit a destructive verb (delete/revoke/remove-member). Off by default \u2014 a guardrail for autonomous agents."
48959
+ },
48960
+ {
48961
+ name: "dry-run",
48962
+ type: "boolean",
48963
+ description: "Preview a create/update/delete/archive (prints before/after) without writing \u2014 for agent-driven cleanup."
48964
+ }
48965
+ ];
48966
+
48967
+ // src/commands/money.ts
48968
+ function refuseUnknownFlags(flags, own) {
48969
+ const known = new Set([...own, ...GLOBAL_FLAGS.map((f) => f.name)]);
48970
+ const unknown = Object.keys(flags).filter((name) => !known.has(name));
48971
+ if (unknown.length > 0) {
48972
+ throw usageError(`unknown flag ${unknown.map((n) => `--${n}`).join(", ")} (takes ${own.map((n) => `--${n}`).join(", ")}; see \`do help\`)`);
48973
+ }
48974
+ }
48975
+ var ACCOUNT_KIND_FLAGS = [
48976
+ {
48977
+ name: "kind",
48978
+ type: "string",
48979
+ description: "current \xB7 savings \xB7 credit_card \xB7 cash \xB7 voucher \xB7 loan \xB7 investment \xB7 pension \xB7 asset_manual."
48980
+ },
48981
+ {
48982
+ name: "subtype",
48983
+ type: "string",
48984
+ description: "A pension's kind: group_insurance \xB7 pension_savings_fund \xB7 pension_savings_insurance \xB7 vapz (`none` clears it)."
48985
+ },
48986
+ {
48987
+ name: "locked-until",
48988
+ type: "string",
48989
+ description: "YYYY-MM-DD until which it can't be taken out (`none` clears it)."
48990
+ }
48991
+ ];
48992
+ function accountKindFields(flags) {
48993
+ const out = {};
48994
+ const kind = flagString(flags, "kind");
48995
+ const subtype = flagString(flags, "subtype");
48996
+ const lockedUntil = flagString(flags, "locked-until");
48997
+ if (kind !== undefined)
48998
+ out.kind = kind;
48999
+ if (subtype !== undefined)
49000
+ out.pensionSubtype = subtype === "none" ? null : subtype;
49001
+ if (lockedUntil !== undefined)
49002
+ out.lockedUntil = lockedUntil === "none" ? null : lockedUntil;
49003
+ return out;
49004
+ }
48189
49005
  function renderDedupe(data) {
48190
49006
  const r = data;
48191
49007
  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" : ""}`);
@@ -48194,6 +49010,38 @@ function renderDedupe(data) {
48194
49010
  return lines.join(`
48195
49011
  `);
48196
49012
  }
49013
+ function renderGoals(data) {
49014
+ const goals = Array.isArray(data) ? data : [];
49015
+ if (goals.length === 0)
49016
+ return "(no goals yet)";
49017
+ return goals.map((g) => {
49018
+ const whole = (a) => formatMoney(a, { whole: true });
49019
+ const parts = [
49020
+ `${whole(g.saved)} of ${whole(g.targetAmount)} (${goalSavedSource(g)})`,
49021
+ Number(g.remaining) > 0 ? `${whole(g.remaining)} to go` : "reached",
49022
+ g.monthlyNeed && g.targetOn ? `${formatMoney(g.monthlyNeed)} a month by ${g.targetOn}` : null
49023
+ ];
49024
+ return `${g.title} ${parts.filter(Boolean).join(" \xB7 ")} ${g.id}`;
49025
+ }).join(`
49026
+ `);
49027
+ }
49028
+ function syncLine(r) {
49029
+ const when = r.startedAt ? `${String(r.startedAt).slice(0, 16).replace("T", " ")} ` : "";
49030
+ const what = r.accountTitle ? `${r.accountTitle} ` : "";
49031
+ const counts = `fetched ${r.fetched ?? 0} \xB7 imported ${r.imported ?? 0} \xB7 matched ${r.matched ?? 0} \xB7 duplicates ${r.duplicates ?? 0}`;
49032
+ return `${when}${what}${r.outcome ?? "?"} ${counts}${r.error ? `
49033
+ ${r.error}` : ""}`;
49034
+ }
49035
+ function renderSyncLog(data) {
49036
+ const rows = Array.isArray(data) ? data : data.data ?? [];
49037
+ return rows.length === 0 ? "(no syncs yet)" : rows.map(syncLine).join(`
49038
+ `);
49039
+ }
49040
+ function renderSyncResults(data) {
49041
+ const { results } = data;
49042
+ return results.length === 0 ? "(no live bank links)" : results.map(syncLine).join(`
49043
+ `);
49044
+ }
48197
49045
  function partyFlag(flags) {
48198
49046
  const userId = flagString(flags, "with-user");
48199
49047
  const contactId = flagString(flags, "with-contact");
@@ -48237,13 +49085,17 @@ var commands31 = [
48237
49085
  path: ["money", "account-add"],
48238
49086
  summary: "Open a money account (Only me unless --data says visibility: space).",
48239
49087
  args: [],
48240
- flags: [{ name: "data", type: "string", description: "CreateMoneyAccountInput JSON." }],
49088
+ flags: [
49089
+ { name: "data", type: "string", description: "CreateMoneyAccountInput JSON." },
49090
+ ...ACCOUNT_KIND_FLAGS
49091
+ ],
48241
49092
  returns: "MoneyAccountView",
48242
- notes: `. \`do money account-add --data '{"title":"Joint","kind":"current","holder":"joint","visibility":"space"}'\`. An IBAN is stored only masked and hashed.`,
49093
+ notes: `. \`do money account-add --data '{"title":"Joint","kind":"current","holder":"joint","visibility":"space"}'\`. An IBAN is stored only masked and hashed. A pension: \`--kind pension --subtype group_insurance --locked-until 2055-01-01\` (a subtype on another kind is a 400).`,
48243
49094
  run: async ({ flags, requireClient }) => {
48244
49095
  const body = parseInput(createMoneyAccountInputSchema, {
48245
49096
  id: ulid3(),
48246
- ...readDataFlag(flags)
49097
+ ...readDataFlag(flags),
49098
+ ...accountKindFields(flags)
48247
49099
  });
48248
49100
  return {
48249
49101
  data: await requireClient().post(pathOf(moneyOperations.createAccount), body),
@@ -48258,6 +49110,7 @@ var commands31 = [
48258
49110
  args: [{ name: "id", required: true, description: "Account ULID." }],
48259
49111
  flags: [
48260
49112
  { name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." },
49113
+ ...ACCOUNT_KIND_FLAGS,
48261
49114
  {
48262
49115
  name: "balance",
48263
49116
  type: "string",
@@ -48270,8 +49123,14 @@ var commands31 = [
48270
49123
  }
48271
49124
  ],
48272
49125
  returns: "MoneyAccountView",
48273
- notes: ". `--balance 1234.56 --balance-at 2026-10-27` anchors an account whose bank export has no running balance (ING's): net worth counts from it, back and forward through the ledger. A negative one rides as `--balance=-20`. A day ahead is refused, and so is an account a bank link keeps. `--data` takes the rest (title, kind, iban, includeInNetWorth, \u2026).",
49126
+ 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. `--kind pension --subtype vapz --locked-until 2055-01-01` refiles it (an investment account moved to pension keeps its balance and ledger; moved off pension, the subtype is cleared). `--data` takes the rest (title, iban, includeInNetWorth, \u2026). A flag it doesn't take, or nothing to change, is refused before anything is sent.",
48274
49127
  run: async ({ args, flags, io, requireClient }) => {
49128
+ refuseUnknownFlags(flags, [
49129
+ "data",
49130
+ ...ACCOUNT_KIND_FLAGS.map((f) => f.name),
49131
+ "balance",
49132
+ "balance-at"
49133
+ ]);
48275
49134
  const id = requireArg(args, 0, "account id");
48276
49135
  const balance = flagString(flags, "balance");
48277
49136
  const balanceAt = flagString(flags, "balance-at");
@@ -48280,6 +49139,7 @@ var commands31 = [
48280
49139
  }
48281
49140
  const body = parseInput(updateMoneyAccountInputSchema, {
48282
49141
  ...readDataFlag(flags),
49142
+ ...accountKindFields(flags),
48283
49143
  ...balance !== undefined ? { balance, balanceAt: balanceAt ?? isoDay(io.now()) } : {}
48284
49144
  });
48285
49145
  return {
@@ -48330,7 +49190,7 @@ var commands31 = [
48330
49190
  });
48331
49191
  return {
48332
49192
  data: await requireClient().post(pathOf(moneyOperations.syncConnections), body),
48333
- render: renderRecord
49193
+ render: renderSyncResults
48334
49194
  };
48335
49195
  }
48336
49196
  }),
@@ -48367,7 +49227,7 @@ var commands31 = [
48367
49227
  args: [{ name: "id", required: true, description: "Bank link (connection) ULID." }],
48368
49228
  flags: [],
48369
49229
  returns: "MoneyConnectionSync[]",
48370
- run: readOperation(moneyOperations.listConnectionSyncs, renderList, { id: "id" })
49230
+ run: readOperation(moneyOperations.listConnectionSyncs, renderSyncLog, { id: "id" })
48371
49231
  }),
48372
49232
  defineCommand({
48373
49233
  operation: moneyOperations.listCategories,
@@ -49076,7 +49936,7 @@ var commands31 = [
49076
49936
  }
49077
49937
  ],
49078
49938
  returns: "MoneyNetWorth",
49079
- notes: ". total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed. `complete: false` when an account counts but its balance isn't known (a bank export with no running balance): it's listed under `unknown`, left out of the total, never counted as 0; `do money account-update <id> --balance \u2026 --balance-at \u2026` states it.",
49939
+ notes: ". total, have and owe this month, and liquid beside locked (a pension, or an account locked until a day ahead; each account says `locked`, `lockedUntil` and a pension's `pensionSubtype`); 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.",
49080
49940
  run: async ({ flags, requireClient }) => ({
49081
49941
  data: await requireClient().get(pathOf(moneyOperations.netWorthRead), {
49082
49942
  query: { scope: flagString(flags, "scope"), months: flagString(flags, "months") }
@@ -49115,7 +49975,8 @@ var commands31 = [
49115
49975
  args: [],
49116
49976
  flags: [],
49117
49977
  returns: "MoneyGoalView[]",
49118
- run: readOperation(moneyOperations.listGoals, renderList)
49978
+ 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`.",
49979
+ run: readOperation(moneyOperations.listGoals, renderGoals)
49119
49980
  }),
49120
49981
  defineCommand({
49121
49982
  operation: moneyOperations.getGoal,
@@ -49660,6 +50521,139 @@ var commands33 = [
49660
50521
  // src/commands/people.ts
49661
50522
  import { readFileSync as readFileSync9 } from "fs";
49662
50523
  import { basename as basename6 } from "path";
50524
+
50525
+ // ../../packages/domain/src/family-tree.ts
50526
+ function lifeYears(p) {
50527
+ if (p.birthYear && p.deathYear)
50528
+ return `${p.birthYear}\u2013${p.deathYear}`;
50529
+ if (p.deathYear)
50530
+ return `\u2013${p.deathYear}`;
50531
+ return p.birthYear ? String(p.birthYear) : "";
50532
+ }
50533
+ function greats(generation) {
50534
+ if (generation < 3)
50535
+ return "";
50536
+ const n = generation - 2;
50537
+ return n === 1 ? "great-" : `${n}\xD7 great-`;
50538
+ }
50539
+ function generationLabel(generation) {
50540
+ if (generation === 0)
50541
+ return "You";
50542
+ if (generation === 1)
50543
+ return "Parents";
50544
+ const word = `${greats(generation)}grandparents`;
50545
+ return word.charAt(0).toUpperCase() + word.slice(1);
50546
+ }
50547
+ function ancestorTitle(n) {
50548
+ const generation = ahnentafelGeneration(n);
50549
+ if (generation === 0)
50550
+ return "You";
50551
+ const male = n % 2 === 0;
50552
+ if (generation === 1)
50553
+ return male ? "Father" : "Mother";
50554
+ const word = `${greats(generation)}grand${male ? "father" : "mother"}`;
50555
+ return word.charAt(0).toUpperCase() + word.slice(1);
50556
+ }
50557
+ function pedigreeOutline(view) {
50558
+ const people = new Map(view.people.map((p) => [p.ref, p]));
50559
+ const slots = new Map(view.ancestors.map((a) => [a.n, a]));
50560
+ const rows = [];
50561
+ const visit = (n) => {
50562
+ const slot = slots.get(n);
50563
+ const person = slot ? people.get(slot.ref) : undefined;
50564
+ if (!slot || !person)
50565
+ return;
50566
+ const parents = [2 * n, 2 * n + 1].filter((m) => slots.has(m));
50567
+ rows.push({
50568
+ n,
50569
+ generation: slot.generation,
50570
+ title: ancestorTitle(n),
50571
+ person,
50572
+ repeatOf: slot.repeatOf,
50573
+ parents
50574
+ });
50575
+ for (const m of parents)
50576
+ visit(m);
50577
+ };
50578
+ visit(1);
50579
+ return rows;
50580
+ }
50581
+ function pedigreeText(view) {
50582
+ const lines = pedigreeOutline(view).map((r) => {
50583
+ const years = lifeYears(r.person);
50584
+ const name = years ? `${r.person.name} (${years})` : r.person.name;
50585
+ const again = r.repeatOf ? ` \xB7 also #${r.repeatOf}, line shown there` : "";
50586
+ return `${" ".repeat(r.generation)}${r.title} \xB7 ${name} \xB7 ${r.person.ref}${again}`;
50587
+ });
50588
+ const deepest = generationLabel(view.generations).toLowerCase();
50589
+ const more = view.deeper ? " The tree goes further back: ask for a bigger depth." : "";
50590
+ return [...lines, "", `${view.generations} generations back (to the ${deepest}).${more}`].join(`
50591
+ `);
50592
+ }
50593
+
50594
+ // src/commands/people.ts
50595
+ var EDIT_NOTE = "Answers with a receipt: edit.id (for `do people tree undo`), the changes person by person, and the person after. Edits are kept over a new import.";
50596
+ var PERSON_FLAGS = [
50597
+ { name: "given", type: "string", description: "First name(s)." },
50598
+ { name: "surname", type: "string", description: "Surname." },
50599
+ { name: "sex", type: "string", description: "M or F." },
50600
+ { name: "born", type: "string", description: "Birth date: YYYY or YYYY-MM-DD." },
50601
+ { name: "born-place", type: "string", description: "Where they were born." },
50602
+ { name: "died", type: "string", description: "Death date: YYYY or YYYY-MM-DD." },
50603
+ { name: "died-place", type: "string", description: "Where they died." }
50604
+ ];
50605
+ var PERSON_FLAG_FIELDS = {
50606
+ given: "given",
50607
+ surname: "surname",
50608
+ sex: "sex",
50609
+ born: "birthDate",
50610
+ "born-place": "birthPlace",
50611
+ died: "deathDate",
50612
+ "died-place": "deathPlace"
50613
+ };
50614
+ var FACT_FLAGS = [
50615
+ { name: "date", type: "string", description: "The date as written (ABT 1720, 1801-03-02)." },
50616
+ { name: "place", type: "string", description: "Where." },
50617
+ { name: "value", type: "string", description: "The value or note (farmer, a line of text)." }
50618
+ ];
50619
+ var CLEAR_FLAG = {
50620
+ name: "clear",
50621
+ type: "string",
50622
+ description: "Fields to clear, comma-separated."
50623
+ };
50624
+ var FACT_ARGS = [
50625
+ { name: "ref", required: true, description: "Their ref in the tree." },
50626
+ { name: "factId", required: true, description: "The fact's id (OCCU-1, f-\u2026)." }
50627
+ ];
50628
+ var LINK_ARGS = [
50629
+ { name: "kind", required: true, description: "parent, child or partner." },
50630
+ { name: "ref", required: true, description: "The person's ref." },
50631
+ { name: "other", required: true, description: "Their parent's, child's or partner's ref." }
50632
+ ];
50633
+ var pick2 = (flags, fields) => Object.fromEntries(Object.entries(fields).flatMap(([flag, field]) => {
50634
+ const v = flagString(flags, flag);
50635
+ return v === undefined ? [] : [[field, v]];
50636
+ }));
50637
+ var personFields = (flags) => pick2(flags, { name: "name", ...PERSON_FLAG_FIELDS });
50638
+ var factFields = (flags) => pick2(flags, { date: "date", place: "place", value: "value" });
50639
+ var clears = (flags) => Object.fromEntries((flagString(flags, "clear") ?? "").split(",").map((f) => f.trim()).filter(Boolean).map((f) => [f, null]));
50640
+ var factParams = (args) => ({
50641
+ ref: parseInput(familyTreeRefSchema, requireArg(args, 0, "ref")),
50642
+ factId: parseInput(familyTreeFactIdSchema, requireArg(args, 1, "fact id"))
50643
+ });
50644
+ function linkBody(args) {
50645
+ const kind = requireArg(args, 0, "kind");
50646
+ const ref = requireArg(args, 1, "ref");
50647
+ const other = requireArg(args, 2, "other");
50648
+ if (kind === "parent")
50649
+ return parseInput(familyTreeLinkInputSchema, { kind, parent: other, child: ref });
50650
+ if (kind === "child") {
50651
+ return parseInput(familyTreeLinkInputSchema, { kind: "parent", parent: ref, child: other });
50652
+ }
50653
+ if (kind === "partner")
50654
+ return parseInput(familyTreeLinkInputSchema, { kind, a: ref, b: other });
50655
+ throw usageError("the kind is parent, child or partner");
50656
+ }
49663
50657
  var commands34 = [
49664
50658
  {
49665
50659
  spec: {
@@ -50015,6 +51009,34 @@ var commands34 = [
50015
51009
  };
50016
51010
  }
50017
51011
  },
51012
+ {
51013
+ spec: {
51014
+ path: ["people", "tree", "ancestors"],
51015
+ summary: "Your direct line back, as a text pedigree: as far as --depth, or all of it.",
51016
+ args: [],
51017
+ flags: [
51018
+ {
51019
+ name: "depth",
51020
+ type: "string",
51021
+ description: "How many generations back (1\u201350; default all)."
51022
+ },
51023
+ { name: "ref", type: "string", description: "Whose ancestors (default: you in the tree)." }
51024
+ ],
51025
+ scopes: [...familyTreeOperations.ancestors.scopes],
51026
+ returns: "FamilyTreeAncestors",
51027
+ notes: "Each line is who they are to you, the name, the years and their ref, indented by generation; father's line before mother's. --json gives the slots by Ahnentafel number (you 1, a father 2n, a mother 2n+1)."
51028
+ },
51029
+ run: async ({ flags, requireClient }) => {
51030
+ const depth = flagString(flags, "depth");
51031
+ const ref = flagString(flags, "ref");
51032
+ const query = { ...depth ? { depth } : {}, ...ref ? { ref } : {} };
51033
+ parseInput(familyTreeAncestorsQuerySchema, query);
51034
+ return {
51035
+ data: await requireClient().get(pathOf(familyTreeOperations.ancestors), { query }),
51036
+ render: (data) => pedigreeText(data)
51037
+ };
51038
+ }
51039
+ },
50018
51040
  {
50019
51041
  spec: {
50020
51042
  path: ["people", "tree", "list"],
@@ -50099,6 +51121,223 @@ var commands34 = [
50099
51121
  render: renderRecord
50100
51122
  };
50101
51123
  }
51124
+ },
51125
+ {
51126
+ spec: {
51127
+ path: ["people", "tree", "person"],
51128
+ summary: "One person in your tree: facts (with their ids), parents, partners, children.",
51129
+ args: [{ name: "ref", required: true, description: "Their ref in the tree (I12)." }],
51130
+ flags: [],
51131
+ scopes: [...familyTreeOperations.person.scopes],
51132
+ returns: "FamilyTreePersonDetail"
51133
+ },
51134
+ run: async ({ args, requireClient }) => {
51135
+ const ref = parseInput(familyTreeRefSchema, requireArg(args, 0, "ref"));
51136
+ return {
51137
+ data: await requireClient().get(pathOf(familyTreeOperations.person, { ref })),
51138
+ render: renderRecord
51139
+ };
51140
+ }
51141
+ },
51142
+ {
51143
+ spec: {
51144
+ path: ["people", "tree", "person", "add"],
51145
+ summary: "Add someone your tree is missing.",
51146
+ args: [{ name: "name", required: true, description: "Their full name." }],
51147
+ flags: PERSON_FLAGS,
51148
+ scopes: [...familyTreeOperations.addPerson.scopes],
51149
+ returns: "FamilyTreeEditReceipt",
51150
+ notes: `${EDIT_NOTE} They get a new ref (P\u2026): link them next (\`do people tree link parent <child> <parent>\`).`
51151
+ },
51152
+ run: async ({ args, flags, requireClient }) => {
51153
+ const body = parseInput(addFamilyTreePersonInputSchema, {
51154
+ ...personFields(flags),
51155
+ name: args.join(" ").trim() || requireArg(args, 0, "name")
51156
+ });
51157
+ return {
51158
+ data: await requireClient().post(pathOf(familyTreeOperations.addPerson), body),
51159
+ render: renderRecord
51160
+ };
51161
+ }
51162
+ },
51163
+ {
51164
+ spec: {
51165
+ path: ["people", "tree", "person", "edit"],
51166
+ summary: "Change a person in your tree: name, sex, birth, death.",
51167
+ args: [{ name: "ref", required: true, description: "Their ref in the tree." }],
51168
+ flags: [
51169
+ { name: "name", type: "string", description: "Their full name." },
51170
+ ...PERSON_FLAGS,
51171
+ CLEAR_FLAG
51172
+ ],
51173
+ scopes: [...familyTreeOperations.editPerson.scopes],
51174
+ returns: "FamilyTreeEditReceipt",
51175
+ notes: `${EDIT_NOTE} --clear takes field names (birthPlace,deathDate,\u2026).`
51176
+ },
51177
+ run: async ({ args, flags, requireClient }) => {
51178
+ const ref = parseInput(familyTreeRefSchema, requireArg(args, 0, "ref"));
51179
+ const body = parseInput(editFamilyTreePersonInputSchema, {
51180
+ ...personFields(flags),
51181
+ ...clears(flags),
51182
+ ...readDataFlag(flags)
51183
+ });
51184
+ return {
51185
+ data: await requireClient().patch(pathOf(familyTreeOperations.editPerson, { ref }), body),
51186
+ render: renderRecord
51187
+ };
51188
+ }
51189
+ },
51190
+ {
51191
+ spec: {
51192
+ path: ["people", "tree", "person", "remove"],
51193
+ summary: "Take a person out of your tree (and out of their families).",
51194
+ args: [{ name: "ref", required: true, description: "Their ref in the tree." }],
51195
+ flags: [],
51196
+ scopes: [...familyTreeOperations.removePerson.scopes],
51197
+ returns: "FamilyTreeEditReceipt",
51198
+ notes: `${EDIT_NOTE} An agent's key waits for the member's yes, as every delete does.`
51199
+ },
51200
+ run: async ({ args, requireClient }) => {
51201
+ const ref = parseInput(familyTreeRefSchema, requireArg(args, 0, "ref"));
51202
+ return {
51203
+ data: await requireClient().delete(pathOf(familyTreeOperations.removePerson, { ref })),
51204
+ render: renderRecord
51205
+ };
51206
+ }
51207
+ },
51208
+ {
51209
+ spec: {
51210
+ path: ["people", "tree", "fact", "add"],
51211
+ summary: "Add a fact to a person: an occupation, a residence, a note.",
51212
+ args: [
51213
+ { name: "ref", required: true, description: "Their ref in the tree." },
51214
+ {
51215
+ name: "type",
51216
+ required: true,
51217
+ description: "What kind (occupation, residence, note, \u2026)."
51218
+ }
51219
+ ],
51220
+ flags: FACT_FLAGS,
51221
+ scopes: [...familyTreeOperations.addFact.scopes],
51222
+ returns: "FamilyTreeEditReceipt",
51223
+ notes: EDIT_NOTE
51224
+ },
51225
+ run: async ({ args, flags, requireClient }) => {
51226
+ const ref = parseInput(familyTreeRefSchema, requireArg(args, 0, "ref"));
51227
+ const body = parseInput(addFamilyTreeFactInputSchema, {
51228
+ type: requireArg(args, 1, "type"),
51229
+ ...factFields(flags)
51230
+ });
51231
+ return {
51232
+ data: await requireClient().post(pathOf(familyTreeOperations.addFact, { ref }), body),
51233
+ render: renderRecord
51234
+ };
51235
+ }
51236
+ },
51237
+ {
51238
+ spec: {
51239
+ path: ["people", "tree", "fact", "edit"],
51240
+ summary: "Change one of a person's facts.",
51241
+ args: FACT_ARGS,
51242
+ flags: [
51243
+ { name: "type", type: "string", description: "What kind of fact." },
51244
+ ...FACT_FLAGS,
51245
+ CLEAR_FLAG
51246
+ ],
51247
+ scopes: [...familyTreeOperations.editFact.scopes],
51248
+ returns: "FamilyTreeEditReceipt",
51249
+ notes: `${EDIT_NOTE} --clear takes field names (date,place,value).`
51250
+ },
51251
+ run: async ({ args, flags, requireClient }) => {
51252
+ const params = factParams(args);
51253
+ const type = flagString(flags, "type");
51254
+ const body = parseInput(editFamilyTreeFactInputSchema, {
51255
+ ...type ? { type } : {},
51256
+ ...factFields(flags),
51257
+ ...clears(flags)
51258
+ });
51259
+ return {
51260
+ data: await requireClient().patch(pathOf(familyTreeOperations.editFact, params), body),
51261
+ render: renderRecord
51262
+ };
51263
+ }
51264
+ },
51265
+ {
51266
+ spec: {
51267
+ path: ["people", "tree", "fact", "remove"],
51268
+ summary: "Remove one of a person's facts.",
51269
+ args: FACT_ARGS,
51270
+ flags: [],
51271
+ scopes: [...familyTreeOperations.removeFact.scopes],
51272
+ returns: "FamilyTreeEditReceipt",
51273
+ notes: `${EDIT_NOTE} An agent's key waits for the member's yes, as every delete does.`
51274
+ },
51275
+ run: async ({ args, requireClient }) => {
51276
+ return {
51277
+ data: await requireClient().delete(pathOf(familyTreeOperations.removeFact, factParams(args))),
51278
+ render: renderRecord
51279
+ };
51280
+ }
51281
+ },
51282
+ {
51283
+ spec: {
51284
+ path: ["people", "tree", "link"],
51285
+ summary: "Link two people: give someone a parent or a child, or a partner.",
51286
+ args: LINK_ARGS,
51287
+ flags: [],
51288
+ scopes: [...familyTreeOperations.link.scopes],
51289
+ returns: "FamilyTreeEditReceipt",
51290
+ notes: `${EDIT_NOTE} A child has at most two parents.`
51291
+ },
51292
+ run: async ({ args, requireClient }) => ({
51293
+ data: await requireClient().post(pathOf(familyTreeOperations.link), linkBody(args)),
51294
+ render: renderRecord
51295
+ })
51296
+ },
51297
+ {
51298
+ spec: {
51299
+ path: ["people", "tree", "unlink"],
51300
+ summary: "Undo a parent, child or partner link.",
51301
+ args: LINK_ARGS,
51302
+ flags: [],
51303
+ scopes: [...familyTreeOperations.unlink.scopes],
51304
+ returns: "FamilyTreeEditReceipt",
51305
+ notes: `${EDIT_NOTE} Partners with children together: unlink a parent first.`
51306
+ },
51307
+ run: async ({ args, requireClient }) => ({
51308
+ data: await requireClient().post(pathOf(familyTreeOperations.unlink), linkBody(args)),
51309
+ render: renderRecord
51310
+ })
51311
+ },
51312
+ {
51313
+ spec: {
51314
+ path: ["people", "tree", "edits"],
51315
+ summary: "Your edits to the tree, oldest first; the ones the current file has no room for are flagged.",
51316
+ args: [],
51317
+ flags: [],
51318
+ scopes: [...familyTreeOperations.edits.scopes],
51319
+ returns: "FamilyTreeEdits",
51320
+ notes: "Edits are kept over every new import: one whose person isn't in the new file is applied: false with its reason, and applies again when a later file has them."
51321
+ },
51322
+ run: async ({ requireClient }) => ({
51323
+ data: await requireClient().get(pathOf(familyTreeOperations.edits)),
51324
+ render: renderRecord
51325
+ })
51326
+ },
51327
+ {
51328
+ spec: {
51329
+ path: ["people", "tree", "undo"],
51330
+ summary: "Undo one edit to the tree, by the id its receipt gave.",
51331
+ args: [{ name: "editId", required: true, description: "The receipt's edit.id." }],
51332
+ flags: [],
51333
+ scopes: [...familyTreeOperations.undo.scopes],
51334
+ returns: "FamilyTreeUndoResult",
51335
+ notes: "skipped lists the edits that leaned on it (a fact on a person it added)."
51336
+ },
51337
+ run: async ({ args, requireClient }) => ({
51338
+ data: await requireClient().post(pathOf(familyTreeOperations.undo, { id: requireArg(args, 0, "edit id") }), {}),
51339
+ render: renderRecord
51340
+ })
50102
51341
  }
50103
51342
  ];
50104
51343
 
@@ -52147,7 +53386,7 @@ var commands49 = [
52147
53386
  "#tag": "Adds a tag. Repeatable: '#home #urgent'.",
52148
53387
  "!": "Stars the task \u2014 the 1\u20132 things that matter (aliases: !star/!s/!flag/!f). Drives Today's focus and the starred filter.",
52149
53388
  "~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`).",
52150
- 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.",
53389
+ 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.",
52151
53390
  "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.",
52152
53391
  estimate: "How long it takes (durationMinutes): '30m', '45 min', '1h', '1.5h', '1h30', '2 hours', 'for 20 mins'. Not 'in 2 hours'."
52153
53392
  }
@@ -52942,38 +54181,6 @@ var FEATURE_COMMANDS = [
52942
54181
  // src/registry.ts
52943
54182
  var COMMANDS = FEATURE_COMMANDS;
52944
54183
  var COMMAND_SPECS = COMMANDS.map((c) => c.spec);
52945
- var GLOBAL_FLAGS = [
52946
- { name: "json", type: "boolean", description: "Emit JSON (automatic when stdout is piped)." },
52947
- { name: "quiet", type: "boolean", description: "Suppress human output (errors still report)." },
52948
- { name: "verbose", type: "boolean", description: "Extra diagnostics on stderr." },
52949
- {
52950
- name: "api-key",
52951
- type: "string",
52952
- description: "API key; overrides DO_API_KEY and the config file."
52953
- },
52954
- {
52955
- name: "url",
52956
- type: "string",
52957
- description: "API origin; overrides DO_API_URL and the config file."
52958
- },
52959
- { name: "help", type: "boolean", description: "Show help for the command." },
52960
- { name: "version", type: "boolean", description: "Print the CLI version." },
52961
- {
52962
- name: "data",
52963
- type: "string",
52964
- description: `JSON object body for create/update (e.g. --data '{"title":"Buy milk"}').`
52965
- },
52966
- {
52967
- name: "allow-destructive",
52968
- type: "boolean",
52969
- description: "Permit a destructive verb (delete/revoke/remove-member). Off by default \u2014 a guardrail for autonomous agents."
52970
- },
52971
- {
52972
- name: "dry-run",
52973
- type: "boolean",
52974
- description: "Preview a create/update/delete/archive (prints before/after) without writing \u2014 for agent-driven cleanup."
52975
- }
52976
- ];
52977
54184
  function matchCommand(positionals) {
52978
54185
  for (const len of [4, 3, 2, 1]) {
52979
54186
  const candidate = positionals.slice(0, len);