@robbeverhelst/do 1.314.1 → 1.316.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 +1328 -189
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -22627,6 +22627,31 @@ var houseUpkeepYearSchema = object({
22627
22627
  });
22628
22628
  var houseUpkeepYearQuerySchema = object({ year: exports_coerce.number().int().min(2000).max(2100).optional() }).strict();
22629
22629
 
22630
+ // ../../packages/schema/src/contracts/health-export.ts
22631
+ var HEALTH_EXPORT_RESOURCE_TYPES = [
22632
+ "Patient",
22633
+ "AllergyIntolerance",
22634
+ "Condition",
22635
+ "Observation",
22636
+ "Encounter",
22637
+ "MedicationStatement",
22638
+ "MedicationAdministration",
22639
+ "DiagnosticReport",
22640
+ "DocumentReference"
22641
+ ];
22642
+ var healthExportQuerySchema = object({
22643
+ files: _enum2(["true", "false"]).default("true").transform((v) => v === "true")
22644
+ });
22645
+ var fhirResourceSchema = object({ resourceType: _enum2(HEALTH_EXPORT_RESOURCE_TYPES), id: string2().min(1) }).passthrough();
22646
+ var healthExportBundleSchema = object({
22647
+ resourceType: literal("Bundle"),
22648
+ id: string2().min(1),
22649
+ type: literal("collection"),
22650
+ timestamp: string2().datetime({ offset: true }),
22651
+ meta: object({ tag: array(object({ system: string2(), code: string2() })) }),
22652
+ entry: array(object({ fullUrl: string2().min(1), resource: fhirResourceSchema }))
22653
+ });
22654
+
22630
22655
  // ../../packages/schema/src/receipt-kinds.ts
22631
22656
  var MONEY_RECEIPT_NODE_TYPE = "money_transaction";
22632
22657
  var RECEIPT_KINDS = {
@@ -26941,6 +26966,120 @@ var documentsQuerySchema = object({
26941
26966
  providerId: ulidSchema.optional()
26942
26967
  });
26943
26968
 
26969
+ // ../../packages/schema/src/contracts/document-ask.ts
26970
+ var documentPassageSourceSchema = _enum2(["field", "text"]);
26971
+ var documentPassageSchema = object({
26972
+ documentId: ulidSchema,
26973
+ documentTitle: string2(),
26974
+ documentKind: documentKindSchema,
26975
+ source: documentPassageSourceSchema,
26976
+ fileId: ulidSchema.nullable(),
26977
+ fileName: string2().nullable(),
26978
+ page: number2().int().positive().nullable(),
26979
+ text: string2(),
26980
+ matched: array(string2())
26981
+ });
26982
+ var documentWithoutTextSchema = object({
26983
+ documentId: ulidSchema,
26984
+ documentTitle: string2()
26985
+ });
26986
+ var documentAskInputSchema = object({
26987
+ question: string2().trim().min(2).max(500),
26988
+ documentId: ulidSchema.optional(),
26989
+ limit: exports_coerce.number().int().min(1).max(20).optional()
26990
+ }).strict();
26991
+ var documentAnswerSchema = object({
26992
+ question: string2(),
26993
+ found: boolean3(),
26994
+ passages: array(documentPassageSchema),
26995
+ pages: array(object({ documentId: ulidSchema, pages: array(number2().int()) })),
26996
+ withoutText: array(documentWithoutTextSchema),
26997
+ note: string2()
26998
+ });
26999
+ var documentSearchQuerySchema = object({
27000
+ q: string2().trim().min(2).max(200),
27001
+ documentId: ulidSchema.optional(),
27002
+ limit: exports_coerce.number().int().min(1).max(50).optional()
27003
+ }).strict();
27004
+ var documentSearchResultSchema = object({
27005
+ q: string2(),
27006
+ hits: array(documentPassageSchema)
27007
+ });
27008
+
27009
+ // ../../packages/schema/src/contracts/document-payments.ts
27010
+ var documentPaymentSchema = object({
27011
+ transactionId: ulidSchema,
27012
+ accountId: ulidSchema,
27013
+ accountTitle: string2(),
27014
+ bookedOn: string2(),
27015
+ amount: string2(),
27016
+ currency: string2(),
27017
+ counterpartyName: string2().nullable()
27018
+ });
27019
+ var unmatchedPaperSchema = object({
27020
+ id: ulidSchema,
27021
+ title: string2(),
27022
+ kind: documentKindSchema,
27023
+ amountDue: number2(),
27024
+ payBy: string2().nullable(),
27025
+ providerTitle: string2().nullable(),
27026
+ candidates: number2().int().nonnegative()
27027
+ });
27028
+ var paperMatchStatusSchema = _enum2([
27029
+ "matched",
27030
+ "already",
27031
+ "ambiguous",
27032
+ "none",
27033
+ "not_payable"
27034
+ ]);
27035
+ var paperMatchResultSchema = object({
27036
+ documentId: ulidSchema,
27037
+ status: paperMatchStatusSchema,
27038
+ transactionId: ulidSchema.nullable(),
27039
+ by: _enum2(["ref", "iban"]).nullable(),
27040
+ candidates: array(ulidSchema)
27041
+ });
27042
+ var linkPaperPaymentInputSchema = object({ transactionId: ulidSchema }).strict();
27043
+
27044
+ // ../../packages/schema/src/contracts/doccle.ts
27045
+ var DOCCLE_FAILURE_KINDS = ["credentials", "captcha", "changed", "unavailable"];
27046
+ var doccleFailureKindSchema = _enum2(DOCCLE_FAILURE_KINDS);
27047
+ var doccleFailureSchema = object({
27048
+ kind: doccleFailureKindSchema,
27049
+ title: string2(),
27050
+ hint: string2(),
27051
+ since: exports_coerce.date()
27052
+ });
27053
+ var doccleStatusSchema = object({
27054
+ enabled: boolean3(),
27055
+ connected: boolean3(),
27056
+ archiveAfterImport: boolean3(),
27057
+ lastRunAt: exports_coerce.date().nullable(),
27058
+ lastOkAt: exports_coerce.date().nullable(),
27059
+ failure: doccleFailureSchema.nullable(),
27060
+ lastRun: object({
27061
+ filed: number2().int().nonnegative(),
27062
+ alreadyFiled: number2().int().nonnegative(),
27063
+ secured: number2().int().nonnegative()
27064
+ }),
27065
+ imported: number2().int().nonnegative()
27066
+ });
27067
+ var doccleLoginInputSchema = object({
27068
+ username: string2().trim().min(1).max(200),
27069
+ password: string2().min(1).max(200)
27070
+ }).strict();
27071
+ var doccleSettingsInputSchema = object({ archiveAfterImport: boolean3() }).strict();
27072
+ var DOCCLE_RUN_OUTCOMES = ["ok", "failed", "not_connected", "off", "busy"];
27073
+ var doccleSyncResultSchema = object({
27074
+ outcome: _enum2(DOCCLE_RUN_OUTCOMES),
27075
+ filed: number2().int().nonnegative(),
27076
+ alreadyFiled: number2().int().nonnegative(),
27077
+ secured: number2().int().nonnegative(),
27078
+ archived: number2().int().nonnegative(),
27079
+ documentIds: array(string2()),
27080
+ failure: doccleFailureSchema.nullable()
27081
+ });
27082
+
26944
27083
  // ../../packages/schema/src/contracts/incidents.ts
26945
27084
  var incidentKindSchema = _enum2(incidentKind.enumValues);
26946
27085
  var incidentStatusSchema = _enum2(incidentStatus.enumValues);
@@ -31599,6 +31738,277 @@ var dayOperations = defineOperations({
31599
31738
  }
31600
31739
  });
31601
31740
 
