@robbeverhelst/do 1.334.0 → 1.336.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 +801 -50
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -14418,7 +14418,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14418
14418
 
14419
14419
  // src/meta.ts
14420
14420
  var CLI_NAME = "do";
14421
- var VERSION = "1.334.0";
14421
+ var VERSION = "1.336.0";
14422
14422
  var API_PREFIX = "/api/v1";
14423
14423
  var DEFAULT_API_URL = "http://localhost:3001";
14424
14424
 
@@ -22915,6 +22915,12 @@ var moneyAccountKind = pgEnum("money_account_kind", [
22915
22915
  "pension",
22916
22916
  "asset_manual"
22917
22917
  ]);
22918
+ var moneyPensionSubtype = pgEnum("money_pension_subtype", [
22919
+ "group_insurance",
22920
+ "pension_savings_fund",
22921
+ "pension_savings_insurance",
22922
+ "vapz"
22923
+ ]);
22918
22924
  var moneyAccountHolder = pgEnum("money_account_holder", ["me", "joint", "other"]);
22919
22925
  var moneyAccountEntity = pgEnum("money_account_entity", ["personal", "business"]);
22920
22926
  var moneyImportSource = pgEnum("money_import_source", [
@@ -22966,6 +22972,8 @@ var moneyTaxTag = pgEnum("money_tax_tag", [
22966
22972
  var moneyAccounts = pgTable("money_accounts", {
22967
22973
  ...leafColumns(),
22968
22974
  kind: moneyAccountKind("kind").notNull().default("current"),
22975
+ pensionSubtype: moneyPensionSubtype("pension_subtype"),
22976
+ lockedUntil: date5("locked_until"),
22969
22977
  holder: moneyAccountHolder("holder").notNull().default("me"),
22970
22978
  entity: moneyAccountEntity("entity").notNull().default("personal"),
22971
22979
  institutionContactId: text("institution_contact_id").references(() => contacts.id, {
@@ -25202,6 +25210,7 @@ var fileViewLinkSchema = object({
25202
25210
 
25203
25211
  // ../../packages/schema/src/contracts/money.ts
25204
25212
  var moneyAccountKindSchema = _enum2(moneyAccountKind.enumValues);
25213
+ var moneyPensionSubtypeSchema = _enum2(moneyPensionSubtype.enumValues);
25205
25214
  var moneyAccountHolderSchema = _enum2(moneyAccountHolder.enumValues);
25206
25215
  var moneyAccountEntitySchema = _enum2(moneyAccountEntity.enumValues);
25207
25216
  var moneyImportSourceSchema = _enum2(moneyImportSource.enumValues);
@@ -25297,6 +25306,8 @@ var createMoneyAccountInputSchema = object({
25297
25306
  id: ulidSchema,
25298
25307
  title: string2().trim().min(1).max(120),
25299
25308
  kind: moneyAccountKindSchema.optional(),
25309
+ pensionSubtype: moneyPensionSubtypeSchema.nullish(),
25310
+ lockedUntil: keyDateDaySchema.nullish(),
25300
25311
  holder: moneyAccountHolderSchema.optional(),
25301
25312
  institutionContactId: ulidSchema.nullish(),
25302
25313
  iban: string2().trim().transform(compactIban).pipe(string2().regex(/^[A-Z]{2}\d{2}[A-Z0-9]{8,30}$/, "expected an IBAN")).nullish(),
@@ -25310,7 +25321,9 @@ var moneyBalanceAtSchema = union([keyDateDaySchema, exports_iso.datetime({ offse
25310
25321
  var updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().extend({
25311
25322
  balance: moneyAmountSchema.optional(),
25312
25323
  balanceAt: moneyBalanceAtSchema.optional()
25313
- }).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), {
25314
25327
  message: "a balance and the day it's from go together",
25315
25328
  path: ["balanceAt"]
25316
25329
  });
@@ -26558,6 +26571,16 @@ var familyTreeUnionSchema = object({
26558
26571
  partners: array(familyTreeRefSchema),
26559
26572
  children: array(familyTreeRefSchema)
26560
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
+ }
26561
26584
  var familyTreePersonViewSchema = object({
26562
26585
  ref: familyTreeRefSchema,
26563
26586
  name: string2(),
@@ -26615,11 +26638,24 @@ var familyTreeViewSchema = object({
26615
26638
  birthdaysOffered: array(familyTreeRefSchema),
26616
26639
  branch: familyTreeBranchViewSchema.nullable(),
26617
26640
  matches: array(familyTreeMatchSchema),
26641
+ edits: object({ count: number2().int(), skipped: number2().int() }),
26618
26642
  updatedAt: exports_coerce.date()
26619
26643
  });
26620
26644
  var familyTreeResponseSchema = object({ tree: familyTreeViewSchema.nullable() });
26621
26645
  var sourceNameSchema = string2().trim().min(1).max(80);
26622
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
+ });
26623
26659
  var familyTreeQuerySchema = object({ side: familyTreeSideSchema.optional() });
26624
26660
  var familyTreeUploadFieldsSchema = object({
26625
26661
  sourceName: preprocess((v) => v === "" ? undefined : v, sourceNameSchema.optional()),
@@ -26656,7 +26692,118 @@ var familyTreeMatchDecisionResultSchema = object({
26656
26692
  matches: array(familyTreeMatchViewSchema)
26657
26693
  });
26658
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
+
26659
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.";
26660
26807
  var NOT_IN_STAR = "A sensitive scope (ADR 0028): never part of `*`, and only the tree of the member who minted the key.";
26661
26808
  var familyTreeOperations = defineOperations({
26662
26809
  import: {
@@ -26693,6 +26840,31 @@ var familyTreeOperations = defineOperations({
26693
26840
  response: { description: "`{ tree }`." },
26694
26841
  errors: { 400: true, 403: "The key doesn't hold `people:family-tree`, or is no member's." }
26695
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
+ },
26696
26868
  people: {
26697
26869
  method: "get",
26698
26870
  path: "/family-tree/people",
@@ -26744,6 +26916,150 @@ var familyTreeOperations = defineOperations({
26744
26916
  status: 200,
26745
26917
  response: { description: "`{ id, did, matches }`." },
26746
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"
26747
27063
  }
26748
27064
  });
26749
27065
 
@@ -27940,7 +28256,7 @@ var foodPatternsSchema = object({
27940
28256
  // ../../packages/schema/src/operations/food-log.ts
27941
28257
  var NO_MEMBER = "The key holds no health scope by name, or has no member behind it.";
27942
28258
  var HIDDEN2 = "No such meal, or it isn't the key's member's.";
27943
- 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.";
27944
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.";
27945
28261
  var meal = { id: "The meal's ULID." };
27946
28262
  var mealId = { id: ulidSchema };
@@ -27970,7 +28286,7 @@ var foodLogOperations = defineOperations({
27970
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.`,
27971
28287
  body: createMealLogInputSchema,
27972
28288
  status: 201,
27973
- response: { description: RECEIPT },
28289
+ response: { description: RECEIPT2 },
27974
28290
  errors: { 400: true, 403: NO_MEMBER },
27975
28291
  operationId: "food_log"
27976
28292
  },
@@ -27998,7 +28314,7 @@ var foodLogOperations = defineOperations({
27998
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}`,
27999
28315
  body: correctMealLogInputSchema,
28000
28316
  status: 200,
28001
- response: { description: RECEIPT },
28317
+ response: { description: RECEIPT2 },
28002
28318
  errors: { 400: true, 404: HIDDEN2 },
28003
28319
  operationId: "food_correct"
28004
28320
  },
@@ -28025,7 +28341,7 @@ var foodLogOperations = defineOperations({
28025
28341
  summary: "Undo a meal's last correction",
28026
28342
  description: "Take back the latest correction: the items it replaced or removed are current again.",
28027
28343
  status: 200,
28028
- response: { description: RECEIPT },
28344
+ response: { description: RECEIPT2 },
28029
28345
  errors: { 400: true, 404: HIDDEN2 }
28030
28346
  },
28031
28347
  saveMeal: {
@@ -33038,7 +33354,10 @@ var moneyNetWorthAccountSchema = object({
33038
33354
  currency: string2(),
33039
33355
  value: amount5,
33040
33356
  source: moneySnapshotSourceSchema,
33041
- month: moneyMonthSchema
33357
+ month: moneyMonthSchema,
33358
+ pensionSubtype: moneyPensionSubtypeSchema.nullable(),
33359
+ lockedUntil: string2().nullable(),
33360
+ locked: boolean2()
33042
33361
  });
33043
33362
  var moneyNetWorthUnknownSchema = moneyNetWorthAccountSchema.pick({
33044
33363
  accountId: true,
@@ -33053,6 +33372,8 @@ var moneyNetWorthSchema = object({
33053
33372
  total: amount5,
33054
33373
  have: amount5,
33055
33374
  owe: amount5,
33375
+ liquid: amount5,
33376
+ locked: amount5,
33056
33377
  points: array(moneyNetWorthPointSchema),
33057
33378
  accounts: array(moneyNetWorthAccountSchema),
33058
33379
  complete: boolean2(),
@@ -33263,7 +33584,7 @@ var moneyOperations = defineOperations({
33263
33584
  scopes: ["finance:write"],
33264
33585
  tag: "Money",
33265
33586
  summary: "Open a money account",
33266
- 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.',
33267
33588
  body: createMoneyAccountInputSchema,
33268
33589
  status: 201,
33269
33590
  response: { description: "The account." },
@@ -33338,7 +33659,7 @@ var moneyOperations = defineOperations({
33338
33659
  scopes: ["finance:write"],
33339
33660
  tag: "Money",
33340
33661
  summary: "Update a money account",
33341
- description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it. `balance` with `balanceAt` (a day, read as its end, or a moment) states what it held then: the anchor net worth counts from, back and forward through the ledger, for a bank whose export has no running balance. The two go together; a day ahead, or an account a bank link keeps, is a 400.",
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.",
33342
33663
  pathParams: { id: "The account's ULID." },
33343
33664
  pathParamSchemas: { id: ulidSchema },
33344
33665
  body: updateMoneyAccountInputSchema,
@@ -33858,7 +34179,7 @@ var moneyOperations = defineOperations({
33858
34179
  scopes: ["finance:read"],
33859
34180
  tag: "Money",
33860
34181
  summary: "Get net worth and its monthly line",
33861
- description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `points` (one per month, oldest first, the current month last; `total` null before any account had a value) and `accounts` (biggest first, each with its `value`, `source` \u2014 balance, ledger or manual \u2014 and the `month` that value is from). A loan or a credit card always counts as owed. This month follows each account's balance, carried forward by what was booked after `balanceAt`; the months before an account was first read are worked back from its ledger; a value set by hand carries on until the next one. An account whose ledger came from a bank but has no balance yet (an export with no running balance) is never counted as 0: it is listed under `unknown` and left out of the total, and `complete` is false. `scope`: `all` (default), `space` or `owner`; `months` 2\u201360 (12 by default).",
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).",
33862
34183
  query: [
33863
34184
  { name: "scope", description: "all, space or owner." },
33864
34185
  { name: "months", description: "How many months the line covers (2\u201360, default 12)." }
@@ -34566,12 +34887,12 @@ var periodOperations = defineOperations({
34566
34887
  var placePointSchema = tuple([number2().finite(), number2().finite()]);
34567
34888
  var placeOutlineSchema = array(placePointSchema).min(3).max(200);
34568
34889
  var placeKindSchema = _enum2(placeKindValues);
34569
- var placeSchema = createSelectSchema(places, {
34890
+ var placeSchema2 = createSelectSchema(places, {
34570
34891
  outline: placeOutlineSchema.nullable(),
34571
34892
  kind: string2().default("room"),
34572
34893
  gardenFeatureId: string2().nullable().default(null)
34573
34894
  });
34574
- var placeViewSchema = placeSchema.extend({
34895
+ var placeViewSchema = placeSchema2.extend({
34575
34896
  title: string2(),
34576
34897
  parentId: string2().nullable().default(null),
34577
34898
  createdAt: exports_coerce.date(),
@@ -36009,7 +36330,8 @@ var projectOperations = defineOperations({
36009
36330
  var occurrenceViewSchema = occurrenceSchema.extend({
36010
36331
  title: string2(),
36011
36332
  templateKind: _enum2(["task", "habit", "routine"]),
36012
- measurementType: habitMeasurementTypeSchema.nullable()
36333
+ measurementType: habitMeasurementTypeSchema.nullable(),
36334
+ allDay: boolean2().optional()
36013
36335
  });
36014
36336
  var todayResponseSchema = object({
36015
36337
  from: exports_coerce.date(),
@@ -48608,7 +48930,78 @@ function goalSavedSource(goal) {
48608
48930
  return "counted toward it";
48609
48931
  }
48610
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
+
48611
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
+ }
48612
49005
  function renderDedupe(data) {
48613
49006
  const r = data;
48614
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" : ""}`);
@@ -48692,13 +49085,17 @@ var commands31 = [
48692
49085
  path: ["money", "account-add"],
48693
49086
  summary: "Open a money account (Only me unless --data says visibility: space).",
48694
49087
  args: [],
48695
- flags: [{ name: "data", type: "string", description: "CreateMoneyAccountInput JSON." }],
49088
+ flags: [
49089
+ { name: "data", type: "string", description: "CreateMoneyAccountInput JSON." },
49090
+ ...ACCOUNT_KIND_FLAGS
49091
+ ],
48696
49092
  returns: "MoneyAccountView",
48697
- 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).`,
48698
49094
  run: async ({ flags, requireClient }) => {
48699
49095
  const body = parseInput(createMoneyAccountInputSchema, {
48700
49096
  id: ulid3(),
48701
- ...readDataFlag(flags)
49097
+ ...readDataFlag(flags),
49098
+ ...accountKindFields(flags)
48702
49099
  });
48703
49100
  return {
48704
49101
  data: await requireClient().post(pathOf(moneyOperations.createAccount), body),
@@ -48713,6 +49110,7 @@ var commands31 = [
48713
49110
  args: [{ name: "id", required: true, description: "Account ULID." }],
48714
49111
  flags: [
48715
49112
  { name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." },
49113
+ ...ACCOUNT_KIND_FLAGS,
48716
49114
  {
48717
49115
  name: "balance",
48718
49116
  type: "string",
@@ -48725,8 +49123,14 @@ var commands31 = [
48725
49123
  }
48726
49124
  ],
48727
49125
  returns: "MoneyAccountView",
48728
- notes: ". `--balance 1234.56 --balance-at 2026-10-27` anchors an account whose bank export has no running balance (ING's): net worth counts from it, back and forward through the ledger. A negative one rides as `--balance=-20`. A day ahead is refused, and so is an account a bank link keeps. `--data` takes the rest (title, kind, iban, includeInNetWorth, \u2026).",
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.",
48729
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
+ ]);
48730
49134
  const id = requireArg(args, 0, "account id");
48731
49135
  const balance = flagString(flags, "balance");
48732
49136
  const balanceAt = flagString(flags, "balance-at");
@@ -48735,6 +49139,7 @@ var commands31 = [
48735
49139
  }
48736
49140
  const body = parseInput(updateMoneyAccountInputSchema, {
48737
49141
  ...readDataFlag(flags),
49142
+ ...accountKindFields(flags),
48738
49143
  ...balance !== undefined ? { balance, balanceAt: balanceAt ?? isoDay(io.now()) } : {}
48739
49144
  });
48740
49145
  return {
@@ -49531,7 +49936,7 @@ var commands31 = [
49531
49936
  }
49532
49937
  ],
49533
49938
  returns: "MoneyNetWorth",
49534
- notes: ". total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed. `complete: false` when an account counts but its balance isn't known (a bank export with no running balance): it's listed under `unknown`, left out of the total, never counted as 0; `do money account-update <id> --balance \u2026 --balance-at \u2026` states it.",
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.",
49535
49940
  run: async ({ flags, requireClient }) => ({
49536
49941
  data: await requireClient().get(pathOf(moneyOperations.netWorthRead), {
49537
49942
  query: { scope: flagString(flags, "scope"), months: flagString(flags, "months") }
@@ -50116,6 +50521,139 @@ var commands33 = [
50116
50521
  // src/commands/people.ts
50117
50522
  import { readFileSync as readFileSync9 } from "fs";
50118
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
+ }
50119
50657
  var commands34 = [
50120
50658
  {
50121
50659
  spec: {
@@ -50471,6 +51009,34 @@ var commands34 = [
50471
51009
  };
50472
51010
  }
50473
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
+ },
50474
51040
  {
50475
51041
  spec: {
50476
51042
  path: ["people", "tree", "list"],
@@ -50555,6 +51121,223 @@ var commands34 = [
50555
51121
  render: renderRecord
50556
51122
  };
50557
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
+ })
50558
51341
  }
50559
51342
  ];
50560
51343
 
@@ -53398,38 +54181,6 @@ var FEATURE_COMMANDS = [
53398
54181
  // src/registry.ts
53399
54182
  var COMMANDS = FEATURE_COMMANDS;
53400
54183
  var COMMAND_SPECS = COMMANDS.map((c) => c.spec);
53401
- var GLOBAL_FLAGS = [
53402
- { name: "json", type: "boolean", description: "Emit JSON (automatic when stdout is piped)." },
53403
- { name: "quiet", type: "boolean", description: "Suppress human output (errors still report)." },
53404
- { name: "verbose", type: "boolean", description: "Extra diagnostics on stderr." },
53405
- {
53406
- name: "api-key",
53407
- type: "string",
53408
- description: "API key; overrides DO_API_KEY and the config file."
53409
- },
53410
- {
53411
- name: "url",
53412
- type: "string",
53413
- description: "API origin; overrides DO_API_URL and the config file."
53414
- },
53415
- { name: "help", type: "boolean", description: "Show help for the command." },
53416
- { name: "version", type: "boolean", description: "Print the CLI version." },
53417
- {
53418
- name: "data",
53419
- type: "string",
53420
- description: `JSON object body for create/update (e.g. --data '{"title":"Buy milk"}').`
53421
- },
53422
- {
53423
- name: "allow-destructive",
53424
- type: "boolean",
53425
- description: "Permit a destructive verb (delete/revoke/remove-member). Off by default \u2014 a guardrail for autonomous agents."
53426
- },
53427
- {
53428
- name: "dry-run",
53429
- type: "boolean",
53430
- description: "Preview a create/update/delete/archive (prints before/after) without writing \u2014 for agent-driven cleanup."
53431
- }
53432
- ];
53433
54184
  function matchCommand(positionals) {
53434
54185
  for (const len of [4, 3, 2, 1]) {
53435
54186
  const candidate = positionals.slice(0, len);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.334.0",
3
+ "version": "1.336.0",
4
4
  "private": false,
5
5
  "description": "do — the agent-drivable CLI over the public /api/v1 surface",
6
6
  "homepage": "https://github.com/robbeverhelst/do/tree/main/apps/cli",