31741
+ // ../../packages/schema/src/operations/documents.ts
31742
+ var DOCCLE_STATUS = "The import's status.";
31743
+ var PAPER_ID = { id: ulidSchema };
31744
+ var documentOperations = defineOperations({
31745
+ ...crudOperations({
31746
+ segment: "documents",
31747
+ tag: "Documents",
31748
+ noun: "document",
31749
+ read: "documents:read",
31750
+ write: "documents:write",
31751
+ create: createDocumentInputSchema,
31752
+ update: updateDocumentInputSchema,
31753
+ listQuery: [
31754
+ {
31755
+ name: "kind",
31756
+ description: "Only this kind (`insurance`, `contract`, `subscription`, `id_paper`, `house_cert`, `warranty`, \u2026)."
31757
+ },
31758
+ {
31759
+ name: "status",
31760
+ description: "Only this status (`active`, `ended`, `superseded`, `draft`)."
31761
+ },
31762
+ { name: "providerId", description: "Only the documents issued by this Contact." }
31763
+ ],
31764
+ listQuerySchema: documentsQuerySchema
31765
+ }),
31766
+ unmatched: {
31767
+ method: "get",
31768
+ path: "/documents/unmatched",
31769
+ scopes: ["documents:read", "finance:read"],
31770
+ tag: "Documents",
31771
+ summary: "Papers waiting for their payment",
31772
+ description: "The papers that ask for money (`fields.amountDue`, with an IBAN or a structured reference to match on) and have no payment linked, oldest due first: `{ id, title, kind, amountDue, payBy, providerTitle, candidates }`. `candidates` counts the payments that fit equally well. A payment on an account the key can't see doesn't count.",
31773
+ status: 200,
31774
+ response: { description: "The unmatched papers." }
31775
+ },
31776
+ matchAll: {
31777
+ method: "post",
31778
+ path: "/documents/match",
31779
+ scopes: ["documents:read", "finance:write"],
31780
+ tag: "Documents",
31781
+ summary: "Match every paper to its payment",
31782
+ description: "Match every paper still waiting for its payment. A payment is money out of exactly the amount due, booked between a week before the paper and 45 days after its due day, carrying its structured reference or (without one) going to its IBAN; it's linked only when exactly one fits, as the transaction's `evidence`. Returns one `{ documentId, status, transactionId, by, candidates }` per paper; `status` is `matched`, `already`, `ambiguous`, `none` or `not_payable`. Papers are also matched when filed or changed by a key with `finance:write`, and after a bank import.",
31783
+ status: 200,
31784
+ response: { description: "One result per paper." }
31785
+ },
31786
+ filed: {
31787
+ method: "get",
31788
+ path: "/documents/filed",
31789
+ scopes: ["documents:read"],
31790
+ tag: "Documents",
31791
+ summary: "Check a filed source",
31792
+ description: "Whether a mail (`gmail:<id>`) or an import was already filed: `{ filed, documentId }`. The Gmail watch and backfill dedupe with it. 403 until `DOCUMENTS_IMPORT_ENABLED=true`.",
31793
+ query: [{ name: "source", description: "`gmail:<id>` or `doccle_zip`." }],
31794
+ querySchema: object({ source: fileSourceSchema }),
31795
+ status: 200,
31796
+ response: { description: "`{ filed, documentId }`." },
31797
+ errors: { 403: "Imports are off." },
31798
+ operationId: "document_filed"
31799
+ },
31800
+ doccleStatus: {
31801
+ method: "get",
31802
+ path: "/documents/doccle",
31803
+ scopes: ["documents:read"],
31804
+ tag: "Documents",
31805
+ summary: "Your Doccle import",
31806
+ description: "The key's member's Doccle import (ADR 0033), which files their Doccle mailbox into Papers every night: `{ enabled, connected, archiveAfterImport, lastRunAt, lastOkAt, failure, lastRun: { filed, alreadyFiled, secured }, imported }`. `failure` (`{ kind, title, hint, since }`, kind `credentials` \xB7 `captcha` \xB7 `changed` \xB7 `unavailable`) says why it is stuck, while it is. The login itself is never returned.",
31807
+ status: 200,
31808
+ response: { description: DOCCLE_STATUS }
31809
+ },
31810
+ doccleSettings: {
31811
+ method: "patch",
31812
+ path: "/documents/doccle",
31813
+ scopes: ["documents:write"],
31814
+ tag: "Documents",
31815
+ summary: "Change your Doccle import's setting",
31816
+ description: "`{ archiveAfterImport }`: archive each document in Doccle once it is filed as a paper (off by default). Returns the status.",
31817
+ body: doccleSettingsInputSchema,
31818
+ status: 200,
31819
+ response: { description: DOCCLE_STATUS },
31820
+ errors: { 400: "Not a boolean." }
31821
+ },
31822
+ doccleConnect: {
31823
+ method: "put",
31824
+ path: "/documents/doccle/login",
31825
+ scopes: ["connections:write"],
31826
+ tag: "Documents",
31827
+ summary: "Connect your Doccle login",
31828
+ description: "`{ username, password }`: do checks the login with Doccle, then keeps it sealed in the connection-secret store (ADR 0032) for the nightly import. It is never returned or logged. Returns the status.",
31829
+ afterScopes: "`*` and `private:*` never grant `connections:write`: a key holds it by name, and a guest's key never does.",
31830
+ body: doccleLoginInputSchema,
31831
+ status: 200,
31832
+ response: { description: "The import's status, connected." },
31833
+ errors: {
31834
+ 403: "The key doesn't hold `connections:write`, or is a guest's.",
31835
+ 422: "Doccle refused the login or asked for a captcha; nothing is kept.",
31836
+ 503: "Doccle couldn't be reached."
31837
+ },
31838
+ mcp: false
31839
+ },
31840
+ doccleDisconnect: {
31841
+ method: "delete",
31842
+ path: "/documents/doccle/login",
31843
+ scopes: ["documents:write"],
31844
+ tag: "Documents",
31845
+ summary: "Forget your Doccle login",
31846
+ description: "The nightly import stops; the papers it filed stay. Returns the status.",
31847
+ status: 200,
31848
+ response: { description: DOCCLE_STATUS }
31849
+ },
31850
+ doccleSync: {
31851
+ method: "post",
31852
+ path: "/documents/doccle/sync",
31853
+ scopes: ["documents:write"],
31854
+ tag: "Documents",
31855
+ summary: "Run your Doccle import now",
31856
+ description: "Each Doccle document not seen before becomes a paper: its sender as the provider (an organisation Contact), its date, its PDF (`source: doccle`), an invoice's amount, IBAN and payment reference, then matched to its payment. Nothing is filed twice: a document is skipped on Doccle's id, and a PDF already in Papers (a Doccle ZIP export, or by hand) on its SHA-256. `{ outcome, filed, alreadyFiled, secured, archived, documentIds, failure }`; `outcome` is `ok`, `failed` (the member is told once), `not_connected`, `off` (`DOCUMENTS_IMPORT_ENABLED` isn't on) or `busy`.",
31857
+ status: 200,
31858
+ response: { description: "What the run did." }
31859
+ },
31860
+ ask: {
31861
+ method: "post",
31862
+ path: "/documents/ask",
31863
+ scopes: ["documents:read"],
31864
+ tag: "Documents",
31865
+ summary: "Ask the papers (doc_ask)",
31866
+ description: "`doc_ask`: `{ question, documentId?, limit? }` asks one paper, or every paper the key can read, and returns the passages that answer, best first, each with its paper, file and page: `{ question, found, passages: [{ documentId, documentTitle, documentKind, source, fileId, fileName, page, text, matched }], pages: [{ documentId, pages }], withoutText, note }`. The words come from the papers' key fields (`source: field`, the page an extraction read it on) and their files' `textExtract` (`source: text`, a page per form feed). do writes no prose: word the answer from the passages, quote the pages, and say it is not legal advice (`note`). `withoutText` names the papers asked (or, asking them all, those the question names) that hold no file text, so you can offer to fetch the conditions. An Only me paper answers only its owner's key with `documents:private`.",
31867
+ body: documentAskInputSchema,
31868
+ status: 200,
31869
+ response: { description: "The passages that answer." },
31870
+ errors: { 404: "No such paper." },
31871
+ reads: true
31872
+ },
31873
+ search: {
31874
+ method: "get",
31875
+ path: "/documents/search",
31876
+ scopes: ["documents:read"],
31877
+ tag: "Documents",
31878
+ summary: "Search the papers' words",
31879
+ description: "Search the papers' key fields and file text: `{ q, hits }`, a passage per page or field that holds the words, best first, shaped as `doc_ask`'s passages.",
31880
+ query: [
31881
+ { name: "q", description: "The words to find (two letters or more)." },
31882
+ { name: "documentId", description: "Only this paper." },
31883
+ { name: "limit", description: "How many hits at most (default 20, up to 50)." }
31884
+ ],
31885
+ querySchema: documentSearchQuerySchema,
31886
+ status: 200,
31887
+ response: { description: "The hits." }
31888
+ },
31889
+ matchOne: {
31890
+ method: "post",
31891
+ path: "/documents/{id}/match",
31892
+ scopes: ["documents:read", "finance:write"],
31893
+ tag: "Documents",
31894
+ summary: "Match a paper to its payment",
31895
+ description: "Match this paper to its payment, if exactly one fits: `{ documentId, status, transactionId, by, candidates }`.",
31896
+ pathParamSchemas: PAPER_ID,
31897
+ status: 200,
31898
+ response: { description: "What matching did." },
31899
+ errors: { 404: "No such paper." }
31900
+ },
31901
+ payments: {
31902
+ method: "get",
31903
+ path: "/documents/{id}/payments",
31904
+ scopes: ["documents:read", "finance:read"],
31905
+ tag: "Documents",
31906
+ summary: "A paper's payments",
31907
+ description: "The payments linked to this paper that the key may see: `{ transactionId, accountId, accountTitle, bookedOn, amount, currency, counterpartyName }`.",
31908
+ pathParamSchemas: PAPER_ID,
31909
+ status: 200,
31910
+ response: { description: "The payments." }
31911
+ },
31912
+ linkPayment: {
31913
+ method: "put",
31914
+ path: "/documents/{id}/payments",
31915
+ scopes: ["documents:read", "finance:write"],
31916
+ tag: "Documents",
31917
+ summary: "Link a payment to a paper",
31918
+ description: "Link a payment to this paper by hand (`{ transactionId }`), e.g. one of the fits when matching was ambiguous. Returns the paper's payments.",
31919
+ pathParamSchemas: PAPER_ID,
31920
+ body: linkPaperPaymentInputSchema,
31921
+ status: 200,
31922
+ response: { description: "The paper's payments." },
31923
+ errors: { 404: "No such paper or transaction." }
31924
+ },
31925
+ unlinkPayment: {
31926
+ method: "delete",
31927
+ path: "/documents/{id}/payments/{transactionId}",
31928
+ scopes: ["documents:read", "finance:write"],
31929
+ tag: "Documents",
31930
+ summary: "Take a payment off a paper",
31931
+ description: "Unlink this payment from the paper.",
31932
+ pathParams: { transactionId: "The Money transaction's ULID." },
31933
+ pathParamSchemas: { id: ulidSchema, transactionId: ulidSchema },
31934
+ status: 204,
31935
+ response: { description: "Unlinked." },
31936
+ errors: { 404: "That payment isn't linked to this paper." }
31937
+ }
31938
+ });
31939
+
31940
+ // ../../packages/schema/src/operations/family-tree.ts
31941
+ var familyTreeOperations = defineOperations({
31942
+ get: {
31943
+ method: "get",
31944
+ path: "/family-tree",
31945
+ scopes: ["people:family-tree"],
31946
+ tag: "People",
31947
+ summary: "The family tree",
31948
+ description: "The member's tree: the import facts, the branch two generations up from them once they've said who they are (`side` = `father` or `mother`), and what it noticed. `{ tree: null }` before an import.",
31949
+ afterScopes: "A sensitive scope (ADR 0028): never part of `*`, and only the tree of the member who minted the key.",
31950
+ query: [
31951
+ { name: "side", description: "Which grandparents the branch climbs to: father or mother." }
31952
+ ],
31953
+ querySchema: familyTreeQuerySchema,
31954
+ status: 200,
31955
+ response: { description: "`{ tree }`." },
31956
+ errors: { 400: true, 403: "The key doesn't hold `people:family-tree`, or is no member's." }
31957
+ },
31958
+ people: {
31959
+ method: "get",
31960
+ path: "/family-tree/people",
31961
+ scopes: ["people:family-tree"],
31962
+ tag: "People",
31963
+ summary: "Everyone in the tree",
31964
+ description: "By name, with `q` (a part of the name), `filter` (all \xB7 living \xB7 linked) and `limit` (1\u2013500, default 100): how to find a person's `ref`.",
31965
+ query: [
31966
+ { name: "q", description: "A part of the name." },
31967
+ { name: "filter", description: "all, living or linked." },
31968
+ { name: "limit", description: "At most this many (1\u2013500, default 100)." }
31969
+ ],
31970
+ querySchema: familyTreePeopleQuerySchema,
31971
+ status: 200,
31972
+ response: { description: "`{ people, total }`." },
31973
+ errors: { 404: true }
31974
+ },
31975
+ me: {
31976
+ method: "put",
31977
+ path: "/family-tree/me",
31978
+ scopes: ["people:family-tree"],
31979
+ tag: "People",
31980
+ summary: "This is me",
31981
+ description: "`ref`: the member in the tree. It becomes the root of the branch, and a living person is linked to the member's own Contact (the one that is them).",
31982
+ body: familyTreeMeInputSchema,
31983
+ status: 200,
31984
+ response: { description: "The tree." },
31985
+ errors: { 400: true, 404: true }
31986
+ },
31987
+ matches: {
31988
+ method: "get",
31989
+ path: "/family-tree/matches",
31990
+ scopes: ["people:family-tree"],
31991
+ tag: "People",
31992
+ summary: "What the tree noticed",
31993
+ description: "Each with the `id` a decision takes: `suggest` (a living relative who looks like an unlinked Contact), `birthday_differs` (a linked relative's birthday falls on another day than their Contact's), `not_in_tree` (a linked relative's partner or kid who isn't in the tree). What was said no to isn't listed.",
31994
+ status: 200,
31995
+ response: { description: "`{ matches }`." },
31996
+ errors: { 404: true }
31997
+ },
31998
+ decide: {
31999
+ method: "post",
32000
+ path: "/family-tree/matches",
32001
+ scopes: ["people:family-tree"],
32002
+ tag: "People",
32003
+ summary: "Yes or no to a match",
32004
+ description: "`{ id, decision: accept | reject }`. Yes links a suggested relative to the Contact, or takes the tree's birthday onto it; someone not in the tree has nothing to take, so only no (a 400). No hides the match for good. Returns `{ id, did, matches }` (`did`: linked \xB7 birthday_taken \xB7 dismissed).",
32005
+ body: familyTreeMatchDecisionInputSchema,
32006
+ status: 200,
32007
+ response: { description: "`{ id, did, matches }`." },
32008
+ errors: { 400: true, 404: true }
32009
+ }
32010
+ });
32011
+
31602
32012
  // ../../packages/schema/src/operations/files.ts
31603
32013
  var fileOperations = defineOperations({
31604
32014
  list: {
@@ -32362,122 +32772,796 @@ var habitOperations = defineOperations({
32362
32772
  response: { description: "The check-ins." },
32363
32773
  errors: { 404: true }
32364
32774
  },
32365
- checkIn: {
32775
+ checkIn: {
32776
+ method: "post",
32777
+ path: "/habits/{id}/check-ins",
32778
+ scopes: ["habits:write"],
32779
+ tag: "Habits",
32780
+ summary: "Check in",
32781
+ description: "Record a check-in, with an optional short `note`; fires `habit.updated` with the refreshed streak. A habit counted from an ingest (`source` other than `manual`) answers 409.",
32782
+ body: createHabitEntryInputSchema,
32783
+ status: 201,
32784
+ response: { description: "The refreshed habit." },
32785
+ errors: {
32786
+ 400: true,
32787
+ 404: true
32788
+ },
32789
+ webhook: "habit.updated",
32790
+ operationId: "check_in_habit"
32791
+ },
32792
+ skip: {
32793
+ method: "post",
32794
+ path: "/habits/{id}/skip",
32795
+ scopes: ["habits:write"],
32796
+ tag: "Habits",
32797
+ summary: "Skip a habit",
32798
+ description: '"Skip gym today": a neutral rest day (a `skip` entry, value 0), never a miss. The streak treats the period as neutral and spends no grace. `date` (YYYY-MM-DD) defaults to today; an optional `note` says why ("Dentist"). Skipping a skipped day changes nothing but its note. Undo by deleting the skip entry like a check-in. Returns the refreshed habit; fires `habit.updated`.',
32799
+ body: habitSkipInputSchema,
32800
+ status: 201,
32801
+ response: { description: "The refreshed habit." },
32802
+ errors: {
32803
+ 400: true,
32804
+ 404: true
32805
+ },
32806
+ webhook: "habit.updated"
32807
+ },
32808
+ pause: {
32809
+ method: "post",
32810
+ path: "/habits/{id}/pause",
32811
+ scopes: ["habits:write"],
32812
+ tag: "Habits",
32813
+ summary: "Pause a habit",
32814
+ description: '"Pause gym till the 26th": the habit rests until `until` (the day it\'s back, after today): off Now, no push, streak and plant wait. Pausing a paused habit moves its end. Returns the refreshed habit with `pause`; fires `habit.updated`.',
32815
+ body: habitPauseInputSchema,
32816
+ status: 201,
32817
+ response: { description: "The refreshed habit." },
32818
+ errors: {
32819
+ 400: true,
32820
+ 404: true
32821
+ },
32822
+ webhook: "habit.updated"
32823
+ },
32824
+ resume: {
32825
+ method: "post",
32826
+ path: "/habits/{id}/resume",
32827
+ scopes: ["habits:write"],
32828
+ tag: "Habits",
32829
+ summary: "Resume a habit",
32830
+ description: "End a habit's pause: it's back today. The rest so far stays neutral. Returns the refreshed habit; fires `habit.updated`.",
32831
+ status: 201,
32832
+ response: { description: "The refreshed habit." },
32833
+ errors: { 404: true },
32834
+ webhook: "habit.updated"
32835
+ },
32836
+ retire: {
32837
+ method: "post",
32838
+ path: "/habits/{id}/retire",
32839
+ scopes: ["habits:write"],
32840
+ tag: "Habits",
32841
+ summary: "Retire a habit",
32842
+ description: '"I\'m done with the cold showers habit": the habit moves to the Grown shelf with its plant in bloom and keeps its history. It leaves Now and Today, sends no push, takes no check-in by title and is never due; Progress keeps its past. Returns the refreshed habit with `retiredAt`; undo with `/unretire`. Fires `habit.updated`.',
32843
+ status: 201,
32844
+ response: { description: "The refreshed habit." },
32845
+ errors: { 404: true },
32846
+ webhook: "habit.updated"
32847
+ },
32848
+ unretire: {
32849
+ method: "post",
32850
+ path: "/habits/{id}/unretire",
32851
+ scopes: ["habits:write"],
32852
+ tag: "Habits",
32853
+ summary: "Bring a habit back",
32854
+ description: "Bring a grown (retired) habit back: it's growing again from today, its history intact. Returns the refreshed habit; fires `habit.updated`.",
32855
+ status: 201,
32856
+ response: { description: "The refreshed habit." },
32857
+ errors: { 404: true },
32858
+ webhook: "habit.updated"
32859
+ },
32860
+ updateCheckIn: {
32861
+ method: "patch",
32862
+ path: "/habits/{id}/check-ins/{entryId}",
32863
+ pathParams: { entryId: "The check-in entry's ULID." },
32864
+ scopes: ["habits:write"],
32865
+ tag: "Habits",
32866
+ summary: "Note a check-in",
32867
+ description: "Edit a check-in afterwards: set, replace or clear (`null` or blank) its short `note` (at most 280 characters). The amount and time stay; undo a wrong check-in instead. Returns the check-in; fires `habit.updated`.",
32868
+ body: updateHabitEntryInputSchema,
32869
+ status: 200,
32870
+ response: { description: "The check-in, with its note." },
32871
+ errors: {
32872
+ 400: true,
32873
+ 404: true
32874
+ },
32875
+ webhook: "habit.updated",
32876
+ operationId: "note_habit_check_in"
32877
+ },
32878
+ removeCheckIn: {
32879
+ method: "delete",
32880
+ path: "/habits/{id}/check-ins/{entryId}",
32881
+ pathParams: { entryId: "The check-in entry's ULID." },
32882
+ scopes: ["habits:write"],
32883
+ tag: "Habits",
32884
+ summary: "Undo a check-in",
32885
+ description: "Delete a check-in; fires `habit.updated`.",
32886
+ status: 200,
32887
+ response: { description: "The refreshed habit." },
32888
+ errors: { 404: true },
32889
+ webhook: "habit.updated",
32890
+ operationId: "undo_habit_check_in"
32891
+ }
32892
+ });
32893
+
32894
+ // ../../packages/schema/src/operations/health.ts
32895
+ var healthOperations = defineOperations({
32896
+ export: {
32897
+ method: "get",
32898
+ path: "/health/export",
32899
+ scopes: ["health:export"],
32900
+ tag: "Health",
32901
+ summary: "Export every health record as FHIR",
32902
+ description: "Every health record the member holds, as one FHIR R4 `collection` Bundle: the member (Patient), allergies, Conditions and Episodes (Condition), symptom entries and lab values (Observation; a correction is `corrected`, what it replaced `entered-in-error`), visits (Encounter), medicines and doses (MedicationStatement, MedicationAdministration), lab draws (DiagnosticReport) and the original documents (DocumentReference, bytes inline as base64 unless `files=false`). Works with Health turned off. Logged in the member's Who looked.",
32903
+ query: [
32904
+ { name: "files", description: "`false` leaves the documents' bytes out; default `true`." }
32905
+ ],
32906
+ querySchema: healthExportQuerySchema,
32907
+ status: 200,
32908
+ response: { description: "The FHIR Bundle." },
32909
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
32910
+ },
32911
+ getConsent: {
32912
+ method: "get",
32913
+ path: "/health/consent",
32914
+ scopes: ["health:read"],
32915
+ tag: "Health",
32916
+ summary: "Get the health consent",
32917
+ description: "Whether the member has turned Health on: `{ granted, version, grantedAt, currentVersion, renewalNeeded, aiProvider }`. `granted` means on at today's consent text (`currentVersion`); `renewalNeeded` means the member agreed to an older text and is asked to agree again, so writes wait until they do. `aiProvider` is the AI provider this deployment names for what the agent reads (null when none). Only the member turns it on or off, in do; a key never can.",
32918
+ status: 200,
32919
+ response: { description: "The consent state." },
32920
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
32921
+ },
32922
+ getProfile: {
32923
+ method: "get",
32924
+ path: "/health/profile",
32925
+ scopes: ["health:read"],
32926
+ tag: "Health",
32927
+ summary: "Get the health profile",
32928
+ description: "The member's blood group, allergies, height, emergency contact and GP (Contacts); empty until set.",
32929
+ status: 200,
32930
+ response: { description: "The profile." },
32931
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
32932
+ },
32933
+ updateProfile: {
32934
+ method: "patch",
32935
+ path: "/health/profile",
32936
+ scopes: ["health:write"],
32937
+ tag: "Health",
32938
+ summary: "Update the health profile",
32939
+ description: "Set profile fields; an absent key keeps a field, `null` clears it.",
32940
+ body: updateHealthProfileInputSchema,
32941
+ status: 200,
32942
+ response: { description: "The profile." },
32943
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
32944
+ },
32945
+ log: {
32946
+ method: "post",
32947
+ path: "/health/log",
32948
+ scopes: ["health:write"],
32949
+ tag: "Health",
32950
+ summary: "Log a health message",
32951
+ description: "One chat turn about health in one call: name the Episode (`episodeId`) or describe it (`episode`: the member's open Episode in the same region or with the same title, active in the last 14 days, else a new one), then a `symptom`, a `dose` (a Medication by `medicationId`, or by name in `medication`, added as needed if new), or both. Returns a receipt: `{ episode, items[{ kind, id, line, guessed, undo }], entry, dose }`; each item's `undo` goes to `POST /api/v1/health/undo`. With `thread` (the member's home thread by slug, the Space's agent key with `agents:converse`), the receipt card is posted there too (\"Added to **Sprained ankle** (day 5).\" with what was logged, Undo and Edit) and `postId` names it.",
32952
+ body: logHealthInputSchema,
32953
+ status: 201,
32954
+ response: { description: "The receipt." },
32955
+ errors: {
32956
+ 400: true,
32957
+ 403: "Health is off for the member, or the key acts for no member.",
32958
+ 404: true
32959
+ }
32960
+ },
32961
+ correct: {
32962
+ method: "post",
32963
+ path: "/health/correct",
32964
+ scopes: ["health:write"],
32965
+ tag: "Health",
32966
+ summary: "Correct a health message",
32967
+ description: "Correct a symptom entry by its id (`{ entryId, change }`): a new entry supersedes it, the history stays. Returns a receipt. 409 when the entry was corrected already.",
32968
+ body: correctHealthInputSchema,
32969
+ status: 201,
32970
+ response: { description: "The receipt." },
32971
+ errors: { 400: true, 404: true, 409: true }
32972
+ },
32973
+ undo: {
32974
+ method: "post",
32975
+ path: "/health/undo",
32976
+ scopes: ["health:write"],
32977
+ tag: "Health",
32978
+ summary: "Undo a health receipt",
32979
+ description: "Take back what a receipt wrote: `{ kind: entry | dose | episode, id }`. A POST, so it doesn't wait on a confirmation the way a delete does.",
32980
+ body: healthUndoInputSchema,
32981
+ status: 200,
32982
+ response: { description: "`{ undone }`." },
32983
+ errors: { 404: true, 409: true }
32984
+ },
32985
+ checkIn: {
32986
+ method: "post",
32987
+ path: "/health/episodes/{id}/check-in",
32988
+ scopes: ["health:write"],
32989
+ tag: "Health",
32990
+ summary: "Check in on an Episode",
32991
+ description: "Better / Same / Worse (`{ trend }`): today's trend against yesterday, as an entry in the member's words. One a day in the member's zone; a second one the same day corrects the first. Returns a receipt whose `undo` goes to `POST /api/v1/health/undo`.",
32992
+ body: healthCheckInInputSchema,
32993
+ status: 201,
32994
+ response: { description: "The receipt." },
32995
+ errors: {
32996
+ 400: true,
32997
+ 403: "Health is off for the member, or the key acts for no member.",
32998
+ 404: true
32999
+ }
33000
+ },
33001
+ redFlags: {
33002
+ method: "post",
33003
+ path: "/health/episodes/{id}/red-flags",
33004
+ scopes: ["health:write"],
33005
+ tag: "Health",
33006
+ summary: "Record the red flags asked in chat",
33007
+ description: 'The red-flag screen answered in chat (`{ answers: { fever: false, vomiting: false }, thread? }`): the answers merge into the ones asked before and the Episode is marked checked today, so For the doctor shows it. With `thread` (the Space\'s agent key with `agents:converse`), a card confirms it there ("Noted as **checked: no red flags** (17 Oct).") with ways on to the summary and the Episode. Returns `{ episodeId, redFlags, checkedAt, postId }`.',
33008
+ body: healthRedFlagsInputSchema,
33009
+ status: 200,
33010
+ response: { description: "The answers, when, and the card's post." },
33011
+ errors: {
33012
+ 400: true,
33013
+ 403: "Health is off for the member, or the key acts for no member.",
33014
+ 404: true
33015
+ }
33016
+ },
33017
+ upsertEpisode: {
33018
+ method: "post",
33019
+ path: "/health/episodes/upsert",
33020
+ scopes: ["health:write"],
33021
+ tag: "Health",
33022
+ summary: "Find or start a health episode",
33023
+ description: "The member's open Episode this belongs to (same subject, same region or title, active in the last 14 days), or a new one. Returns `{ episode, created }`.",
33024
+ body: upsertHealthEpisodeInputSchema,
33025
+ status: 200,
33026
+ response: { description: "`{ episode, created }`." },
33027
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
33028
+ },
33029
+ summary: {
33030
+ method: "get",
33031
+ path: "/health/summary",
33032
+ scopes: ["health:read"],
33033
+ tag: "Health",
33034
+ summary: "Get the doctor summary",
33035
+ description: "Everything the one-page doctor summary reads, for one Episode (`episodeId`, default the latest open one): the Episode and its course day by day, its entries, what was tried with what each did, the open Conditions, current Medications, the profile, the GP, the next visit with its questions, `labs` (the last 3 confirmed lab draws, each with its results, the lab's range and flag), and `trends` (sleep and weight from Metrics over the last 28 days, one point a day). The member's words, never a diagnosis.",
33036
+ query: [{ name: "episodeId", description: "The Episode; default the latest open one." }],
33037
+ querySchema: healthSummaryQuerySchema,
33038
+ status: 200,
33039
+ response: { description: "The summary." },
33040
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33041
+ },
33042
+ medicalId: {
33043
+ method: "get",
33044
+ path: "/health/medical-id",
33045
+ scopes: ["health:read"],
33046
+ tag: "Health",
33047
+ summary: "Get the Medical ID",
33048
+ description: "The few facts someone helping in an emergency needs, like a phone's medical ID: the member's `name` (their display name), `bloodGroup`, `allergies`, `heightCm`, the `medications` they take now (not stopped, scheduled ones first) and the `emergencyContact` from the profile with their name and phone. A field not set is `null` (or empty). Each read by a key is appended to the member's access log.",
33049
+ status: 200,
33050
+ response: { description: "The Medical ID." },
33051
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33052
+ },
33053
+ listConditions: {
33054
+ method: "get",
33055
+ path: "/health/conditions",
33056
+ scopes: ["health:read"],
33057
+ tag: "Health",
33058
+ summary: "List health conditions",
33059
+ description: "The member's conditions. Something lasting (hay fever, runner's knee); `onset` is a fuzzy date. Reads and writes as the member who minted the key; another member's records read as missing.",
33060
+ status: 200,
33061
+ response: { description: "The member's conditions.", list: true },
33062
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33063
+ },
33064
+ getCondition: {
33065
+ method: "get",
33066
+ path: "/health/conditions/{id}",
33067
+ scopes: ["health:read"],
33068
+ tag: "Health",
33069
+ summary: "Get a health condition",
33070
+ description: "One condition.",
33071
+ status: 200,
33072
+ response: { description: "The condition." },
33073
+ errors: { 404: true }
33074
+ },
33075
+ createCondition: {
33076
+ method: "post",
33077
+ path: "/health/conditions",
33078
+ scopes: ["health:write"],
33079
+ tag: "Health",
33080
+ summary: "Create a health condition",
33081
+ description: "Add a condition, always private to the member (`subjectNodeId` names who it is about; absent = the member).",
33082
+ body: createHealthConditionInputSchema,
33083
+ status: 201,
33084
+ response: { description: "The condition." },
33085
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
33086
+ },
33087
+ updateCondition: {
33088
+ method: "patch",
33089
+ path: "/health/conditions/{id}",
33090
+ scopes: ["health:write"],
33091
+ tag: "Health",
33092
+ summary: "Update a health condition",
33093
+ description: "Patch a condition; `null` clears a field.",
33094
+ body: updateHealthConditionInputSchema,
33095
+ status: 200,
33096
+ response: { description: "The condition." },
33097
+ errors: {
33098
+ 400: true,
33099
+ 403: "Health is off for the member, or the key acts for no member.",
33100
+ 404: true
33101
+ }
33102
+ },
33103
+ deleteCondition: {
33104
+ method: "delete",
33105
+ path: "/health/conditions/{id}",
33106
+ scopes: ["health:write"],
33107
+ tag: "Health",
33108
+ summary: "Delete a health condition",
33109
+ description: "Delete a condition. An Agent's delete waits on the member's confirmation (428).",
33110
+ status: 204,
33111
+ response: { description: "Deleted." },
33112
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33113
+ },
33114
+ addCareTeam: {
33115
+ method: "post",
33116
+ path: "/health/conditions/{id}/care-team",
33117
+ scopes: ["health:write"],
33118
+ tag: "Health",
33119
+ summary: "Add to a health condition's care team",
33120
+ description: "Link a Contact to the Condition (`cared_by`, condition \u2192 contact) with their role on it in `label` (\"dermatologist\"). Adding one already on it sets the role. The Condition's `careTeam` lists them by name with their phone; the Health home's Care team card and For the doctor read them.",
33121
+ body: addHealthCareTeamInputSchema,
33122
+ status: 201,
33123
+ response: { description: "The Condition, with its care team." },
33124
+ errors: {
33125
+ 400: true,
33126
+ 403: "Health is off for the member, or the key acts for no member.",
33127
+ 404: true
33128
+ }
33129
+ },
33130
+ removeCareTeam: {
33131
+ method: "delete",
33132
+ path: "/health/conditions/{id}/care-team/{contactId}",
33133
+ scopes: ["health:write"],
33134
+ tag: "Health",
33135
+ summary: "Remove from a health condition's care team",
33136
+ description: "Take the Contact off the Condition's care team (the Contact stays). 404 when they weren't on it.",
33137
+ pathParams: { contactId: "The Contact on the care team." },
33138
+ status: 200,
33139
+ response: { description: "The Condition, with its care team." },
33140
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33141
+ },
33142
+ listEpisodes: {
33143
+ method: "get",
33144
+ path: "/health/episodes",
33145
+ scopes: ["health:read"],
33146
+ tag: "Health",
33147
+ summary: "List health episodes",
33148
+ description: "The member's episodes. One stretch of a complaint, open ones first, with the latest live severity. `GET \u2026/{id}` returns its entries (superseded ones too), doses, visits and Condition. Create one already over with `status: \"resolved\"` and `endedAt` (not before `startedAt`, not in the future; `endedAt` alone implies resolved). Resolving one stamps `endedAt`; `redFlags` answers merge. Reads and writes as the member who minted the key; another member's records read as missing.",
33149
+ query: [{ name: "status", description: "`open`, `watching` or `resolved`." }],
33150
+ querySchema: healthEpisodeListQuerySchema,
33151
+ status: 200,
33152
+ response: { description: "The member's episodes.", list: true },
33153
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33154
+ },
33155
+ getEpisode: {
33156
+ method: "get",
33157
+ path: "/health/episodes/{id}",
33158
+ scopes: ["health:read"],
33159
+ tag: "Health",
33160
+ summary: "Get a health episode",
33161
+ description: "One episode.",
33162
+ status: 200,
33163
+ response: { description: "The episode." },
33164
+ errors: { 404: true }
33165
+ },
33166
+ createEpisode: {
33167
+ method: "post",
33168
+ path: "/health/episodes",
33169
+ scopes: ["health:write"],
33170
+ tag: "Health",
33171
+ summary: "Create a health episode",
33172
+ description: "Add a episode, always private to the member (`subjectNodeId` names who it is about; absent = the member).",
33173
+ body: createHealthEpisodeInputSchema,
33174
+ status: 201,
33175
+ response: { description: "The episode." },
33176
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
33177
+ },
33178
+ updateEpisode: {
33179
+ method: "patch",
33180
+ path: "/health/episodes/{id}",
33181
+ scopes: ["health:write"],
33182
+ tag: "Health",
33183
+ summary: "Update a health episode",
33184
+ description: "Patch a episode; `null` clears a field.",
33185
+ body: updateHealthEpisodeInputSchema,
33186
+ status: 200,
33187
+ response: { description: "The episode." },
33188
+ errors: {
33189
+ 400: true,
33190
+ 403: "Health is off for the member, or the key acts for no member.",
33191
+ 404: true
33192
+ }
33193
+ },
33194
+ deleteEpisode: {
33195
+ method: "delete",
33196
+ path: "/health/episodes/{id}",
33197
+ scopes: ["health:write"],
33198
+ tag: "Health",
33199
+ summary: "Delete a health episode",
33200
+ description: "Delete a episode. An Agent's delete waits on the member's confirmation (428).",
33201
+ status: 204,
33202
+ response: { description: "Deleted." },
33203
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33204
+ },
33205
+ logSymptom: {
33206
+ method: "post",
33207
+ path: "/health/episodes/{id}/entries",
33208
+ scopes: ["health:write"],
33209
+ tag: "Health",
33210
+ summary: "Log a health symptom",
33211
+ description: "Add one thing said about an Episode (`symptom`, `bodyRegion`, `severity` 0\u201310, `severityGuessed` when it is a guess from the wording, `qualifiers`). An Agent's entry is `source: chat`.",
33212
+ body: logHealthSymptomInputSchema,
33213
+ status: 201,
33214
+ response: { description: "The entry." },
33215
+ errors: {
33216
+ 400: true,
33217
+ 403: "Health is off for the member, or the key acts for no member.",
33218
+ 404: true
33219
+ }
33220
+ },
33221
+ correctSymptom: {
33222
+ method: "post",
33223
+ path: "/health/episodes/{id}/entries/{entryId}/correct",
33224
+ scopes: ["health:write"],
33225
+ tag: "Health",
33226
+ summary: "Correct a health symptom",
33227
+ description: 'Correct the live version of an entry ("not the foot, the ankle"): a new entry that supersedes it and copies every field not named, so the history stays. 409 when the entry was corrected already.',
33228
+ pathParams: { entryId: "The symptom entry's id." },
33229
+ body: correctHealthSymptomInputSchema,
33230
+ status: 201,
33231
+ response: { description: "The new entry." },
33232
+ errors: { 400: true, 404: true, 409: true }
33233
+ },
33234
+ deleteSymptom: {
33235
+ method: "delete",
33236
+ path: "/health/episodes/{id}/entries/{entryId}",
33237
+ scopes: ["health:write"],
33238
+ tag: "Health",
33239
+ summary: "Take back a health symptom",
33240
+ description: "Take back the live version of an entry; what it corrected is live again. 409 when it was corrected since.",
33241
+ pathParams: { entryId: "The symptom entry's id." },
33242
+ status: 204,
33243
+ response: { description: "Taken back." },
33244
+ errors: { 404: true, 409: true }
33245
+ },
33246
+ listEncounters: {
33247
+ method: "get",
33248
+ path: "/health/encounters",
33249
+ scopes: ["health:read"],
33250
+ tag: "Health",
33251
+ summary: "List health encounters",
33252
+ description: "The member's encounters. Visits, latest first, each with its doctor's name (a Contact), questions to ask and outcome. Reads and writes as the member who minted the key; another member's records read as missing.",
33253
+ status: 200,
33254
+ response: { description: "The member's encounters.", list: true },
33255
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33256
+ },
33257
+ getEncounter: {
33258
+ method: "get",
33259
+ path: "/health/encounters/{id}",
33260
+ scopes: ["health:read"],
33261
+ tag: "Health",
33262
+ summary: "Get a health encounter",
33263
+ description: "One encounter.",
33264
+ status: 200,
33265
+ response: { description: "The encounter." },
33266
+ errors: { 404: true }
33267
+ },
33268
+ createEncounter: {
32366
33269
  method: "post",
32367
- path: "/habits/{id}/check-ins",
32368
- scopes: ["habits:write"],
32369
- tag: "Habits",
32370
- summary: "Check in",
32371
- description: "Record a check-in, with an optional short `note`; fires `habit.updated` with the refreshed streak. A habit counted from an ingest (`source` other than `manual`) answers 409.",
32372
- body: createHabitEntryInputSchema,
33270
+ path: "/health/encounters",
33271
+ scopes: ["health:write"],
33272
+ tag: "Health",
33273
+ summary: "Create a health encounter",
33274
+ description: "Add a encounter, always private to the member (`subjectNodeId` names who it is about; absent = the member).",
33275
+ body: createHealthEncounterInputSchema,
32373
33276
  status: 201,
32374
- response: { description: "The refreshed habit." },
33277
+ response: { description: "The encounter." },
33278
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
33279
+ },
33280
+ updateEncounter: {
33281
+ method: "patch",
33282
+ path: "/health/encounters/{id}",
33283
+ scopes: ["health:write"],
33284
+ tag: "Health",
33285
+ summary: "Update a health encounter",
33286
+ description: "Patch a encounter; `null` clears a field.",
33287
+ body: updateHealthEncounterInputSchema,
33288
+ status: 200,
33289
+ response: { description: "The encounter." },
32375
33290
  errors: {
32376
33291
  400: true,
33292
+ 403: "Health is off for the member, or the key acts for no member.",
32377
33293
  404: true
32378
- },
32379
- webhook: "habit.updated",
32380
- operationId: "check_in_habit"
33294
+ }
32381
33295
  },
32382
- skip: {
33296
+ deleteEncounter: {
33297
+ method: "delete",
33298
+ path: "/health/encounters/{id}",
33299
+ scopes: ["health:write"],
33300
+ tag: "Health",
33301
+ summary: "Delete a health encounter",
33302
+ description: "Delete a encounter. An Agent's delete waits on the member's confirmation (428).",
33303
+ status: 204,
33304
+ response: { description: "Deleted." },
33305
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33306
+ },
33307
+ listMedications: {
33308
+ method: "get",
33309
+ path: "/health/medications",
33310
+ scopes: ["health:read"],
33311
+ tag: "Health",
33312
+ summary: "List health medications",
33313
+ description: "The member's medications. Drugs and supplements, stopped ones last, with when one was last taken. Reads and writes as the member who minted the key; another member's records read as missing.",
33314
+ status: 200,
33315
+ response: { description: "The member's medications.", list: true },
33316
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33317
+ },
33318
+ getMedication: {
33319
+ method: "get",
33320
+ path: "/health/medications/{id}",
33321
+ scopes: ["health:read"],
33322
+ tag: "Health",
33323
+ summary: "Get a health medication",
33324
+ description: "One medication.",
33325
+ status: 200,
33326
+ response: { description: "The medication." },
33327
+ errors: { 404: true }
33328
+ },
33329
+ createMedication: {
32383
33330
  method: "post",
32384
- path: "/habits/{id}/skip",
32385
- scopes: ["habits:write"],
32386
- tag: "Habits",
32387
- summary: "Skip a habit",
32388
- description: '"Skip gym today": a neutral rest day (a `skip` entry, value 0), never a miss. The streak treats the period as neutral and spends no grace. `date` (YYYY-MM-DD) defaults to today; an optional `note` says why ("Dentist"). Skipping a skipped day changes nothing but its note. Undo by deleting the skip entry like a check-in. Returns the refreshed habit; fires `habit.updated`.',
32389
- body: habitSkipInputSchema,
33331
+ path: "/health/medications",
33332
+ scopes: ["health:write"],
33333
+ tag: "Health",
33334
+ summary: "Create a health medication",
33335
+ description: "Add a medication, always private to the member (`subjectNodeId` names who it is about; absent = the member).",
33336
+ body: createHealthMedicationInputSchema,
32390
33337
  status: 201,
32391
- response: { description: "The refreshed habit." },
33338
+ response: { description: "The medication." },
33339
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
33340
+ },
33341
+ updateMedication: {
33342
+ method: "patch",
33343
+ path: "/health/medications/{id}",
33344
+ scopes: ["health:write"],
33345
+ tag: "Health",
33346
+ summary: "Update a health medication",
33347
+ description: "Patch a medication; `null` clears a field.",
33348
+ body: updateHealthMedicationInputSchema,
33349
+ status: 200,
33350
+ response: { description: "The medication." },
32392
33351
  errors: {
32393
33352
  400: true,
33353
+ 403: "Health is off for the member, or the key acts for no member.",
32394
33354
  404: true
32395
- },
32396
- webhook: "habit.updated"
33355
+ }
32397
33356
  },
32398
- pause: {
33357
+ deleteMedication: {
33358
+ method: "delete",
33359
+ path: "/health/medications/{id}",
33360
+ scopes: ["health:write"],
33361
+ tag: "Health",
33362
+ summary: "Delete a health medication",
33363
+ description: "Delete a medication. An Agent's delete waits on the member's confirmation (428).",
33364
+ status: 204,
33365
+ response: { description: "Deleted." },
33366
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33367
+ },
33368
+ listDoses: {
33369
+ method: "get",
33370
+ path: "/health/medications/{id}/doses",
33371
+ scopes: ["health:read"],
33372
+ tag: "Health",
33373
+ summary: "List health doses",
33374
+ description: "The doses of one Medication, latest first.",
33375
+ status: 200,
33376
+ response: { description: "The doses.", list: true },
33377
+ errors: { 404: true }
33378
+ },
33379
+ logDose: {
32399
33380
  method: "post",
32400
- path: "/habits/{id}/pause",
32401
- scopes: ["habits:write"],
32402
- tag: "Habits",
32403
- summary: "Pause a habit",
32404
- description: '"Pause gym till the 26th": the habit rests until `until` (the day it\'s back, after today): off Now, no push, streak and plant wait. Pausing a paused habit moves its end. Returns the refreshed habit with `pause`; fires `habit.updated`.',
32405
- body: habitPauseInputSchema,
33381
+ path: "/health/medications/{id}/doses",
33382
+ scopes: ["health:write"],
33383
+ tag: "Health",
33384
+ summary: "Log a health dose",
33385
+ description: "Log a dose taken, optionally for an Episode, with what it did (`effect`: none, a_little, helped, worse).",
33386
+ body: logHealthDoseInputSchema,
32406
33387
  status: 201,
32407
- response: { description: "The refreshed habit." },
33388
+ response: { description: "The dose." },
32408
33389
  errors: {
32409
33390
  400: true,
33391
+ 403: "Health is off for the member, or the key acts for no member.",
32410
33392
  404: true
32411
- },
32412
- webhook: "habit.updated"
33393
+ }
32413
33394
  },
32414
- resume: {
32415
- method: "post",
32416
- path: "/habits/{id}/resume",
32417
- scopes: ["habits:write"],
32418
- tag: "Habits",
32419
- summary: "Resume a habit",
32420
- description: "End a habit's pause: it's back today. The rest so far stays neutral. Returns the refreshed habit; fires `habit.updated`.",
32421
- status: 201,
32422
- response: { description: "The refreshed habit." },
32423
- errors: { 404: true },
32424
- webhook: "habit.updated"
33395
+ updateDose: {
33396
+ method: "patch",
33397
+ path: "/health/medications/{id}/doses/{doseId}",
33398
+ scopes: ["health:write"],
33399
+ tag: "Health",
33400
+ summary: "Update a health dose",
33401
+ description: "Say later what a dose did, or fix it.",
33402
+ pathParams: { doseId: "The dose's id." },
33403
+ body: updateHealthDoseInputSchema,
33404
+ status: 200,
33405
+ response: { description: "The dose." },
33406
+ errors: { 400: true, 404: true }
32425
33407
  },
32426
- retire: {
32427
- method: "post",
32428
- path: "/habits/{id}/retire",
32429
- scopes: ["habits:write"],
32430
- tag: "Habits",
32431
- summary: "Retire a habit",
32432
- description: '"I\'m done with the cold showers habit": the habit moves to the Grown shelf with its plant in bloom and keeps its history. It leaves Now and Today, sends no push, takes no check-in by title and is never due; Progress keeps its past. Returns the refreshed habit with `retiredAt`; undo with `/unretire`. Fires `habit.updated`.',
32433
- status: 201,
32434
- response: { description: "The refreshed habit." },
32435
- errors: { 404: true },
32436
- webhook: "habit.updated"
33408
+ deleteDose: {
33409
+ method: "delete",
33410
+ path: "/health/medications/{id}/doses/{doseId}",
33411
+ scopes: ["health:write"],
33412
+ tag: "Health",
33413
+ summary: "Take back a health dose",
33414
+ description: "Take a dose back.",
33415
+ pathParams: { doseId: "The dose's id." },
33416
+ status: 204,
33417
+ response: { description: "Taken back." },
33418
+ errors: { 404: true }
32437
33419
  },
32438
- unretire: {
33420
+ listShares: {
33421
+ method: "get",
33422
+ path: "/health/shares",
33423
+ scopes: ["health:read"],
33424
+ tag: "Health",
33425
+ summary: "List doctor summary shares",
33426
+ description: "The member's \"For the doctor\" links, newest first: the sections each froze, when it expires or was taken back, how often it was opened, and `live`. Never the link's secret or the snapshot. Only the member makes one, in do.",
33427
+ status: 200,
33428
+ response: { description: "The shares." },
33429
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33430
+ },
33431
+ revokeShare: {
33432
+ method: "delete",
33433
+ path: "/health/shares/{id}",
33434
+ scopes: ["health:write"],
33435
+ tag: "Health",
33436
+ summary: "Take a doctor summary share back",
33437
+ description: "The link stops opening (410). Idempotent.",
33438
+ pathParams: { id: "The share's id." },
33439
+ status: 200,
33440
+ response: { description: "The share." },
33441
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33442
+ },
33443
+ listLabPanels: {
33444
+ method: "get",
33445
+ path: "/health/labs",
33446
+ scopes: ["health:read"],
33447
+ tag: "Health",
33448
+ summary: "List health lab draws",
33449
+ description: "The member's lab draws. Lab draws, latest first, each with its sheet of results (FHIR Observation-shaped: an `analyte` key, `loinc`, a UCUM `unit`, the lab's `refLow`/`refHigh`; `flag` is worked out from the range when absent). A draw read out of a document stays `status: draft` until the member checks it. On update, `results` replaces the whole sheet. Reads and writes as the member who minted the key; another member's records read as missing.",
33450
+ status: 200,
33451
+ response: { description: "The member's lab draws.", list: true },
33452
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33453
+ },
33454
+ listLabAnalytes: {
33455
+ method: "get",
33456
+ path: "/health/labs/analytes",
33457
+ scopes: ["health:read"],
33458
+ tag: "Health",
33459
+ summary: "List the canonical lab analytes",
33460
+ description: 'The fixed analyte vocabulary: `key`, the printed `name`, `group`, the usual `unit`, `loinc` where plain, and `aliases` (English and Dutch; matched regardless of case, spaces, punctuation and accents). A lab result is written under the canonical key its `analyte` or `name` stands for ("ALAT" \u2192 `alt`, "25-OH vitamin D" \u2192 `vitamin_d`); a name that matches none keeps its own slug and comes back in the draw\'s `unmatched`. Trends read old keys as their canonical one.',
33461
+ status: 200,
33462
+ response: { description: "The analytes." },
33463
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33464
+ },
33465
+ labNormalizeReport: {
33466
+ method: "get",
33467
+ path: "/health/labs/normalize",
33468
+ scopes: ["health:read"],
33469
+ tag: "Health",
33470
+ summary: "Report how lab keys would normalize",
33471
+ description: "A dry run: each stored result whose key isn't its canonical one (`changes`: `resultId`, `from`, `to`), and the keys matching no canonical analyte (`unmatched`, with how many results). Nothing is rewritten.",
33472
+ status: 200,
33473
+ response: { description: "The report." },
33474
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33475
+ },
33476
+ getLabPanel: {
33477
+ method: "get",
33478
+ path: "/health/labs/{id}",
33479
+ scopes: ["health:read"],
33480
+ tag: "Health",
33481
+ summary: "Get a health lab draw",
33482
+ description: "One lab draw.",
33483
+ status: 200,
33484
+ response: { description: "The lab draw." },
33485
+ errors: { 404: true }
33486
+ },
33487
+ createLabPanel: {
32439
33488
  method: "post",
32440
- path: "/habits/{id}/unretire",
32441
- scopes: ["habits:write"],
32442
- tag: "Habits",
32443
- summary: "Bring a habit back",
32444
- description: "Bring a grown (retired) habit back: it's growing again from today, its history intact. Returns the refreshed habit; fires `habit.updated`.",
33489
+ path: "/health/labs",
33490
+ scopes: ["health:write"],
33491
+ tag: "Health",
33492
+ summary: "Create a health lab draw",
33493
+ description: "Add a lab draw, always private to the member (`subjectNodeId` names who it is about; absent = the member).",
33494
+ body: createHealthLabPanelInputSchema,
32445
33495
  status: 201,
32446
- response: { description: "The refreshed habit." },
32447
- errors: { 404: true },
32448
- webhook: "habit.updated"
33496
+ response: { description: "The lab draw." },
33497
+ errors: { 400: true, 403: "Health is off for the member, or the key acts for no member." }
32449
33498
  },
32450
- updateCheckIn: {
33499
+ updateLabPanel: {
32451
33500
  method: "patch",
32452
- path: "/habits/{id}/check-ins/{entryId}",
32453
- pathParams: { entryId: "The check-in entry's ULID." },
32454
- scopes: ["habits:write"],
32455
- tag: "Habits",
32456
- summary: "Note a check-in",
32457
- description: "Edit a check-in afterwards: set, replace or clear (`null` or blank) its short `note` (at most 280 characters). The amount and time stay; undo a wrong check-in instead. Returns the check-in; fires `habit.updated`.",
32458
- body: updateHabitEntryInputSchema,
33501
+ path: "/health/labs/{id}",
33502
+ scopes: ["health:write"],
33503
+ tag: "Health",
33504
+ summary: "Update a health lab draw",
33505
+ description: "Patch a lab draw; `null` clears a field.",
33506
+ body: updateHealthLabPanelInputSchema,
32459
33507
  status: 200,
32460
- response: { description: "The check-in, with its note." },
33508
+ response: { description: "The lab draw." },
32461
33509
  errors: {
32462
33510
  400: true,
33511
+ 403: "Health is off for the member, or the key acts for no member.",
32463
33512
  404: true
32464
- },
32465
- webhook: "habit.updated",
32466
- operationId: "note_habit_check_in"
33513
+ }
32467
33514
  },
32468
- removeCheckIn: {
33515
+ deleteLabPanel: {
32469
33516
  method: "delete",
32470
- path: "/habits/{id}/check-ins/{entryId}",
32471
- pathParams: { entryId: "The check-in entry's ULID." },
32472
- scopes: ["habits:write"],
32473
- tag: "Habits",
32474
- summary: "Undo a check-in",
32475
- description: "Delete a check-in; fires `habit.updated`.",
33517
+ path: "/health/labs/{id}",
33518
+ scopes: ["health:write"],
33519
+ tag: "Health",
33520
+ summary: "Delete a health lab draw",
33521
+ description: "Delete a lab draw. An Agent's delete waits on the member's confirmation (428).",
33522
+ status: 204,
33523
+ response: { description: "Deleted." },
33524
+ errors: { 403: "Health is off for the member, or the key acts for no member.", 404: true }
33525
+ },
33526
+ labOverview: {
33527
+ method: "get",
33528
+ path: "/health/analytes",
33529
+ scopes: ["health:read"],
33530
+ tag: "Health",
33531
+ summary: "List lab analytes",
33532
+ description: "Every analyte the member's draws measured, each with its trend (oldest point first), the personal target band and the supplement taken for it; plus `draws`, `firstAt`, `lastAt`, `lastLab`.",
32476
33533
  status: 200,
32477
- response: { description: "The refreshed habit." },
32478
- errors: { 404: true },
32479
- webhook: "habit.updated",
32480
- operationId: "undo_habit_check_in"
33534
+ response: { description: "The lab overview." },
33535
+ errors: { 403: "Health is off for the member, or the key acts for no member." }
33536
+ },
33537
+ getLabAnalyte: {
33538
+ method: "get",
33539
+ path: "/health/analytes/{analyte}",
33540
+ scopes: ["health:read"],
33541
+ tag: "Health",
33542
+ summary: "Get a lab analyte",
33543
+ description: "One analyte's trend across draws. 404 when no draw measured it.",
33544
+ pathParams: { analyte: "The analyte key (`vitamin_d`)." },
33545
+ status: 200,
33546
+ response: { description: "The analyte." },
33547
+ errors: { 404: true }
33548
+ },
33549
+ setLabAnalyte: {
33550
+ method: "put",
33551
+ path: "/health/analytes/{analyte}",
33552
+ scopes: ["health:write"],
33553
+ tag: "Health",
33554
+ summary: "Set a lab analyte",
33555
+ description: "Keep a personal target band (`targetLow`/`targetHigh`, who set it in `targetByContactId`) or the Medication taken for it. An absent key keeps a field; `null` clears it.",
33556
+ pathParams: { analyte: "The analyte key (`vitamin_d`)." },
33557
+ body: setHealthLabAnalyteInputSchema,
33558
+ status: 200,
33559
+ response: { description: "The analyte." },
33560
+ errors: {
33561
+ 400: true,
33562
+ 403: "Health is off for the member, or the key acts for no member.",
33563
+ 404: true
33564
+ }
32481
33565
  }
32482
33566
  });
32483
33567
 
@@ -36671,7 +37755,7 @@ function parseCapture2(input, now) {
36671
37755
 
36672
37756
  // src/meta.ts
36673
37757
  var CLI_NAME = "do";
36674
- var VERSION = "1.314.1";
37758
+ var VERSION = "1.316.0";
36675
37759
  var API_PREFIX = "/api/v1";
36676
37760
  var DEFAULT_API_URL = "http://localhost:3001";
36677
37761
 
@@ -38150,7 +39234,7 @@ var CONNECTION_COMMAND_SPECS = [
38150
39234
  ],
38151
39235
  scopes: ["connections:write"],
38152
39236
  returns: "DoccleStatus",
38153
- notes: "PUT /api/v1/documents/doccle/login, as `do documents doccle connect`. The password is read from the file's first line, never a flag. 422 when Doccle refuses the login or asks for a captcha (nothing is kept)."
39237
+ notes: `${routeOf(documentOperations.doccleConnect)}, as \`do documents doccle connect\`. The password is read from the file's first line, never a flag. 422 when Doccle refuses the login or asks for a captcha (nothing is kept).`
38154
39238
  },
38155
39239
  {
38156
39240
  path: ["steam", "link"],
@@ -38300,7 +39384,7 @@ async function connectDoccle({
38300
39384
  }
38301
39385
  const secret = password;
38302
39386
  return {
38303
- data: await guarded([secret], () => requireClient().put("/documents/doccle/login", { username, password: secret })),
39387
+ data: await guarded([secret], () => requireClient().put(pathOf(documentOperations.doccleConnect), { username, password: secret })),
38304
39388
  render: renderRecord
38305
39389
  };
38306
39390
  }
@@ -38898,12 +39982,9 @@ var ENTITIES = [
38898
39982
  createInput: "CreateKeyDateInput",
38899
39983
  updateInput: "UpdateKeyDateInput"
38900
39984
  }),
38901
- {
39985
+ entityFromOperations(documentOperations, {
38902
39986
  name: "documents",
38903
- path: "/documents",
38904
- readScopes: ["documents:read"],
38905
39987
  returns: "DocumentView",
38906
- canGet: true,
38907
39988
  listFlags: [
38908
39989
  {
38909
39990
  name: "kind",
@@ -38921,15 +40002,9 @@ var ENTITIES = [
38921
40002
  query: "providerId"
38922
40003
  }
38923
40004
  ],
38924
- write: {
38925
- scopes: ["documents:write"],
38926
- createSchema: createDocumentInputSchema,
38927
- createInput: "CreateDocumentInput",
38928
- updateSchema: updateDocumentInputSchema,
38929
- updateInput: "UpdateDocumentInput",
38930
- canDelete: true
38931
- }
38932
- },
40005
+ createInput: "CreateDocumentInput",
40006
+ updateInput: "UpdateDocumentInput"
40007
+ }),
38933
40008
  entityFromOperations(incidentOperations, {
38934
40009
  name: "incidents",
38935
40010
  returns: "IncidentView",
@@ -39546,12 +40621,42 @@ var HEALTH_COMMAND_SPECS = [
39546
40621
  notes: "Body: SetHealthLabAnalyteInput. An absent key keeps a field; null clears it."
39547
40622
  }
39548
40623
  ];
39549
- var HEALTH_RECORD_PATHS = {
39550
- condition: "/health/conditions",
39551
- episode: "/health/episodes",
39552
- visit: "/health/encounters",
39553
- med: "/health/medications",
39554
- lab: "/health/labs"
40624
+ var HEALTH_RECORD_OPERATIONS = {
40625
+ condition: {
40626
+ list: healthOperations.listConditions,
40627
+ get: healthOperations.getCondition,
40628
+ create: healthOperations.createCondition,
40629
+ update: healthOperations.updateCondition,
40630
+ remove: healthOperations.deleteCondition
40631
+ },
40632
+ episode: {
40633
+ list: healthOperations.listEpisodes,
40634
+ get: healthOperations.getEpisode,
40635
+ create: healthOperations.createEpisode,
40636
+ update: healthOperations.updateEpisode,
40637
+ remove: healthOperations.deleteEpisode
40638
+ },
40639
+ visit: {
40640
+ list: healthOperations.listEncounters,
40641
+ get: healthOperations.getEncounter,
40642
+ create: healthOperations.createEncounter,
40643
+ update: healthOperations.updateEncounter,
40644
+ remove: healthOperations.deleteEncounter
40645
+ },
40646
+ med: {
40647
+ list: healthOperations.listMedications,
40648
+ get: healthOperations.getMedication,
40649
+ create: healthOperations.createMedication,
40650
+ update: healthOperations.updateMedication,
40651
+ remove: healthOperations.deleteMedication
40652
+ },
40653
+ lab: {
40654
+ list: healthOperations.listLabPanels,
40655
+ get: healthOperations.getLabPanel,
40656
+ create: healthOperations.createLabPanel,
40657
+ update: healthOperations.updateLabPanel,
40658
+ remove: healthOperations.deleteLabPanel
40659
+ }
39555
40660
  };
39556
40661
 
39557
40662
  // src/journal-import.ts
@@ -40975,27 +42080,27 @@ var COMMAND_SPECS = [
40975
42080
  flags: [
40976
42081
  { name: "source", type: "string", description: "gmail:<id> or doccle_zip (required)." }
40977
42082
  ],
40978
- scopes: ["documents:read"],
42083
+ scopes: [...documentOperations.filed.scopes],
40979
42084
  returns: "DocumentFiled",
40980
- notes: "GET /api/v1/documents/filed?source=. Off (403) until DOCUMENTS_IMPORT_ENABLED=true."
42085
+ notes: `${routeOf(documentOperations.filed)}?source=. Off (403) until DOCUMENTS_IMPORT_ENABLED=true.`
40981
42086
  },
40982
42087
  {
40983
42088
  path: ["documents", "doccle", "status"],
40984
42089
  summary: "Your Doccle import: whether it's connected, archive after import, how the last run went and why it's stuck, if it is.",
40985
42090
  args: [],
40986
42091
  flags: [],
40987
- scopes: ["documents:read"],
42092
+ scopes: [...documentOperations.doccleStatus.scopes],
40988
42093
  returns: "DoccleStatus",
40989
- notes: "GET /api/v1/documents/doccle. The import files your Doccle mailbox into Papers every night (03:30 Brussels). `failure` says why it stopped (credentials, captcha, changed, unavailable) and what to do. Never shows the login."
42094
+ notes: `${routeOf(documentOperations.doccleStatus)}. The import files your Doccle mailbox into Papers every night (03:30 Brussels). \`failure\` says why it stopped (credentials, captcha, changed, unavailable) and what to do. Never shows the login.`
40990
42095
  },
40991
42096
  {
40992
42097
  path: ["documents", "doccle", "sync"],
40993
42098
  summary: "Run your Doccle import now: each new Doccle document becomes a paper (sender, date, PDF), nothing twice.",
40994
42099
  args: [],
40995
42100
  flags: [],
40996
- scopes: ["documents:write"],
42101
+ scopes: [...documentOperations.doccleSync.scopes],
40997
42102
  returns: "DoccleSyncResult",
40998
- notes: "POST /api/v1/documents/doccle/sync. Deduped on Doccle's id and on the PDF's hash (so papers from a Doccle ZIP export aren't filed again). outcome: ok, failed (see failure), not_connected, off (DOCUMENTS_IMPORT_ENABLED isn't on), busy (a run is going)."
42103
+ notes: `${routeOf(documentOperations.doccleSync)}. Deduped on Doccle's id and on the PDF's hash (so papers from a Doccle ZIP export aren't filed again). outcome: ok, failed (see failure), not_connected, off (DOCUMENTS_IMPORT_ENABLED isn't on), busy (a run is going).`
40999
42104
  },
41000
42105
  {
41001
42106
  path: ["documents", "doccle", "connect"],
@@ -41014,9 +42119,9 @@ var COMMAND_SPECS = [
41014
42119
  description: "Read the password from the first line of stdin (else DO_DOCCLE_PASSWORD)."
41015
42120
  }
41016
42121
  ],
41017
- scopes: ["connections:write"],
42122
+ scopes: [...documentOperations.doccleConnect.scopes],
41018
42123
  returns: "DoccleStatus",
41019
- notes: "PUT /api/v1/documents/doccle/login. Needs a key with the opt-in connections:write (held by name: Full access never grants it, and a guest's key never holds it). The password is never a flag (shell history): pass --password-file <path>, pipe it in with --password-stdin, or set DO_DOCCLE_PASSWORD. 422 when Doccle refuses the login or asks for a captcha (nothing is kept), 503 when Doccle is down. The login is never returned."
42124
+ notes: `${routeOf(documentOperations.doccleConnect)}. Needs a key with the opt-in connections:write (held by name: Full access never grants it, and a guest's key never holds it). The password is never a flag (shell history): pass --password-file <path>, pipe it in with --password-stdin, or set DO_DOCCLE_PASSWORD. 422 when Doccle refuses the login or asks for a captcha (nothing is kept), 503 when Doccle is down. The login is never returned.`
41020
42125
  },
41021
42126
  {
41022
42127
  path: ["documents", "doccle", "disconnect"],
@@ -41025,9 +42130,9 @@ var COMMAND_SPECS = [
41025
42130
  flags: [
41026
42131
  { name: "allow-destructive", type: "boolean", description: "Required to forget the login." }
41027
42132
  ],
41028
- scopes: ["documents:write"],
42133
+ scopes: [...documentOperations.doccleDisconnect.scopes],
41029
42134
  returns: "DoccleStatus",
41030
- notes: "DELETE /api/v1/documents/doccle/login. Gated behind --allow-destructive."
42135
+ notes: `${routeOf(documentOperations.doccleDisconnect)}. Gated behind --allow-destructive.`
41031
42136
  },
41032
42137
  {
41033
42138
  path: ["documents", "doccle", "set"],
@@ -41040,9 +42145,9 @@ var COMMAND_SPECS = [
41040
42145
  description: "on or off (off by default): archive in Doccle after import."
41041
42146
  }
41042
42147
  ],
41043
- scopes: ["documents:write"],
42148
+ scopes: [...documentOperations.doccleSettings.scopes],
41044
42149
  returns: "DoccleStatus",
41045
- notes: "PATCH /api/v1/documents/doccle with { archiveAfterImport }."
42150
+ notes: `${routeOf(documentOperations.doccleSettings)} with { archiveAfterImport }.`
41046
42151
  },
41047
42152
  {
41048
42153
  path: ["documents", "ask"],
@@ -41062,9 +42167,9 @@ var COMMAND_SPECS = [
41062
42167
  description: "How many passages at most (default 5, up to 20)."
41063
42168
  }
41064
42169
  ],
41065
- scopes: ["documents:read"],
42170
+ scopes: [...documentOperations.ask.scopes],
41066
42171
  returns: "DocumentAnswer",
41067
- notes: "POST /api/v1/documents/ask with { question, documentId?, limit? }. Reads the papers' key fields and their files' text (a page per form feed). do writes no prose: word the answer from the passages, quote the pages, and say it is not legal advice. `withoutText` names the papers with no file text to read. An Only me paper answers only its owner's key with documents:private."
42172
+ notes: `${routeOf(documentOperations.ask)} with { question, documentId?, limit? }. Reads the papers' key fields and their files' text (a page per form feed). do writes no prose: word the answer from the passages, quote the pages, and say it is not legal advice. \`withoutText\` names the papers with no file text to read. An Only me paper answers only its owner's key with documents:private.`
41068
42173
  },
41069
42174
  {
41070
42175
  path: ["documents", "search"],
@@ -41078,36 +42183,36 @@ var COMMAND_SPECS = [
41078
42183
  description: "How many hits at most (default 20, up to 50)."
41079
42184
  }
41080
42185
  ],
41081
- scopes: ["documents:read"],
42186
+ scopes: [...documentOperations.search.scopes],
41082
42187
  returns: "{ q, hits: DocumentPassage[] }",
41083
- notes: "GET /api/v1/documents/search?q=&documentId=&limit=."
42188
+ notes: `${routeOf(documentOperations.search)}?q=&documentId=&limit=.`
41084
42189
  },
41085
42190
  {
41086
42191
  path: ["documents", "unmatched"],
41087
42192
  summary: "List the papers that ask for money (an amount due) and have no payment linked yet, oldest due first.",
41088
42193
  args: [],
41089
42194
  flags: [],
41090
- scopes: ["documents:read", "finance:read"],
42195
+ scopes: [...documentOperations.unmatched.scopes],
41091
42196
  returns: "UnmatchedPaper",
41092
- notes: "GET /api/v1/documents/unmatched. `candidates` counts the payments that fit equally well (pick one with `do documents link-payment`). A payment on an account you can't see doesn't count."
42197
+ notes: `${routeOf(documentOperations.unmatched)}. \`candidates\` counts the payments that fit equally well (pick one with \`do documents link-payment\`). A payment on an account you can't see doesn't count.`
41093
42198
  },
41094
42199
  {
41095
42200
  path: ["documents", "match"],
41096
42201
  summary: "Match a paper (or, without an id, every paper still waiting) to the payment that paid it.",
41097
42202
  args: [{ name: "id", required: false, description: "The paper; left out, all of them." }],
41098
42203
  flags: [],
41099
- scopes: ["documents:read", "finance:write"],
42204
+ scopes: [...documentOperations.matchOne.scopes],
41100
42205
  returns: "PaperMatchResult",
41101
- notes: "POST /api/v1/documents/:id/match or /api/v1/documents/match. A payment is money out of exactly the amount due, within the paper's window, with its structured reference or to its IBAN. Status: matched, already, ambiguous, none or not_payable."
42206
+ notes: `${routeOf(documentOperations.matchOne)} or ${PUBLIC_API_PREFIX}${documentOperations.matchAll.path}. A payment is money out of exactly the amount due, within the paper's window, with its structured reference or to its IBAN. Status: matched, already, ambiguous, none or not_payable.`
41102
42207
  },
41103
42208
  {
41104
42209
  path: ["documents", "payments"],
41105
42210
  summary: "Show the payments linked to a paper.",
41106
42211
  args: [{ name: "id", required: true, description: "The paper (document id)." }],
41107
42212
  flags: [],
41108
- scopes: ["documents:read", "finance:read"],
42213
+ scopes: [...documentOperations.payments.scopes],
41109
42214
  returns: "DocumentPayment",
41110
- notes: "GET /api/v1/documents/:id/payments."
42215
+ notes: `${routeOf(documentOperations.payments)}.`
41111
42216
  },
41112
42217
  {
41113
42218
  path: ["documents", "link-payment"],
@@ -41117,9 +42222,9 @@ var COMMAND_SPECS = [
41117
42222
  { name: "transactionId", required: true, description: "The Money transaction." }
41118
42223
  ],
41119
42224
  flags: [],
41120
- scopes: ["documents:read", "finance:write"],
42225
+ scopes: [...documentOperations.linkPayment.scopes],
41121
42226
  returns: "DocumentPayment",
41122
- notes: "PUT /api/v1/documents/:id/payments with { transactionId }."
42227
+ notes: `${routeOf(documentOperations.linkPayment)} with { transactionId }.`
41123
42228
  },
41124
42229
  {
41125
42230
  path: ["documents", "unlink-payment"],
@@ -41129,9 +42234,9 @@ var COMMAND_SPECS = [
41129
42234
  { name: "transactionId", required: true, description: "The Money transaction." }
41130
42235
  ],
41131
42236
  flags: [],
41132
- scopes: ["documents:read", "finance:write"],
42237
+ scopes: [...documentOperations.unlinkPayment.scopes],
41133
42238
  returns: "{ ok, unlinked }",
41134
- notes: "DELETE /api/v1/documents/:id/payments/:transactionId."
42239
+ notes: `${routeOf(documentOperations.unlinkPayment)}.`
41135
42240
  },
41136
42241
  {
41137
42242
  path: ["incidents", "open"],
@@ -43874,7 +44979,7 @@ var COMMAND_SPECS = [
43874
44979
  description: "Which grandparents to climb to: father or mother."
43875
44980
  }
43876
44981
  ],
43877
- scopes: ["people:family-tree"],
44982
+ scopes: [...familyTreeOperations.get.scopes],
43878
44983
  returns: "FamilyTreeResponse",
43879
44984
  notes: "people:family-tree is sensitive: never part of a full-access key, opened in Settings for one key, and only your own tree. { tree: null } before an import."
43880
44985
  },
@@ -43898,7 +45003,7 @@ var COMMAND_SPECS = [
43898
45003
  { name: "filter", type: "string", description: "all (default), living or linked." },
43899
45004
  { name: "limit", type: "string", description: "At most this many (1\u2013500, default 100)." }
43900
45005
  ],
43901
- scopes: ["people:family-tree"],
45006
+ scopes: [...familyTreeOperations.people.scopes],
43902
45007
  returns: "FamilyTreePeople"
43903
45008
  },
43904
45009
  {
@@ -43906,7 +45011,7 @@ var COMMAND_SPECS = [
43906
45011
  summary: "Say which person in the tree is you.",
43907
45012
  args: [{ name: "personId", required: true, description: "Your ref in the tree (I12)." }],
43908
45013
  flags: [],
43909
- scopes: ["people:family-tree"],
45014
+ scopes: [...familyTreeOperations.me.scopes],
43910
45015
  returns: "FamilyTreeView",
43911
45016
  notes: "The person becomes the root of the branch, and a living one is linked to your own Contact (the one that is you)."
43912
45017
  },
@@ -43922,7 +45027,7 @@ var COMMAND_SPECS = [
43922
45027
  description: "Say no to this match id (never offered again)."
43923
45028
  }
43924
45029
  ],
43925
- scopes: ["people:family-tree"],
45030
+ scopes: [...familyTreeOperations.matches.scopes],
43926
45031
  returns: "FamilyTreeMatches",
43927
45032
  notes: "Without a flag it lists the matches: suggest (a relative who looks like a Contact), birthday_differs, not_in_tree. Yes to a suggest links them; yes to birthday_differs takes the tree's day; not_in_tree only takes no (the fix goes to who sent the tree). Ask the member before either."
43928
45033
  },
@@ -46252,24 +47357,24 @@ var HANDLERS = {
46252
47357
  return { data: await requireClient().upload("/documents/file", form), render: renderRecord };
46253
47358
  },
46254
47359
  "documents filed": async ({ flags, requireClient }) => ({
46255
- data: await requireClient().get("/documents/filed", {
47360
+ data: await requireClient().get(pathOf(documentOperations.filed), {
46256
47361
  query: { source: requireFlag(flags, "source", "documents filed needs --source gmail:<id>") }
46257
47362
  }),
46258
47363
  render: renderRecord
46259
47364
  }),
46260
47365
  "documents doccle status": async ({ requireClient }) => ({
46261
- data: await requireClient().get("/documents/doccle"),
47366
+ data: await requireClient().get(pathOf(documentOperations.doccleStatus)),
46262
47367
  render: renderRecord
46263
47368
  }),
46264
47369
  "documents doccle sync": async ({ requireClient }) => ({
46265
- data: await requireClient().post("/documents/doccle/sync", {}),
47370
+ data: await requireClient().post(pathOf(documentOperations.doccleSync), {}),
46266
47371
  render: renderRecord
46267
47372
  }),
46268
47373
  "documents doccle connect": (ctx) => connectDoccle(ctx),
46269
47374
  "documents doccle disconnect": async ({ flags, requireClient }) => {
46270
47375
  assertDestructiveAllowed(flags, "forget the Doccle login");
46271
47376
  return {
46272
- data: await requireClient().delete("/documents/doccle/login"),
47377
+ data: await requireClient().delete(pathOf(documentOperations.doccleDisconnect)),
46273
47378
  render: renderRecord
46274
47379
  };
46275
47380
  },
@@ -46278,7 +47383,7 @@ var HANDLERS = {
46278
47383
  if (archive !== "on" && archive !== "off")
46279
47384
  throw usageError("--archive takes on or off");
46280
47385
  return {
46281
- data: await requireClient().patch("/documents/doccle", {
47386
+ data: await requireClient().patch(pathOf(documentOperations.doccleSettings), {
46282
47387
  archiveAfterImport: archive === "on"
46283
47388
  }),
46284
47389
  render: renderRecord
@@ -46294,7 +47399,10 @@ var HANDLERS = {
46294
47399
  documentId: flagString(flags, "document"),
46295
47400
  limit: limit === undefined ? undefined : Number(limit)
46296
47401
  }).filter(([, value]) => value !== undefined));
46297
- return { data: await requireClient().post("/documents/ask", body), render: renderRecord };
47402
+ return {
47403
+ data: await requireClient().post(pathOf(documentOperations.ask), body),
47404
+ render: renderRecord
47405
+ };
46298
47406
  },
46299
47407
  "documents search": async ({ args, flags, requireClient }) => {
46300
47408
  const q = args.join(" ").trim();
@@ -46306,28 +47414,30 @@ var HANDLERS = {
46306
47414
  limit: flagString(flags, "limit")
46307
47415
  }).filter(([, value]) => value !== undefined));
46308
47416
  return {
46309
- data: await requireClient().get("/documents/search", { query }),
47417
+ data: await requireClient().get(pathOf(documentOperations.search), { query }),
46310
47418
  render: renderRecord
46311
47419
  };
46312
47420
  },
46313
47421
  "documents unmatched": async ({ requireClient }) => ({
46314
- data: await requireClient().get("/documents/unmatched"),
47422
+ data: await requireClient().get(pathOf(documentOperations.unmatched)),
46315
47423
  render: renderList
46316
47424
  }),
46317
47425
  "documents match": async ({ args, requireClient }) => {
46318
47426
  const id = args[0];
46319
- const path = id ? `/documents/${id}/match` : "/documents/match";
47427
+ const path = id ? pathOf(documentOperations.matchOne, { id }) : pathOf(documentOperations.matchAll);
46320
47428
  return { data: await requireClient().post(path, {}), render: id ? renderRecord : renderList };
46321
47429
  },
46322
47430
  "documents payments": async ({ args, requireClient }) => ({
46323
- data: await requireClient().get(`/documents/${requireArg(args, 0, "document id")}/payments`),
47431
+ data: await requireClient().get(pathOf(documentOperations.payments, { id: requireArg(args, 0, "document id") })),
46324
47432
  render: renderList
46325
47433
  }),
46326
47434
  "documents link-payment": async ({ args, requireClient }) => {
46327
47435
  const id = requireArg(args, 0, "document id");
46328
47436
  const transactionId = requireArg(args, 1, "transaction id");
46329
47437
  return {
46330
- data: await requireClient().put(`/documents/${id}/payments`, { transactionId }),
47438
+ data: await requireClient().put(pathOf(documentOperations.linkPayment, { id }), {
47439
+ transactionId
47440
+ }),
46331
47441
  render: renderList
46332
47442
  };
46333
47443
  },
@@ -46335,7 +47445,7 @@ var HANDLERS = {
46335
47445
  const id = requireArg(args, 0, "document id");
46336
47446
  const transactionId = requireArg(args, 1, "transaction id");
46337
47447
  assertDestructiveAllowed(flags, `unlink payment ${transactionId} from ${id}`);
46338
- await requireClient().delete(`/documents/${id}/payments/${transactionId}`);
47448
+ await requireClient().delete(pathOf(documentOperations.unlinkPayment, { id, transactionId }));
46339
47449
  return { data: { ok: true, unlinked: transactionId }, render: renderRecord };
46340
47450
  },
46341
47451
  "incidents open": async ({ args, flags, requireClient }) => {
@@ -48338,18 +49448,24 @@ var HANDLERS = {
48338
49448
  ...healthRecordHandlers(),
48339
49449
  "health record": async ({ flags, requireClient }) => {
48340
49450
  const body = parseInput(logHealthInputSchema, readDataFlag(flags));
48341
- return { data: await requireClient().post("/health/log", body), render: renderRecord };
49451
+ return {
49452
+ data: await requireClient().post(pathOf(healthOperations.log), body),
49453
+ render: renderRecord
49454
+ };
48342
49455
  },
48343
49456
  "health fix": async ({ args, flags, requireClient }) => {
48344
49457
  const entryId = requireArg(args, 0, "entry");
48345
49458
  const body = parseInput(correctHealthInputSchema, { entryId, change: readDataFlag(flags) });
48346
- return { data: await requireClient().post("/health/correct", body), render: renderRecord };
49459
+ return {
49460
+ data: await requireClient().post(pathOf(healthOperations.correct), body),
49461
+ render: renderRecord
49462
+ };
48347
49463
  },
48348
49464
  "health check-in": async ({ args, requireClient }) => {
48349
49465
  const episodeId = requireArg(args, 0, "episode");
48350
49466
  const body = parseInput(healthCheckInInputSchema, { trend: requireArg(args, 1, "trend") });
48351
49467
  return {
48352
- data: await requireClient().post(`/health/episodes/${episodeId}/check-in`, body),
49468
+ data: await requireClient().post(pathOf(healthOperations.checkIn, { id: episodeId }), body),
48353
49469
  render: renderRecord
48354
49470
  };
48355
49471
  },
@@ -48366,7 +49482,7 @@ var HANDLERS = {
48366
49482
  ...thread ? { thread: { slug: thread } } : {}
48367
49483
  });
48368
49484
  return {
48369
- data: await requireClient().post(`/health/episodes/${episodeId}/red-flags`, body),
49485
+ data: await requireClient().post(pathOf(healthOperations.redFlags, { id: episodeId }), body),
48370
49486
  render: renderRecord
48371
49487
  };
48372
49488
  },
@@ -48375,37 +49491,40 @@ var HANDLERS = {
48375
49491
  kind: requireArg(args, 0, "kind"),
48376
49492
  id: requireArg(args, 1, "id")
48377
49493
  });
48378
- return { data: await requireClient().post("/health/undo", body), render: renderRecord };
49494
+ return {
49495
+ data: await requireClient().post(pathOf(healthOperations.undo), body),
49496
+ render: renderRecord
49497
+ };
48379
49498
  },
48380
49499
  "health find": async ({ flags, requireClient }) => {
48381
49500
  const body = parseInput(upsertHealthEpisodeInputSchema, readDataFlag(flags));
48382
49501
  return {
48383
- data: await requireClient().post("/health/episodes/upsert", body),
49502
+ data: await requireClient().post(pathOf(healthOperations.upsertEpisode), body),
48384
49503
  render: renderRecord
48385
49504
  };
48386
49505
  },
48387
49506
  "health medical-id": async ({ requireClient }) => ({
48388
- data: await requireClient().get("/health/medical-id"),
49507
+ data: await requireClient().get(pathOf(healthOperations.medicalId)),
48389
49508
  render: renderRecord
48390
49509
  }),
48391
49510
  "health summary": async ({ flags, requireClient }) => ({
48392
- data: await requireClient().get("/health/summary", {
49511
+ data: await requireClient().get(pathOf(healthOperations.summary), {
48393
49512
  query: { episodeId: flagString(flags, "episode") }
48394
49513
  }),
48395
49514
  render: renderRecord
48396
49515
  }),
48397
49516
  "health export": async ({ flags, requireClient }) => {
48398
- const raw = await requireClient().getText("/health/export", {
49517
+ const raw = await requireClient().getText(pathOf(healthOperations.export), {
48399
49518
  query: { files: flagBool(flags, "no-files") ? "false" : undefined }
48400
49519
  });
48401
49520
  return { raw, data: undefined, render: () => raw };
48402
49521
  },
48403
49522
  "health shares": async ({ requireClient }) => ({
48404
- data: await requireClient().getPage("/health/shares"),
49523
+ data: await requireClient().getPage(pathOf(healthOperations.listShares)),
48405
49524
  render: renderList
48406
49525
  }),
48407
49526
  "health share revoke": async ({ args, requireClient }) => ({
48408
- data: await requireClient().delete(`/health/shares/${requireArg(args, 0, "id")}`),
49527
+ data: await requireClient().delete(pathOf(healthOperations.revokeShare, { id: requireArg(args, 0, "id") })),
48409
49528
  render: renderRecord
48410
49529
  }),
48411
49530
  "health withings status": async ({ requireClient }) => ({
@@ -48425,11 +49544,11 @@ var HANDLERS = {
48425
49544
  render: renderRecord
48426
49545
  }),
48427
49546
  "health consent": async ({ requireClient }) => ({
48428
- data: await requireClient().get("/health/consent"),
49547
+ data: await requireClient().get(pathOf(healthOperations.getConsent)),
48429
49548
  render: renderRecord
48430
49549
  }),
48431
49550
  "health profile": async ({ requireClient }) => ({
48432
- data: await requireClient().get("/health/profile"),
49551
+ data: await requireClient().get(pathOf(healthOperations.getProfile)),
48433
49552
  render: renderRecord
48434
49553
  }),
48435
49554
  "health profile set": async ({ flags, requireClient }) => {
@@ -48441,7 +49560,10 @@ var HANDLERS = {
48441
49560
  if (sex !== undefined)
48442
49561
  fields.sex = sex.trim() ? sex.trim() : null;
48443
49562
  const body = parseInput(updateHealthProfileInputSchema, fields);
48444
- return { data: await requireClient().patch("/health/profile", body), render: renderRecord };
49563
+ return {
49564
+ data: await requireClient().patch(pathOf(healthOperations.updateProfile), body),
49565
+ render: renderRecord
49566
+ };
48445
49567
  },
48446
49568
  "health log": async ({ args, flags, requireClient }) => {
48447
49569
  const episode = requireArg(args, 0, "episode");
@@ -48460,7 +49582,7 @@ var HANDLERS = {
48460
49582
  };
48461
49583
  const body = parseInput(logHealthSymptomInputSchema, { id: ulid3(), ...raw });
48462
49584
  return {
48463
- data: await requireClient().post(`/health/episodes/${episode}/entries`, body),
49585
+ data: await requireClient().post(pathOf(healthOperations.logSymptom, { id: episode }), body),
48464
49586
  render: renderRecord
48465
49587
  };
48466
49588
  },
@@ -48472,14 +49594,14 @@ var HANDLERS = {
48472
49594
  ...readDataFlag(flags)
48473
49595
  });
48474
49596
  return {
48475
- data: await requireClient().post(`/health/episodes/${episode}/entries/${entry}/correct`, body),
49597
+ data: await requireClient().post(pathOf(healthOperations.correctSymptom, { id: episode, entryId: entry }), body),
48476
49598
  render: renderRecord
48477
49599
  };
48478
49600
  },
48479
49601
  "health unlog": async ({ args, requireClient }) => {
48480
49602
  const episode = requireArg(args, 0, "episode");
48481
49603
  const entry = requireArg(args, 1, "entry");
48482
- await requireClient().delete(`/health/episodes/${episode}/entries/${entry}`);
49604
+ await requireClient().delete(pathOf(healthOperations.deleteSymptom, { id: episode, entryId: entry }));
48483
49605
  return { data: { ok: true, deleted: entry }, render: renderRecord };
48484
49606
  },
48485
49607
  "health lab import": async ({ args, flags, requireClient }) => {
@@ -48506,14 +49628,17 @@ var HANDLERS = {
48506
49628
  };
48507
49629
  },
48508
49630
  "health labs analytes": async ({ requireClient }) => ({
48509
- data: await requireClient().get("/health/labs/analytes"),
49631
+ data: await requireClient().get(pathOf(healthOperations.listLabAnalytes)),
48510
49632
  render: renderList
48511
49633
  }),
48512
49634
  "health labs normalize": async ({ flags, requireClient }) => {
48513
49635
  if (!flagBool(flags, "dry-run")) {
48514
49636
  throw usageError("health labs normalize only reports for now: pass --dry-run (stored keys aren't rewritten)");
48515
49637
  }
48516
- return { data: await requireClient().get("/health/labs/normalize"), render: renderRecord };
49638
+ return {
49639
+ data: await requireClient().get(pathOf(healthOperations.labNormalizeReport)),
49640
+ render: renderRecord
49641
+ };
48517
49642
  },
48518
49643
  "health conditions care-team add": async ({ args, flags, requireClient }) => {
48519
49644
  const condition = requireArg(args, 0, "condition");
@@ -48523,7 +49648,7 @@ var HANDLERS = {
48523
49648
  ...label !== undefined ? { label } : {}
48524
49649
  });
48525
49650
  return {
48526
- data: await requireClient().post(`/health/conditions/${condition}/care-team`, body),
49651
+ data: await requireClient().post(pathOf(healthOperations.addCareTeam, { id: condition }), body),
48527
49652
  render: renderRecord
48528
49653
  };
48529
49654
  },
@@ -48531,28 +49656,28 @@ var HANDLERS = {
48531
49656
  const condition = requireArg(args, 0, "condition");
48532
49657
  const contact = requireArg(args, 1, "contact");
48533
49658
  return {
48534
- data: await requireClient().delete(`/health/conditions/${condition}/care-team/${contact}`),
49659
+ data: await requireClient().delete(pathOf(healthOperations.removeCareTeam, { id: condition, contactId: contact })),
48535
49660
  render: renderRecord
48536
49661
  };
48537
49662
  },
48538
49663
  "health analytes": async ({ requireClient }) => ({
48539
- data: await requireClient().get("/health/analytes"),
49664
+ data: await requireClient().get(pathOf(healthOperations.labOverview)),
48540
49665
  render: renderRecord
48541
49666
  }),
48542
49667
  "health analyte": async ({ args, requireClient }) => ({
48543
- data: await requireClient().get(`/health/analytes/${requireArg(args, 0, "analyte")}`),
49668
+ data: await requireClient().get(pathOf(healthOperations.getLabAnalyte, { analyte: requireArg(args, 0, "analyte") })),
48544
49669
  render: renderRecord
48545
49670
  }),
48546
49671
  "health analyte set": async ({ args, flags, requireClient }) => {
48547
49672
  const analyte = requireArg(args, 0, "analyte");
48548
49673
  const body = parseInput(setHealthLabAnalyteInputSchema, readDataFlag(flags));
48549
49674
  return {
48550
- data: await requireClient().put(`/health/analytes/${analyte}`, body),
49675
+ data: await requireClient().put(pathOf(healthOperations.setLabAnalyte, { analyte }), body),
48551
49676
  render: renderRecord
48552
49677
  };
48553
49678
  },
48554
49679
  "health doses": async ({ args, requireClient }) => ({
48555
- data: await requireClient().getPage(`/health/medications/${requireArg(args, 0, "med")}/doses`),
49680
+ data: await requireClient().getPage(pathOf(healthOperations.listDoses, { id: requireArg(args, 0, "med") })),
48556
49681
  render: renderList
48557
49682
  }),
48558
49683
  "health dose": async ({ args, flags, requireClient }) => {
@@ -48567,7 +49692,7 @@ var HANDLERS = {
48567
49692
  };
48568
49693
  const body = parseInput(logHealthDoseInputSchema, { id: ulid3(), ...raw });
48569
49694
  return {
48570
- data: await requireClient().post(`/health/medications/${med}/doses`, body),
49695
+ data: await requireClient().post(pathOf(healthOperations.logDose, { id: med }), body),
48571
49696
  render: renderRecord
48572
49697
  };
48573
49698
  },
@@ -48576,14 +49701,14 @@ var HANDLERS = {
48576
49701
  const dose = requireArg(args, 1, "dose");
48577
49702
  const body = parseInput(updateHealthDoseInputSchema, readDataFlag(flags));
48578
49703
  return {
48579
- data: await requireClient().patch(`/health/medications/${med}/doses/${dose}`, body),
49704
+ data: await requireClient().patch(pathOf(healthOperations.updateDose, { id: med, doseId: dose }), body),
48580
49705
  render: renderRecord
48581
49706
  };
48582
49707
  },
48583
49708
  "health undose": async ({ args, requireClient }) => {
48584
49709
  const med = requireArg(args, 0, "med");
48585
49710
  const dose = requireArg(args, 1, "dose");
48586
- await requireClient().delete(`/health/medications/${med}/doses/${dose}`);
49711
+ await requireClient().delete(pathOf(healthOperations.deleteDose, { id: med, doseId: dose }));
48587
49712
  return { data: { ok: true, deleted: dose }, render: renderRecord };
48588
49713
  },
48589
49714
  "places import": async ({ args, flags, requireClient }) => {
@@ -48762,7 +49887,9 @@ var HANDLERS = {
48762
49887
  const side = flagString(flags, "side");
48763
49888
  parseInput(familyTreeQuerySchema, side ? { side } : {});
48764
49889
  return {
48765
- data: await requireClient().get("/family-tree", { query: side ? { side } : {} }),
49890
+ data: await requireClient().get(pathOf(familyTreeOperations.get), {
49891
+ query: side ? { side } : {}
49892
+ }),
48766
49893
  render: renderRecord
48767
49894
  };
48768
49895
  },
@@ -48798,13 +49925,16 @@ var HANDLERS = {
48798
49925
  };
48799
49926
  parseInput(familyTreePeopleQuerySchema, query);
48800
49927
  return {
48801
- data: await requireClient().get("/family-tree/people", { query }),
49928
+ data: await requireClient().get(pathOf(familyTreeOperations.people), { query }),
48802
49929
  render: renderRecord
48803
49930
  };
48804
49931
  },
48805
49932
  "people tree me": async ({ args, requireClient }) => {
48806
49933
  const body = parseInput(familyTreeMeInputSchema, { ref: requireArg(args, 0, "person id") });
48807
- return { data: await requireClient().put("/family-tree/me", body), render: renderRecord };
49934
+ return {
49935
+ data: await requireClient().put(pathOf(familyTreeOperations.me), body),
49936
+ render: renderRecord
49937
+ };
48808
49938
  },
48809
49939
  "people tree matches": async ({ flags, requireClient }) => {
48810
49940
  const accept = flagString(flags, "accept");
@@ -48813,13 +49943,16 @@ var HANDLERS = {
48813
49943
  throw usageError("--accept and --reject can't be combined");
48814
49944
  const client = requireClient();
48815
49945
  if (!accept && !reject) {
48816
- return { data: await client.get("/family-tree/matches"), render: renderRecord };
49946
+ return { data: await client.get(pathOf(familyTreeOperations.matches)), render: renderRecord };
48817
49947
  }
48818
49948
  const body = parseInput(familyTreeMatchDecisionInputSchema, {
48819
49949
  id: accept ?? reject,
48820
49950
  decision: accept ? "accept" : "reject"
48821
49951
  });
48822
- return { data: await client.post("/family-tree/matches", body), render: renderRecord };
49952
+ return {
49953
+ data: await client.post(pathOf(familyTreeOperations.decide), body),
49954
+ render: renderRecord
49955
+ };
48823
49956
  },
48824
49957
  "guest-lists overview": async ({ args, requireClient }) => {
48825
49958
  const id = requireArg(args, 0, "guest list id");
@@ -49377,17 +50510,20 @@ function healthRecordHandlers() {
49377
50510
  lab: "labs"
49378
50511
  };
49379
50512
  const out = {};
49380
- for (const kind of Object.keys(HEALTH_RECORD_PATHS)) {
49381
- const path = HEALTH_RECORD_PATHS[kind];
50513
+ for (const kind of Object.keys(HEALTH_RECORD_OPERATIONS)) {
50514
+ const ops = HEALTH_RECORD_OPERATIONS[kind];
49382
50515
  const schemas = schemasOf[kind];
49383
50516
  out[`health ${plurals[kind]}`] = async ({ flags, requireClient }) => {
49384
50517
  const query = {};
49385
50518
  for (const name of schemas.list ?? [])
49386
50519
  query[name] = flagString(flags, name);
49387
- return { data: await requireClient().getPage(path, { query }), render: renderList };
50520
+ return {
50521
+ data: await requireClient().getPage(pathOf(ops.list), { query }),
50522
+ render: renderList
50523
+ };
49388
50524
  };
49389
50525
  out[`health ${kind}`] = async ({ args, requireClient }) => ({
49390
- data: await requireClient().get(`${path}/${requireArg(args, 0, "id")}`),
50526
+ data: await requireClient().get(pathOf(ops.get, { id: requireArg(args, 0, "id") })),
49391
50527
  render: renderRecord
49392
50528
  });
49393
50529
  out[`health ${kind} add`] = async ({ flags, requireClient }) => {
@@ -49396,17 +50532,20 @@ function healthRecordHandlers() {
49396
50532
  ...readDataFlag(flags),
49397
50533
  ...schemas.addFlags?.(flags)
49398
50534
  });
49399
- return { data: await requireClient().post(path, body), render: renderRecord };
50535
+ return { data: await requireClient().post(pathOf(ops.create), body), render: renderRecord };
49400
50536
  };
49401
50537
  out[`health ${kind} update`] = async ({ args, flags, requireClient }) => {
49402
50538
  const id = requireArg(args, 0, "id");
49403
50539
  const body = parseInput(schemas.update, readDataFlag(flags));
49404
- return { data: await requireClient().patch(`${path}/${id}`, body), render: renderRecord };
50540
+ return {
50541
+ data: await requireClient().patch(pathOf(ops.update, { id }), body),
50542
+ render: renderRecord
50543
+ };
49405
50544
  };
49406
50545
  out[`health ${kind} delete`] = async ({ args, flags, requireClient }) => {
49407
50546
  const id = requireArg(args, 0, "id");
49408
50547
  assertDestructiveAllowed(flags, `delete health ${kind} ${id}`);
49409
- await requireClient().delete(`${path}/${id}`);
50548
+ await requireClient().delete(pathOf(ops.remove, { id }));
49410
50549
  return { data: { ok: true, deleted: id, softDelete: true }, render: renderRecord };
49411
50550
  };
49412
50551
  }