@robbeverhelst/do 1.302.0 → 1.304.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 +1320 -183
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -4949,6 +4949,20 @@ var moneyRecurring = pgTable("money_recurring", {
4949
4949
  check("money_recurring_amount", sql`${t.expectedAmount} > 0`),
4950
4950
  check("money_recurring_business_share", sql`${t.businessShare} BETWEEN 0 AND 100`)
4951
4951
  ]);
4952
+ var moneySnapshotSource = pgEnum("money_snapshot_source", ["balance", "ledger", "manual"]);
4953
+ var moneySnapshots = pgTable("money_snapshots", {
4954
+ id: text("id").primaryKey(),
4955
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
4956
+ accountId: text("account_id").notNull().references(() => moneyAccounts.id, { onDelete: "cascade" }),
4957
+ month: char("month", { length: 7 }).notNull(),
4958
+ value: numeric("value", { precision: 14, scale: 2 }).notNull(),
4959
+ source: moneySnapshotSource("source").notNull().default("balance"),
4960
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
4961
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
4962
+ }, (t) => [
4963
+ unique("money_snapshots_account_month_key").on(t.tenantId, t.accountId, t.month),
4964
+ check("money_snapshots_month", sql`${t.month} ~ '^[0-9]{4}-(0[1-9]|1[0-2])$'`)
4965
+ ]);
4952
4966
 
4953
4967
  // ../../packages/schema/src/db/money-connections.ts
4954
4968
  var moneyConnectionStatus = pgEnum("money_connection_status", [
@@ -5174,6 +5188,8 @@ var guestLists = pgTable("guest_lists", {
5174
5188
  finalInfoKeyDateId: text("final_info_key_date_id").references(() => keyDates.id, {
5175
5189
  onDelete: "set null"
5176
5190
  }),
5191
+ siteUrl: text("site_url"),
5192
+ sitePassword: text("site_password"),
5177
5193
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5178
5194
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5179
5195
  }, (t) => [uniqueIndex("guest_lists_project_uniq").on(t.tenantId, t.projectId)]);
@@ -5630,6 +5646,18 @@ var foodWeekReads = pgTable("food_week_reads", {
5630
5646
  weekStart: text("week_start").notNull()
5631
5647
  }, (t) => [unique("food_week_reads_owner_week_uniq").on(t.tenantId, t.ownerUserId, t.weekStart)]);
5632
5648
 
5649
+ // ../../packages/schema/src/db/webhooks.ts
5650
+ var webhookSubscriptions = pgTable("webhook_subscriptions", {
5651
+ id: text("id").primaryKey(),
5652
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
5653
+ url: text("url").notNull(),
5654
+ events: text("events").array().notNull().default([]),
5655
+ secret: text("secret").notNull(),
5656
+ active: boolean("active").notNull().default(true),
5657
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5658
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5659
+ }, (t) => [index("webhook_subscriptions_tenant_idx").on(t.tenantId)]);
5660
+
5633
5661
  // ../../packages/schema/src/db/collaboration.ts
5634
5662
  var capability = pgEnum("collab_capability", ["view", "comment", "edit", "admin"]);
5635
5663
  var grantSubjectType = pgEnum("grant_subject_type", ["member", "role"]);
@@ -19912,6 +19940,12 @@ var linkGoogleInputSchema = object({
19912
19940
  state: string2().min(1)
19913
19941
  });
19914
19942
 
19943
+ // ../../packages/schema/src/contracts/pagination.ts
19944
+ var paginationQuerySchema = object({
19945
+ limit: exports_coerce.number().int().positive().optional(),
19946
+ cursor: string2().min(1).optional()
19947
+ });
19948
+
19915
19949
  // ../../packages/schema/src/contracts/changes.ts
19916
19950
  var changeOpSchema = _enum2(["created", "updated", "deleted"]);
19917
19951
  var CHANGE_ENTITY_SCOPES = {
@@ -20501,6 +20535,21 @@ var collectionCoverBackfillResultSchema = object({
20501
20535
  found: number2().int().min(0),
20502
20536
  colored: number2().int().min(0).default(0)
20503
20537
  });
20538
+ function normalizeTag(raw) {
20539
+ return raw.trim().toLowerCase().replace(/\s+/g, " ");
20540
+ }
20541
+ function normalizeTags(raw) {
20542
+ const seen = new Set;
20543
+ const out = [];
20544
+ for (const r of raw) {
20545
+ const tag = normalizeTag(r);
20546
+ if (!tag || seen.has(tag))
20547
+ continue;
20548
+ seen.add(tag);
20549
+ out.push(tag);
20550
+ }
20551
+ return out;
20552
+ }
20504
20553
  var collectionSavedViewSchema = object({
20505
20554
  id: string2().min(1),
20506
20555
  name: string2().min(1),
@@ -20539,6 +20588,53 @@ var collectionLogResultSchema = object({
20539
20588
  item: collectionItemDetailSchema
20540
20589
  });
20541
20590
 
20591
+ // ../../packages/schema/src/contracts/list-filters.ts
20592
+ function csvList(item) {
20593
+ return preprocess((raw) => (Array.isArray(raw) ? raw : [raw]).flatMap((v) => typeof v === "string" ? v.split(",") : [v]).map((v) => typeof v === "string" ? v.trim() : v).filter((v) => v !== ""), array(item).min(1));
20594
+ }
20595
+ var booleanQuerySchema = _enum2(["true", "false", "1", "0"]).transform((v) => v === "true" || v === "1");
20596
+ var instantQuerySchema = union([exports_iso.datetime({ offset: true }), exports_iso.date()]).transform((s) => new Date(s));
20597
+ var tagListSchema = csvList(string2().min(1)).transform((tags, ctx) => {
20598
+ const normalized = normalizeTags(tags.map((t) => t.replace(/^#/, "")));
20599
+ if (normalized.length === 0) {
20600
+ ctx.addIssue({ code: "custom", message: "must name at least one tag" });
20601
+ return NEVER;
20602
+ }
20603
+ return normalized;
20604
+ });
20605
+ function partyFilterSchema(party) {
20606
+ return union([_enum2(["any", "none"]), party]);
20607
+ }
20608
+ var taskListQuerySchema = object({
20609
+ areaId: ulidSchema.optional(),
20610
+ projectId: ulidSchema.optional(),
20611
+ status: csvList(taskStatusSchema).optional(),
20612
+ tag: tagListSchema.optional(),
20613
+ updatedSince: instantQuerySchema.optional(),
20614
+ dueBefore: instantQuerySchema.optional(),
20615
+ owner: partyFilterSchema(taskOwnerInputSchema).optional(),
20616
+ waitingOn: partyFilterSchema(taskWaitingOnInputSchema).optional(),
20617
+ completedSince: instantQuerySchema.optional(),
20618
+ completedBefore: instantQuerySchema.optional(),
20619
+ proposed: booleanQuerySchema.optional(),
20620
+ ready: booleanQuerySchema.optional(),
20621
+ drifted: booleanQuerySchema.optional()
20622
+ });
20623
+ var projectListQuerySchema = object({
20624
+ areaId: ulidSchema.optional(),
20625
+ status: csvList(projectStatusSchema).optional(),
20626
+ updatedSince: instantQuerySchema.optional(),
20627
+ dueBefore: instantQuerySchema.optional()
20628
+ });
20629
+ var habitListQuerySchema = object({
20630
+ areaId: ulidSchema.optional(),
20631
+ updatedSince: instantQuerySchema.optional(),
20632
+ retired: _enum2(["true", "false"]).transform((v) => v === "true").optional()
20633
+ });
20634
+ var areaListQuerySchema = object({
20635
+ archived: _enum2(["true", "false"]).transform((v) => v === "true").optional()
20636
+ });
20637
+
20542
20638
  // ../../packages/schema/src/contracts/habit-sources.ts
20543
20639
  var habitSourceSchema = _enum2(["manual", "google_fit", "strava", "github"]);
20544
20640
 
@@ -20770,7 +20866,8 @@ var metricSourceSchema = _enum2([
20770
20866
  "agent",
20771
20867
  "google_fit",
20772
20868
  "google_health",
20773
- "health_connect"
20869
+ "health_connect",
20870
+ "withings"
20774
20871
  ]);
20775
20872
  var loggedMetricSourceSchema = _enum2(["manual", "device", "agent"]);
20776
20873
  var metricPointSchema = object({
@@ -24656,6 +24753,50 @@ var dayRecordUndoResultSchema = object({
24656
24753
  });
24657
24754
  var STATUS_QUARTER_MS = RECORD_ROUND_MINUTES * 60000;
24658
24755
 
24756
+ // ../../packages/schema/src/contracts/today.ts
24757
+ var occurrenceViewSchema = occurrenceSchema.extend({
24758
+ title: string2(),
24759
+ templateKind: _enum2(["task", "habit", "routine"]),
24760
+ measurementType: habitMeasurementTypeSchema.nullable()
24761
+ });
24762
+ var todayResponseSchema = object({
24763
+ from: exports_coerce.date(),
24764
+ to: exports_coerce.date(),
24765
+ scheduledTasks: array(taskViewSchema),
24766
+ dueOccurrences: array(occurrenceViewSchema),
24767
+ overdueTasks: array(taskViewSchema),
24768
+ overdueOccurrences: array(occurrenceViewSchema)
24769
+ });
24770
+ var rolloverInputSchema = object({
24771
+ taskIds: array(string2().min(1)).default([]),
24772
+ occurrenceIds: array(string2().min(1)).default([]),
24773
+ action: _enum2(["reschedule", "someday", "drop"]),
24774
+ to: exports_coerce.date().optional()
24775
+ }).refine((v) => v.action !== "reschedule" || v.to !== undefined, {
24776
+ message: "`to` is required when action is `reschedule`",
24777
+ path: ["to"]
24778
+ });
24779
+ var rolloverResultSchema = object({
24780
+ tasks: number2().int().nonnegative(),
24781
+ occurrences: number2().int().nonnegative()
24782
+ });
24783
+ var fromEarlierQuerySchema = object({
24784
+ from: instantQuerySchema.optional(),
24785
+ areaId: ulidSchema.optional()
24786
+ });
24787
+ var fromEarlierResponseSchema = object({
24788
+ from: exports_coerce.date(),
24789
+ since: exports_coerce.date(),
24790
+ laterThisWeek: exports_coerce.date(),
24791
+ tasks: array(taskViewSchema)
24792
+ });
24793
+ var reviewDayStatSchema = object({
24794
+ date: string2(),
24795
+ completedTasks: number2().int().nonnegative(),
24796
+ completedOccurrences: number2().int().nonnegative()
24797
+ });
24798
+ var reviewStatsSchema = array(reviewDayStatSchema);
24799
+
24659
24800
  // ../../packages/schema/src/contracts/insights.ts
24660
24801
  var insightsActivityPointSchema = object({
24661
24802
  day: string2(),
@@ -27240,6 +27381,51 @@ var steamSyncResultSchema = object({
27240
27381
  games: array(steamGameSyncSchema)
27241
27382
  });
27242
27383
 
27384
+ // ../../packages/schema/src/contracts/money-net-worth.ts
27385
+ var amount5 = string2();
27386
+ var moneySnapshotSourceSchema = _enum2(moneySnapshotSource.enumValues);
27387
+ var moneyNetWorthQuerySchema = object({
27388
+ scope: moneyBudgetScopeSchema.optional(),
27389
+ months: exports_coerce.number().int().min(2).max(60).optional()
27390
+ });
27391
+ var moneyNetWorthPointSchema = object({
27392
+ month: moneyMonthSchema,
27393
+ total: amount5.nullable(),
27394
+ have: amount5.nullable(),
27395
+ owe: amount5.nullable()
27396
+ });
27397
+ var moneyNetWorthAccountSchema = object({
27398
+ accountId: ulidSchema,
27399
+ title: string2(),
27400
+ kind: moneyAccountKindSchema,
27401
+ visibility: _enum2(NODE_VISIBILITY_VALUES),
27402
+ currency: string2(),
27403
+ value: amount5,
27404
+ source: moneySnapshotSourceSchema,
27405
+ month: moneyMonthSchema
27406
+ });
27407
+ var moneyNetWorthSchema = object({
27408
+ scope: moneyBudgetScopeSchema,
27409
+ month: moneyMonthSchema,
27410
+ total: amount5,
27411
+ have: amount5,
27412
+ owe: amount5,
27413
+ points: array(moneyNetWorthPointSchema),
27414
+ accounts: array(moneyNetWorthAccountSchema)
27415
+ });
27416
+ var setMoneyNetWorthValueInputSchema = object({
27417
+ accountId: ulidSchema,
27418
+ value: moneyAmountSchema,
27419
+ month: moneyMonthSchema.optional()
27420
+ }).strict();
27421
+ var moneySnapshotSchema = object({
27422
+ id: ulidSchema,
27423
+ accountId: ulidSchema,
27424
+ month: moneyMonthSchema,
27425
+ value: amount5,
27426
+ source: moneySnapshotSourceSchema
27427
+ });
27428
+
27243
27429
  // ../../packages/schema/src/contracts/money-splits.ts
27244
27430
  var moneyPartySchema = union([
27245
27431
  object({ userId: ulidSchema }).strict(),
@@ -27700,6 +27886,7 @@ var guestListViewSchema = guestListSchema.extend({
27700
27886
  finalInfoFrom: keyDateDaySchema.nullable().default(null),
27701
27887
  projectDay: keyDateDaySchema.nullable().default(null)
27702
27888
  });
27889
+ var siteUrlSchema = string2().trim().max(200).regex(/^https:\/\/[^\s/?#]+(\/[^\s?#]*)?$/, "expected an https:// address without ? or #").transform((u) => u.replace(/\/+$/, ""));
27703
27890
  var createGuestListInputSchema = object({
27704
27891
  id: ulidSchema,
27705
27892
  projectId: ulidSchema,
@@ -27707,7 +27894,9 @@ var createGuestListInputSchema = object({
27707
27894
  caps: capsSchema.optional(),
27708
27895
  sides: array(sideSchema).max(4).optional(),
27709
27896
  rsvpBy: keyDateDaySchema.nullish(),
27710
- finalInfoFrom: keyDateDaySchema.nullish()
27897
+ finalInfoFrom: keyDateDaySchema.nullish(),
27898
+ siteUrl: siteUrlSchema.nullish(),
27899
+ sitePassword: string2().trim().min(1).max(64).nullish()
27711
27900
  }).strict();
27712
27901
  var updateGuestListInputSchema = createGuestListInputSchema.omit({ id: true, projectId: true }).partial().strict();
27713
27902
  var guestListsQuerySchema = object({ projectId: ulidSchema.optional() });
@@ -27859,6 +28048,23 @@ var publicRsvpEventSchema = object({
27859
28048
  dressCode: object({ nl: string2().optional(), pt: string2().optional() }).nullable(),
27860
28049
  note: object({ nl: string2().optional(), pt: string2().optional() }).nullable()
27861
28050
  }).strict();
28051
+ var publicTextSchema = object({ nl: string2().optional(), pt: string2().optional() });
28052
+ var publicRsvpQuestionSchema = object({
28053
+ id: ulidSchema,
28054
+ kind: rsvpQuestionKindSchema,
28055
+ label: publicTextSchema,
28056
+ options: array(object({ key: string2(), label: publicTextSchema }).strict()),
28057
+ scope: rsvpQuestionScopeSchema,
28058
+ eventKey: string2().nullable(),
28059
+ required: boolean3(),
28060
+ showIf: array(rsvpShowIfConditionSchema),
28061
+ preset: rsvpQuestionPresetSchema.nullable()
28062
+ }).strict();
28063
+ var publicRsvpAnswerSchema = object({
28064
+ questionId: ulidSchema,
28065
+ guestId: ulidSchema.nullable(),
28066
+ value: rsvpAnswerValueSchema
28067
+ }).strict();
27862
28068
  var publicRsvpViewSchema = object({
27863
28069
  mode: rsvpLinkModeSchema,
27864
28070
  language: rsvpLanguageSchema,
@@ -27868,6 +28074,8 @@ var publicRsvpViewSchema = object({
27868
28074
  finalInfoFrom: keyDateDaySchema.nullable().default(null),
27869
28075
  tiers: array(guestTierSchema),
27870
28076
  events: array(publicRsvpEventSchema).default([]),
28077
+ questions: array(publicRsvpQuestionSchema).default([]),
28078
+ answers: array(publicRsvpAnswerSchema).default([]),
27871
28079
  guests: array(publicRsvpGuestSchema),
27872
28080
  address: householdAddressSchema.nullable(),
27873
28081
  answeredAt: exports_coerce.date().nullable()
@@ -27953,7 +28161,15 @@ var createSeatingProposalInputSchema = object({
27953
28161
  return new Set(ids).size === ids.length;
27954
28162
  }, { message: "a guest can sit at one table only", path: ["tables"] });
27955
28163
  var seatingProposalsQuerySchema = object({ guestListId: ulidSchema });
27956
- var GUEST_EXPORT_KINDS = ["caterer", "venue", "floor-plan", "church", "answers"];
28164
+ var GUEST_EXPORT_KINDS = [
28165
+ "caterer",
28166
+ "venue",
28167
+ "floor-plan",
28168
+ "church",
28169
+ "answers",
28170
+ "invitations",
28171
+ "printer"
28172
+ ];
27957
28173
  var guestExportKindSchema = _enum2(GUEST_EXPORT_KINDS);
27958
28174
  var thankYouGiftSchema = object({
27959
28175
  momentId: ulidSchema,
@@ -28538,6 +28754,209 @@ function scopeGranted(granted, required) {
28538
28754
  return granted.includes(FULL_ACCESS_SCOPE);
28539
28755
  }
28540
28756
 
28757
+ // ../../packages/schema/src/net/ssrf.ts
28758
+ var BLOCKED_HOST_SUFFIXES = [".internal", ".local", ".localhost"];
28759
+ var BLOCKED_HOSTNAMES = new Set(["localhost"]);
28760
+ function parseIpv4(host) {
28761
+ const parts = host.split(".");
28762
+ if (parts.length !== 4)
28763
+ return null;
28764
+ let value = 0;
28765
+ for (const part of parts) {
28766
+ if (!/^\d{1,3}$/.test(part))
28767
+ return null;
28768
+ const octet = Number(part);
28769
+ if (octet > 255)
28770
+ return null;
28771
+ value = value * 256 + octet;
28772
+ }
28773
+ return value >>> 0;
28774
+ }
28775
+ function isBlockedIpv4(ip) {
28776
+ const inRange = (base, bits) => {
28777
+ const baseIp = parseIpv4(base);
28778
+ if (baseIp === null)
28779
+ return false;
28780
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
28781
+ return (ip & mask) === (baseIp & mask);
28782
+ };
28783
+ return inRange("0.0.0.0", 8) || inRange("10.0.0.0", 8) || inRange("100.64.0.0", 10) || inRange("127.0.0.0", 8) || inRange("169.254.0.0", 16) || inRange("172.16.0.0", 12) || inRange("192.0.0.0", 24) || inRange("192.0.2.0", 24) || inRange("192.168.0.0", 16) || inRange("198.18.0.0", 15) || inRange("224.0.0.0", 4) || inRange("240.0.0.0", 4);
28784
+ }
28785
+ function parseIpv6(host) {
28786
+ let text = host;
28787
+ const zone = text.indexOf("%");
28788
+ if (zone !== -1)
28789
+ text = text.slice(0, zone);
28790
+ if (!text.includes(":"))
28791
+ return null;
28792
+ const lastColon = text.lastIndexOf(":");
28793
+ const tail = text.slice(lastColon + 1);
28794
+ if (tail.includes(".")) {
28795
+ const v4 = parseIpv4(tail);
28796
+ if (v4 === null)
28797
+ return null;
28798
+ const hex = `${(v4 >>> 16 & 65535).toString(16)}:${(v4 & 65535).toString(16)}`;
28799
+ text = text.slice(0, lastColon + 1) + hex;
28800
+ }
28801
+ const halves = text.split("::");
28802
+ if (halves.length > 2)
28803
+ return null;
28804
+ const toGroups = (segment) => {
28805
+ if (segment === "")
28806
+ return [];
28807
+ const out = [];
28808
+ for (const g of segment.split(":")) {
28809
+ if (!/^[0-9a-fA-F]{1,4}$/.test(g))
28810
+ return null;
28811
+ out.push(Number.parseInt(g, 16));
28812
+ }
28813
+ return out;
28814
+ };
28815
+ const head = toGroups(halves[0] ?? "");
28816
+ if (head === null)
28817
+ return null;
28818
+ let groups;
28819
+ if (halves.length === 2) {
28820
+ const back = toGroups(halves[1] ?? "");
28821
+ if (back === null)
28822
+ return null;
28823
+ const fill = 8 - head.length - back.length;
28824
+ if (fill < 0)
28825
+ return null;
28826
+ groups = [...head, ...Array(fill).fill(0), ...back];
28827
+ } else {
28828
+ groups = head;
28829
+ }
28830
+ return groups.length === 8 ? groups : null;
28831
+ }
28832
+ function isBlockedIpv6(groups) {
28833
+ const isZero = groups.every((g) => g === 0);
28834
+ if (isZero)
28835
+ return true;
28836
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1)
28837
+ return true;
28838
+ const isV4Mapped = groups.slice(0, 5).every((g) => g === 0) && groups[5] === 65535;
28839
+ const isV4Compat = groups.slice(0, 6).every((g) => g === 0);
28840
+ if (isV4Mapped || isV4Compat) {
28841
+ const v4 = ((groups[6] ?? 0) << 16 | (groups[7] ?? 0)) >>> 0;
28842
+ return isBlockedIpv4(v4);
28843
+ }
28844
+ const first = groups[0] ?? 0;
28845
+ if ((first & 65024) === 64512)
28846
+ return true;
28847
+ if ((first & 65472) === 65152)
28848
+ return true;
28849
+ if ((first & 65280) === 65280)
28850
+ return true;
28851
+ return false;
28852
+ }
28853
+ function isBlockedIp(host) {
28854
+ const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
28855
+ const v4 = parseIpv4(bare);
28856
+ if (v4 !== null)
28857
+ return isBlockedIpv4(v4);
28858
+ const v6 = parseIpv6(bare);
28859
+ if (v6 !== null)
28860
+ return isBlockedIpv6(v6);
28861
+ return false;
28862
+ }
28863
+
28864
+ class BlockedWebhookUrlError extends Error {
28865
+ constructor(reason) {
28866
+ super(`webhook url rejected: ${reason}`);
28867
+ this.name = "BlockedWebhookUrlError";
28868
+ }
28869
+ }
28870
+ function bareHost(hostname) {
28871
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
28872
+ }
28873
+ function isInternalHost(hostname) {
28874
+ const host = hostname.toLowerCase();
28875
+ if (BLOCKED_HOSTNAMES.has(host) || BLOCKED_HOST_SUFFIXES.some((s) => host.endsWith(s))) {
28876
+ return true;
28877
+ }
28878
+ return isBlockedIp(host);
28879
+ }
28880
+ function assertSafeEgressUrl(rawUrl, policy) {
28881
+ let url;
28882
+ try {
28883
+ url = new URL(rawUrl);
28884
+ } catch {
28885
+ throw policy.fail("not a valid URL");
28886
+ }
28887
+ if (!policy.protocols.includes(url.protocol)) {
28888
+ const allowsHttp = policy.protocols.includes("http:");
28889
+ throw policy.fail(`only ${allowsHttp ? "http(s)" : "https"} URLs are allowed`);
28890
+ }
28891
+ if (url.username || url.password) {
28892
+ throw policy.fail("credentials in the URL are not allowed");
28893
+ }
28894
+ if (isInternalHost(url.hostname)) {
28895
+ const kind = isBlockedIp(url.hostname) ? "a private or reserved address" : "internal";
28896
+ throw policy.fail(`host '${url.hostname}' is ${kind}`);
28897
+ }
28898
+ return url;
28899
+ }
28900
+ function webhookUrlAllowlistFromEnv() {
28901
+ const env = globalThis.process?.env;
28902
+ const raw = env?.WEBHOOK_URL_ALLOWLIST;
28903
+ if (!raw)
28904
+ return [];
28905
+ return raw.split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
28906
+ }
28907
+ function isAllowlistedHost(hostname, allowedHosts) {
28908
+ const host = hostname.toLowerCase();
28909
+ const bare = bareHost(host);
28910
+ return allowedHosts.some((h) => h === host || h === bare);
28911
+ }
28912
+ function assertSafeWebhookUrl(rawUrl, opts = {}) {
28913
+ let url;
28914
+ try {
28915
+ url = new URL(rawUrl);
28916
+ } catch {
28917
+ throw new BlockedWebhookUrlError("not a valid URL");
28918
+ }
28919
+ const allowedHosts = opts.allowedHosts ?? webhookUrlAllowlistFromEnv();
28920
+ if (isAllowlistedHost(url.hostname, allowedHosts)) {
28921
+ return url;
28922
+ }
28923
+ return assertSafeEgressUrl(rawUrl, {
28924
+ protocols: ["https:"],
28925
+ fail: (reason) => new BlockedWebhookUrlError(reason)
28926
+ });
28927
+ }
28928
+
28929
+ // ../../packages/schema/src/contracts/webhooks.ts
28930
+ var webhookUrlSchema = string2().superRefine((value, ctx) => {
28931
+ try {
28932
+ assertSafeWebhookUrl(value);
28933
+ } catch (err) {
28934
+ ctx.addIssue({
28935
+ code: ZodIssueCode.custom,
28936
+ message: err instanceof BlockedWebhookUrlError ? err.message : "invalid webhook url"
28937
+ });
28938
+ }
28939
+ });
28940
+ var webhookEventSchema = string2().min(1).max(120).regex(/^[a-z0-9]+(\.[a-z0-9]+)+$/, "must be a dotted event name like 'task.created'");
28941
+ var webhookSubscriptionSchema = createSelectSchema(webhookSubscriptions, {
28942
+ events: array(webhookEventSchema),
28943
+ createdAt: exports_coerce.date(),
28944
+ updatedAt: exports_coerce.date()
28945
+ });
28946
+ var webhookSubscriptionPublicSchema = webhookSubscriptionSchema.omit({ secret: true });
28947
+ var createWebhookSubscriptionInputSchema = createInsertSchema(webhookSubscriptions, {
28948
+ url: webhookUrlSchema,
28949
+ events: array(webhookEventSchema).min(1),
28950
+ secret: string2().min(16).max(200).optional()
28951
+ }).omit({ id: true, tenantId: true, active: true, createdAt: true, updatedAt: true }).strict();
28952
+ var createWebhookSubscriptionResultSchema = webhookSubscriptionPublicSchema.extend({
28953
+ secret: string2()
28954
+ });
28955
+ var updateWebhookSubscriptionInputSchema = createUpdateSchema(webhookSubscriptions, {
28956
+ url: webhookUrlSchema.optional(),
28957
+ events: array(webhookEventSchema).min(1).optional()
28958
+ }).omit({ id: true, tenantId: true, secret: true, createdAt: true, updatedAt: true }).strict();
28959
+
28541
28960
  // ../../packages/schema/src/contracts/roles.ts
28542
28961
  var permissionSetSchema = createSelectSchema(permissionSets, {
28543
28962
  createdAt: exports_coerce.date(),
@@ -29603,10 +30022,592 @@ function routeOf(op) {
29603
30022
  return `${op.method.toUpperCase()} ${PUBLIC_API_PREFIX}${op.path}`;
29604
30023
  }
29605
30024
 
30025
+ // ../../packages/schema/src/operations/agents.ts
30026
+ var agentOperations = defineOperations({
30027
+ list: {
30028
+ method: "get",
30029
+ path: "/agents",
30030
+ scopes: ["agents:read"],
30031
+ tag: "Agents",
30032
+ summary: "List agents",
30033
+ description: "The Space's live Agents (`id`, `name`), oldest first, to resolve an Agent by name.",
30034
+ status: 200,
30035
+ response: { description: "The Space's Agents.", list: true }
30036
+ }
30037
+ });
30038
+
30039
+ // ../../packages/schema/src/operations/crud.ts
30040
+ function crudOperations(r) {
30041
+ const base = `/${r.segment}`;
30042
+ const one = `/${r.segment}/{id}`;
30043
+ const event = r.eventBase ?? r.noun.replace(/ /g, ".");
30044
+ return {
30045
+ list: {
30046
+ method: "get",
30047
+ path: base,
30048
+ scopes: [r.read],
30049
+ tag: r.tag,
30050
+ summary: `List ${r.noun}s`,
30051
+ description: `Every ${r.noun} in the key's Space${r.paginated ? ", newest first" : ""}.`,
30052
+ query: r.listQuery,
30053
+ querySchema: r.listQuerySchema,
30054
+ paginated: r.paginated,
30055
+ status: 200,
30056
+ response: { description: `The Space's ${r.noun}s.`, list: true, view: r.view }
30057
+ },
30058
+ get: {
30059
+ method: "get",
30060
+ path: one,
30061
+ scopes: [r.read],
30062
+ tag: r.tag,
30063
+ summary: `Get a ${r.noun}`,
30064
+ description: `Fetch one ${r.noun} by id.`,
30065
+ status: 200,
30066
+ response: { description: `The ${r.noun}.`, view: r.view },
30067
+ errors: { 404: true }
30068
+ },
30069
+ create: {
30070
+ method: "post",
30071
+ path: base,
30072
+ scopes: [r.write],
30073
+ tag: r.tag,
30074
+ summary: `Create a ${r.noun}`,
30075
+ description: `Create a ${r.noun}; fires the \`${event}.created\` webhook.`,
30076
+ body: r.create,
30077
+ status: 201,
30078
+ response: { description: `The created ${r.noun}.`, view: r.view },
30079
+ errors: { 400: true, 409: true },
30080
+ webhook: `${event}.created`
30081
+ },
30082
+ update: {
30083
+ method: "patch",
30084
+ path: one,
30085
+ scopes: [r.write],
30086
+ tag: r.tag,
30087
+ summary: `Update a ${r.noun}`,
30088
+ description: `Patch a ${r.noun}; fires the \`${event}.updated\` webhook.`,
30089
+ body: r.update,
30090
+ status: 200,
30091
+ response: { description: `The updated ${r.noun}.`, view: r.view },
30092
+ errors: { 400: true, 404: true, 409: true },
30093
+ webhook: `${event}.updated`
30094
+ },
30095
+ remove: {
30096
+ method: "delete",
30097
+ path: one,
30098
+ scopes: [r.write],
30099
+ tag: r.tag,
30100
+ summary: `Delete a ${r.noun}`,
30101
+ description: `Delete a ${r.noun}; fires the \`${event}.deleted\` webhook.`,
30102
+ status: 204,
30103
+ response: { description: "Deleted." },
30104
+ errors: { 404: true },
30105
+ webhook: `${event}.deleted`
30106
+ }
30107
+ };
30108
+ }
30109
+
30110
+ // ../../packages/schema/src/operations/block-templates.ts
30111
+ var crud = crudOperations({
30112
+ segment: "block-templates",
30113
+ tag: "Calendar",
30114
+ noun: "block template",
30115
+ read: "calendar:read",
30116
+ write: "calendar:write",
30117
+ create: createBlockTemplateInputSchema,
30118
+ update: updateBlockTemplateInputSchema,
30119
+ listQuery: []
30120
+ });
30121
+ var blockTemplateOperations = defineOperations({
30122
+ ...crud,
30123
+ create: { ...crud.create, webhook: undefined },
30124
+ update: { ...crud.update, webhook: undefined },
30125
+ remove: { ...crud.remove, webhook: undefined },
30126
+ week: {
30127
+ method: "get",
30128
+ path: "/block-templates/week",
30129
+ scopes: ["calendar:read"],
30130
+ tag: "Calendar",
30131
+ summary: "Lay out a week from the block templates",
30132
+ description: "Every life block the key's member's templates lay out for the week starting `start` (a Monday), in time order, on their wall clock: `{ templateId, lifeKind, title, startAt, endAt, routineId }`. Writes nothing; this is what Sunday planning proposes.",
30133
+ query: [{ name: "start", description: "The week's Monday (YYYY-MM-DD).", format: "date" }],
30134
+ querySchema: blockTemplateWeekQuerySchema,
30135
+ status: 200,
30136
+ response: { description: "The week's blocks." },
30137
+ errors: { 400: true }
30138
+ },
30139
+ defaults: {
30140
+ method: "post",
30141
+ path: "/block-templates/defaults",
30142
+ scopes: ["calendar:write"],
30143
+ tag: "Calendar",
30144
+ summary: "Seed the default week",
30145
+ description: "Give a member with no block templates the default week: sleep, the morning, reading, two work blocks, lunch and dinner with clean-up, a buffer, a free evening and a wind-down. A member who has templates keeps them; the call then returns them unchanged.",
30146
+ status: 200,
30147
+ response: { description: "The member's templates." }
30148
+ }
30149
+ });
30150
+
30151
+ // ../../packages/schema/src/operations/collection-upkeep.ts
30152
+ var collectionUpkeepOperations = defineOperations({
30153
+ list: {
30154
+ method: "get",
30155
+ path: "/collection/{id}/upkeep",
30156
+ scopes: ["collections:read"],
30157
+ tag: "Collection",
30158
+ summary: "List an asset's upkeep and repairs",
30159
+ description: "The Tasks that look after this asset: `upkeep` (`maintains` \u2014 usually recurring, e.g. every 4 weeks from the last time) and `repairs` (one-off fix-it Tasks), open ones first. 404 unless the item is a live `asset`. Returns `{ itemId, upkeep, repairs }`.",
30160
+ status: 200,
30161
+ response: { description: "The asset's Tasks." },
30162
+ errors: { 404: true }
30163
+ },
30164
+ link: {
30165
+ method: "post",
30166
+ path: "/collection/{id}/upkeep",
30167
+ scopes: ["collections:write"],
30168
+ tag: "Collection",
30169
+ summary: "Tie a Task to an asset",
30170
+ description: 'Tie a Task in the Space to this asset as its upkeep (`kind: "maintains"`, the default) or a repair (`"repairs"`). Make the Task (its recurrence, Place, Area) through the tasks API first. When a tied Task is finished \u2014 a one-off marked done, or one occurrence of a recurring one checked off \u2014 the asset gets a `service` / `repair` Entry with the Task on it, once per day, and upkeep moves its `lastServicedAt` forward. Returns the asset\'s Tasks.',
30171
+ body: linkAssetTaskInputSchema,
30172
+ status: 201,
30173
+ response: { description: "The asset's Tasks." },
30174
+ errors: { 400: true, 404: true }
30175
+ },
30176
+ unlink: {
30177
+ method: "delete",
30178
+ path: "/collection/{id}/upkeep/{taskId}",
30179
+ pathParams: { taskId: "The Task's ULID." },
30180
+ scopes: ["collections:write"],
30181
+ tag: "Collection",
30182
+ summary: "Untie a Task from an asset",
30183
+ description: "Untie the Task from this asset; the Task itself stays. Returns the asset's Tasks.",
30184
+ status: 200,
30185
+ response: { description: "The asset's Tasks." },
30186
+ errors: { 404: true }
30187
+ }
30188
+ });
30189
+
30190
+ // ../../packages/schema/src/operations/google-contacts.ts
30191
+ var googleContactsOperations = defineOperations({
30192
+ list: {
30193
+ method: "get",
30194
+ path: "/people/google-contacts",
30195
+ scopes: ["contacts:read"],
30196
+ tag: "People",
30197
+ summary: "Your Google Contacts, to pick from",
30198
+ description: "The key's member's Google Contacts link (`status`: `available`, `mode`, `connected`, `account`) and the people in it matching `q` (a name or an email; everyone without), each with `resourceName`, `name`, `emails`, `phones`, `birthday` (`yearKnown: false` for a day and month only), the contact in do they'd merge into (`match`: by `email`, `phone` or a unique `name`; never a company) and what importing adds (`adds`). Not connected: no candidates. Connecting needs a browser (People \u2192 Import). A key with no member is a 403. Off until `GOOGLE_CONTACTS_API` is `fake` or `live`.",
30199
+ query: [{ name: "q", description: "A name or email to narrow to." }],
30200
+ querySchema: googleContactsQuerySchema,
30201
+ status: 200,
30202
+ response: { description: "The link and the candidates." },
30203
+ errors: { 403: "Not a member's key." }
30204
+ },
30205
+ import: {
30206
+ method: "post",
30207
+ path: "/people/google-contacts/import",
30208
+ scopes: ["contacts:write"],
30209
+ tag: "People",
30210
+ summary: "Import the people you picked from Google Contacts",
30211
+ description: "Only the picked people (`resourceNames`, Google's `people/\u2026` ids, at most 200). Each merges into the contact it matches and fills in only a missing email, phone or birthday (a key date that becomes a Task), else becomes a new contact, so importing twice never duplicates anyone. Returns `{ created, merged (with what each got: added), missing }`. Not connected is a 409; off until `GOOGLE_CONTACTS_API` is set (403).",
30212
+ body: importGoogleContactsInputSchema,
30213
+ status: 200,
30214
+ response: { description: "What the import did." },
30215
+ errors: {
30216
+ 400: true,
30217
+ 403: "Off, or not a member's key.",
30218
+ 409: "Google Contacts isn't connected (or asks to sign in again)."
30219
+ }
30220
+ }
30221
+ });
30222
+
30223
+ // ../../packages/schema/src/operations/google-health.ts
30224
+ var googleHealthOperations = defineOperations({
30225
+ status: {
30226
+ method: "get",
30227
+ path: "/health/google-health",
30228
+ scopes: ["health:read"],
30229
+ tag: "Health",
30230
+ summary: "Get the Google Health link",
30231
+ description: "The key's member's Google Health link (connected in the app, Settings \u2192 Health): `available` and `mode` (off \xB7 fake \xB7 live, the server's switch), `connected`, the Google `account`, `lastSyncAt` and each resource's (sleep, resting_heart_rate, hrv, steps, workouts), and `failure` (`reauth` \xB7 `not_linked` \xB7 `unavailable` \xB7 `other`, with a title and a hint) when the sync stopped, and `nutrition` (writing the member's food back to Google Health, opt-in in the app: `enabled`, `granted` (the link has the write scope), `lastWrittenDay`, `failure`). Never another member's.",
30232
+ status: 200,
30233
+ response: { description: "The link's status." },
30234
+ errors: { 403: true }
30235
+ },
30236
+ sync: {
30237
+ method: "post",
30238
+ path: "/health/google-health/sync",
30239
+ scopes: ["health:write"],
30240
+ tag: "Health",
30241
+ summary: "Sync Google Health now",
30242
+ description: "As the key's member, one sync of their Google Health, as the 07:30 morning sync does: last night's sleep (on the wake day, joining the Sleep and Bedtime Metrics), the day's resting heart rate and HRV, steps and workouts (joining the sessions) onto their own ledger, nothing twice. Returns written and skipped per resource and the status after it; a sync that stops answers 200 with `status.failure`. 409 before the member connected, 403 while the server has Google Health off.",
30243
+ status: 200,
30244
+ response: { description: "What came in, and the status after it." },
30245
+ errors: { 403: true, 409: true }
30246
+ }
30247
+ });
30248
+
30249
+ // ../../packages/schema/src/operations/inventory-items.ts
30250
+ var inventoryItemOperations = defineOperations({
30251
+ ...crudOperations({
30252
+ segment: "inventory-items",
30253
+ tag: "Inventory",
30254
+ noun: "inventory item",
30255
+ read: "inventory:read",
30256
+ write: "inventory:write",
30257
+ create: createInventoryItemInputSchema,
30258
+ update: updateInventoryItemInputSchema,
30259
+ paginated: true,
30260
+ listQuerySchema: paginationQuerySchema
30261
+ })
30262
+ });
30263
+
30264
+ // ../../packages/schema/src/operations/mail-campaigns.ts
30265
+ var pathParams = { id: "The campaign's ULID." };
30266
+ var mailCampaignOperations = defineOperations({
30267
+ list: {
30268
+ method: "get",
30269
+ path: "/mail-campaigns",
30270
+ scopes: ["contacts:read"],
30271
+ tag: "Guests",
30272
+ summary: "List a guest list's mail campaigns",
30273
+ description: "Its drafted and sent mails, newest first.",
30274
+ query: [{ name: "guestListId", description: "The guest list (required)." }],
30275
+ querySchema: mailCampaignsQuerySchema,
30276
+ status: 200,
30277
+ response: { description: "The campaigns." },
30278
+ errors: { 400: true }
30279
+ },
30280
+ create: {
30281
+ method: "post",
30282
+ path: "/mail-campaigns",
30283
+ scopes: ["contacts:write"],
30284
+ tag: "Guests",
30285
+ summary: "Draft a mail campaign",
30286
+ description: "A draft for a guest list's households: its `kind` (`message`, `save_the_date` or `invitation`), the template's `data` (the copy per language, Dutch required) and an optional reply address. A key can draft, preview and edit, never test or send: only a member's own session sends.",
30287
+ body: createMailCampaignInputSchema,
30288
+ status: 201,
30289
+ response: { description: "The draft." },
30290
+ errors: { 400: true, 404: true }
30291
+ },
30292
+ get: {
30293
+ method: "get",
30294
+ path: "/mail-campaigns/{id}",
30295
+ pathParams,
30296
+ scopes: ["contacts:read"],
30297
+ tag: "Guests",
30298
+ summary: "Get a mail campaign",
30299
+ description: "",
30300
+ status: 200,
30301
+ response: { description: "The campaign." },
30302
+ errors: { 404: true }
30303
+ },
30304
+ update: {
30305
+ method: "patch",
30306
+ path: "/mail-campaigns/{id}",
30307
+ pathParams,
30308
+ scopes: ["contacts:write"],
30309
+ tag: "Guests",
30310
+ summary: "Edit a draft",
30311
+ description: "Change its kind, data or reply address. Any edit makes the last test stale: a new test is needed before it can be sent.",
30312
+ body: updateMailCampaignInputSchema,
30313
+ status: 200,
30314
+ response: { description: "The draft." },
30315
+ errors: { 400: true, 404: true }
30316
+ },
30317
+ preview: {
30318
+ method: "get",
30319
+ path: "/mail-campaigns/{id}/preview",
30320
+ pathParams,
30321
+ scopes: ["contacts:read"],
30322
+ tag: "Guests",
30323
+ summary: "Preview a mail campaign",
30324
+ description: "The mail as a household would get it in one language (`nl` by default, or `pt`): its subject and rendered body.",
30325
+ query: [{ name: "language", description: "`nl` (default) or `pt`." }],
30326
+ querySchema: mailPreviewQuerySchema,
30327
+ status: 200,
30328
+ response: { description: "The rendered mail." },
30329
+ errors: { 400: true, 404: true }
30330
+ }
30331
+ });
30332
+
30333
+ // ../../packages/schema/src/operations/meal-plans.ts
30334
+ var mealPlanOperations = defineOperations({
30335
+ ...crudOperations({
30336
+ segment: "meal-plans",
30337
+ tag: "Meals",
30338
+ noun: "meal plan",
30339
+ read: "meals:read",
30340
+ write: "meals:write",
30341
+ create: createMealPlanInputSchema,
30342
+ update: updateMealPlanInputSchema
30343
+ })
30344
+ });
30345
+
30346
+ // ../../packages/schema/src/operations/occurrences.ts
30347
+ var occurrenceOperations = defineOperations({
30348
+ list: {
30349
+ method: "get",
30350
+ path: "/occurrences",
30351
+ scopes: ["occurrences:read"],
30352
+ tag: "Occurrences",
30353
+ summary: "List a template's occurrences",
30354
+ description: "Occurrences of one task/habit template, oldest-first.",
30355
+ query: [{ name: "templateId", description: "The task/habit template's ULID.", required: true }],
30356
+ querySchema: object({ templateId: ulidSchema }),
30357
+ status: 200,
30358
+ response: { description: "The template's occurrences." },
30359
+ errors: { 400: true }
30360
+ },
30361
+ update: {
30362
+ method: "patch",
30363
+ path: "/occurrences/{id}",
30364
+ scopes: ["occurrences:write"],
30365
+ tag: "Occurrences",
30366
+ summary: "Override an occurrence",
30367
+ description: 'Reschedule/skip one occurrence ("edit this"); fires `occurrence.updated`.',
30368
+ body: updateOccurrenceInputSchema,
30369
+ status: 200,
30370
+ response: { description: "The overridden occurrence." },
30371
+ errors: { 400: true, 404: true },
30372
+ webhook: "occurrence.updated"
30373
+ },
30374
+ complete: {
30375
+ method: "post",
30376
+ path: "/occurrences/{id}/complete",
30377
+ scopes: ["occurrences:write"],
30378
+ tag: "Occurrences",
30379
+ summary: "Complete an occurrence",
30380
+ description: "Mark an occurrence done; fires `occurrence.completed`.",
30381
+ status: 201,
30382
+ response: { description: "The completed occurrence." },
30383
+ errors: { 404: true },
30384
+ webhook: "occurrence.completed"
30385
+ }
30386
+ });
30387
+
30388
+ // ../../packages/schema/src/operations/periods.ts
30389
+ var periodOperations = defineOperations({
30390
+ ...crudOperations({
30391
+ segment: "periods",
30392
+ tag: "Periods",
30393
+ noun: "period",
30394
+ read: "periods:read",
30395
+ write: "periods:write",
30396
+ create: createPeriodInputSchema,
30397
+ update: updatePeriodInputSchema,
30398
+ listQuery: [
30399
+ { name: "nodeId", description: "Only the periods on this node." },
30400
+ {
30401
+ name: "kind",
30402
+ description: "Only this kind (`hibernation`, `quarantine`, `away`, `treatment_course`, `withdrawal`, \u2026)."
30403
+ },
30404
+ {
30405
+ name: "active",
30406
+ description: "`true`: only running periods; `false`: only ended or not yet started."
30407
+ }
30408
+ ],
30409
+ listQuerySchema: periodsQuerySchema
30410
+ })
30411
+ });
30412
+
30413
+ // ../../packages/schema/src/operations/planning-rules.ts
30414
+ var planningRuleOperations = defineOperations({
30415
+ ...crudOperations({
30416
+ segment: "planning/rules",
30417
+ tag: "Planning rules",
30418
+ noun: "planning rule",
30419
+ read: "planning:read",
30420
+ write: "planning:write",
30421
+ create: createPlanningRuleInputSchema,
30422
+ update: updatePlanningRuleInputSchema,
30423
+ listQuery: [
30424
+ { name: "forUserId", description: "Only this member's rules." },
30425
+ {
30426
+ name: "kind",
30427
+ description: "Only this kind (`fixed_time`, `keep_free`, `meaning`, `whose`, `limit`, `note`)."
30428
+ }
30429
+ ],
30430
+ listQuerySchema: planningRulesQuerySchema
30431
+ })
30432
+ });
30433
+
30434
+ // ../../packages/schema/src/operations/plans.ts
30435
+ function decision(path, verb, what) {
30436
+ return {
30437
+ method: "post",
30438
+ path,
30439
+ scopes: ["tasks:write"],
30440
+ tag: "Plans",
30441
+ summary: `${verb[0]?.toUpperCase()}${verb.slice(1)} plan items`,
30442
+ description: `${what} \`itemIds\` narrows it to those items; absent (or an empty body) answers every item it applies to. An unknown item id is a 404.`,
30443
+ body: planDecisionInputSchema,
30444
+ bodyOptional: true,
30445
+ status: 200,
30446
+ response: { description: "The plan, after.", view: "Plan" },
30447
+ errors: { 400: true, 404: true }
30448
+ };
30449
+ }
30450
+ var planOperations = defineOperations({
30451
+ list: {
30452
+ method: "get",
30453
+ path: "/plans",
30454
+ scopes: ["tasks:read"],
30455
+ tag: "Plans",
30456
+ summary: "List plans",
30457
+ description: "The Space's plan proposals, newest first (the latest 20), each with its items and each item's task as it is now. `status`: `open` (an item waits for an answer), `settled` (none does) or `replaced` (a newer plan for the same week or day took its place).",
30458
+ query: [
30459
+ { name: "kind", description: "`week` or `day`." },
30460
+ { name: "status", description: "`open`, `settled` or `replaced`." },
30461
+ { name: "startsOn", description: "The week's Monday, or the day (`YYYY-MM-DD`)." },
30462
+ { name: "forUserId", description: "Only this member's plans." }
30463
+ ],
30464
+ querySchema: planListQuerySchema.extend({ forUserId: string2().min(1).optional() }),
30465
+ status: 200,
30466
+ response: { description: "The plans.", list: true, view: "Plan" },
30467
+ errors: { 400: true }
30468
+ },
30469
+ create: {
30470
+ method: "post",
30471
+ path: "/plans",
30472
+ scopes: ["tasks:write"],
30473
+ tag: "Plans",
30474
+ summary: "Propose a plan",
30475
+ description: "The agent proposes a week (`kind: week`, `startsOn` a Monday) or a day for a member (`forUserId`, default the member the key acts for). Each item is about one open task: `plan` it on a `day` (all-day, in the member's zone) or `at` a time (with `durationMinutes` when known), take it back to `someday`, or `let_go`. `reason` is the line shown with it. Nothing is written to a task until the member accepts; a newer plan for the same week or day replaces an open one. An Agent's own work joins only while it waits on the member. A block that is already there (a task, a life block or an event on that day with a similar title) is folded in, not added twice: a task is planned into the block's time, anything else is left out, and `merged` lists each with a hint; `onDuplicate: reject` refuses the plan with those hints instead.",
30476
+ body: createPlanInputSchema,
30477
+ status: 201,
30478
+ response: {
30479
+ description: "The plan, open, with the duplicates it folded in.",
30480
+ view: "PlanCreateResult"
30481
+ },
30482
+ errors: { 400: true }
30483
+ },
30484
+ get: {
30485
+ method: "get",
30486
+ path: "/plans/{id}",
30487
+ scopes: ["tasks:read"],
30488
+ tag: "Plans",
30489
+ summary: "Get a plan",
30490
+ description: "One plan with its items.",
30491
+ status: 200,
30492
+ response: { description: "The plan.", view: "Plan" },
30493
+ errors: { 404: true }
30494
+ },
30495
+ accept: {
30496
+ ...decision("/plans/{id}/accept", "accept", "Accept proposed items: each task is written as the item says (a planned day or time, Someday, or cancelled for Let go; a task still Proposed by the agent joins the plan too), and the item keeps what it replaced for Undo. An item whose task has closed since is dismissed instead."),
30497
+ errors: { 400: true, 404: true, 409: "A newer plan replaced this one." }
30498
+ },
30499
+ dismiss: decision("/plans/{id}/dismiss", "dismiss", "Leave proposed items out. Nothing is written to a task."),
30500
+ undo: decision("/plans/{id}/undo", "undo", "Undo answered items: an accepted item's task is put back as it was before; a dismissed item simply waits again. Both read as proposed afterwards.")
30501
+ });
30502
+
30503
+ // ../../packages/schema/src/operations/rsvp-answers.ts
30504
+ var rsvpAnswerOperations = defineOperations({
30505
+ list: {
30506
+ method: "get",
30507
+ path: "/rsvp-answers",
30508
+ scopes: ["contacts:read"],
30509
+ tag: "Guests",
30510
+ summary: "List RSVP answers",
30511
+ description: "A guest list's answers to its RSVP questions (`guestListId`, required), or one household's (`householdId`): `{ questionId, householdId, guestId, value }`, `guestId` null for the household's own.",
30512
+ query: [
30513
+ { name: "guestListId", description: "The guest list (required)." },
30514
+ { name: "householdId", description: "Only this household's answers." }
30515
+ ],
30516
+ querySchema: rsvpAnswersQuerySchema,
30517
+ status: 200,
30518
+ response: { description: "The answers." },
30519
+ errors: { 400: true, 404: true }
30520
+ },
30521
+ set: {
30522
+ method: "put",
30523
+ path: "/rsvp-answers",
30524
+ scopes: ["contacts:write"],
30525
+ tag: "Guests",
30526
+ summary: "Set a household's RSVP answers",
30527
+ description: "Lays `answers` over the household's stored ones (`value: null` clears one; answers left out stay) and returns what stands. An unknown question, a guest not in the household, a per-guest question without a guest, an event the guest isn't invited to, or a value that doesn't fit the kind is a 422; an answer whose question no longer shows (its `showIf`) is dropped. Fires `rsvp.answers.updated`.",
30528
+ body: setRsvpAnswersInputSchema,
30529
+ status: 200,
30530
+ response: { description: "The household's answers after." },
30531
+ errors: { 400: true, 404: true, 422: "An answer that can't be given (see the message)." },
30532
+ webhook: "rsvp.answers.updated"
30533
+ }
30534
+ });
30535
+
30536
+ // ../../packages/schema/src/operations/rsvp-questions.ts
30537
+ var rsvpQuestionOperations = defineOperations({
30538
+ ...crudOperations({
30539
+ segment: "rsvp-questions",
30540
+ tag: "Guests",
30541
+ noun: "RSVP question",
30542
+ eventBase: "rsvp.question",
30543
+ read: "contacts:read",
30544
+ write: "contacts:write",
30545
+ create: createRsvpQuestionInputSchema,
30546
+ update: updateRsvpQuestionInputSchema,
30547
+ listQuery: [{ name: "guestListId", description: "The guest list (required)." }],
30548
+ listQuerySchema: rsvpQuestionsQuerySchema
30549
+ }),
30550
+ presets: {
30551
+ method: "post",
30552
+ path: "/rsvp-questions/presets",
30553
+ scopes: ["contacts:write"],
30554
+ tag: "Guests",
30555
+ summary: "Add the built-in RSVP questions",
30556
+ description: "Adds the preset questions after the list's own (`diet`: per guest, shown to a guest who said yes to anything, with vegetarian \xB7 vegan \xB7 gluten-free \xB7 lactose-free \xB7 nut allergy \xB7 other, and a text line shown when other is picked; `message`: the household's, once). A preset the list already has stays as it is. Returns the list's questions; fires `rsvp.question.created`.",
30557
+ body: addRsvpPresetsInputSchema,
30558
+ status: 200,
30559
+ response: { description: "The list's questions." },
30560
+ errors: { 400: true, 404: true },
30561
+ webhook: "rsvp.question.created"
30562
+ }
30563
+ });
30564
+
30565
+ // ../../packages/schema/src/operations/settings.ts
30566
+ var settingsOperations = defineOperations({
30567
+ get: {
30568
+ method: "get",
30569
+ path: "/settings",
30570
+ scopes: ["settings:read"],
30571
+ tag: "Settings",
30572
+ summary: "Get settings",
30573
+ description: "The caller's settings (lazily created). The key must carry a user identity.",
30574
+ status: 200,
30575
+ response: { description: "The caller's settings." }
30576
+ },
30577
+ update: {
30578
+ method: "patch",
30579
+ path: "/settings",
30580
+ scopes: ["settings:write"],
30581
+ tag: "Settings",
30582
+ summary: "Update settings",
30583
+ description: "Patch the caller's settings; fires `settings.updated`.",
30584
+ body: updateUserSettingsInputSchema,
30585
+ status: 200,
30586
+ response: { description: "The updated settings." },
30587
+ errors: { 400: true },
30588
+ webhook: "settings.updated"
30589
+ }
30590
+ });
30591
+
30592
+ // ../../packages/schema/src/operations/shopping-lists.ts
30593
+ var shoppingListOperations = defineOperations({
30594
+ ...crudOperations({
30595
+ segment: "shopping-lists",
30596
+ tag: "Shopping",
30597
+ noun: "shopping list",
30598
+ read: "shopping:read",
30599
+ write: "shopping:write",
30600
+ create: createShoppingListInputSchema,
30601
+ update: updateShoppingListInputSchema,
30602
+ paginated: true,
30603
+ listQuerySchema: paginationQuerySchema
30604
+ })
30605
+ });
30606
+
29606
30607
  // ../../packages/schema/src/operations/tap-tags.ts
29607
30608
  var READ = ["habits:read", "routines:read"];
29608
30609
  var WRITE = ["habits:write", "routines:write"];
29609
- var pathParams = { id: "The tag's id: the ULID the sticker's link carries (`/t/{id}`)." };
30610
+ var pathParams2 = { id: "The tag's id: the ULID the sticker's link carries (`/t/{id}`)." };
29610
30611
  var tapTagOperations = defineOperations({
29611
30612
  list: {
29612
30613
  method: "get",
@@ -29634,7 +30635,7 @@ var tapTagOperations = defineOperations({
29634
30635
  get: {
29635
30636
  method: "get",
29636
30637
  path: "/tap-tags/{id}",
29637
- pathParams,
30638
+ pathParams: pathParams2,
29638
30639
  scopes: READ,
29639
30640
  tag: "Habits",
29640
30641
  summary: "Get a tap tag",
@@ -29646,7 +30647,7 @@ var tapTagOperations = defineOperations({
29646
30647
  update: {
29647
30648
  method: "patch",
29648
30649
  path: "/tap-tags/{id}",
29649
- pathParams,
30650
+ pathParams: pathParams2,
29650
30651
  scopes: WRITE,
29651
30652
  tag: "Habits",
29652
30653
  summary: "Rename or rebind a tap tag",
@@ -29659,7 +30660,7 @@ var tapTagOperations = defineOperations({
29659
30660
  remove: {
29660
30661
  method: "delete",
29661
30662
  path: "/tap-tags/{id}",
29662
- pathParams,
30663
+ pathParams: pathParams2,
29663
30664
  scopes: WRITE,
29664
30665
  tag: "Habits",
29665
30666
  summary: "Delete a tap tag",
@@ -29671,7 +30672,7 @@ var tapTagOperations = defineOperations({
29671
30672
  tap: {
29672
30673
  method: "post",
29673
30674
  path: "/tap-tags/{id}/tap",
29674
- pathParams,
30675
+ pathParams: pathParams2,
29675
30676
  scopes: WRITE,
29676
30677
  tag: "Habits",
29677
30678
  summary: "Tap a tap tag",
@@ -29682,6 +30683,103 @@ var tapTagOperations = defineOperations({
29682
30683
  }
29683
30684
  });
29684
30685
 
30686
+ // ../../packages/schema/src/operations/today.ts
30687
+ var todayOperations = defineOperations({
30688
+ fromEarlier: {
30689
+ method: "get",
30690
+ path: "/today/from-earlier",
30691
+ scopes: ["tasks:read"],
30692
+ tag: "Reads",
30693
+ summary: "From earlier",
30694
+ description: 'The planned tasks that slipped (#888): open, non-recurring tasks whose all-day planned day falls in the 14 days before `from`, newest plan first. Timed tasks, tasks placed in a past time block and agent-owned work never roll; older plans simply leave the list (nothing is rewritten). A Space back from 21+ days away gets no pile. Also names `laterThisWeek`, the quietest later day this week (Monday-first; tomorrow on a Sunday) that "Later this week" lands on. Answer with `POST /api/v1/today/rollover`.',
30695
+ query: [
30696
+ {
30697
+ name: "from",
30698
+ description: "Your local day start, an ISO instant (or a bare date, read as UTC midnight). Defaults to the current UTC day."
30699
+ },
30700
+ { name: "areaId", description: "Only tasks in this Area (own or their Project's)." }
30701
+ ],
30702
+ querySchema: fromEarlierQuerySchema,
30703
+ status: 200,
30704
+ response: { description: "From earlier.", view: "FromEarlierResponse" },
30705
+ errors: { 400: true }
30706
+ },
30707
+ rollover: {
30708
+ method: "post",
30709
+ path: "/today/rollover",
30710
+ scopes: ["tasks:write"],
30711
+ tag: "Tasks",
30712
+ summary: "Answer From earlier",
30713
+ description: 'Answer many tasks (and habit/routine occurrences) at once: `reschedule` to `to` (all-day; From earlier\'s Today and Later this week), `someday` (clear the planned day, keep the Area or Project; occurrences untouched), or `drop` ("Let go": tasks cancelled, occurrences skipped; a cancelled task never counts as Landed). Only open tasks and unresolved occurrences change; unknown ids are skipped. Returns how many of each moved. Occurrence ids need `habits:write` too.',
30714
+ body: rolloverInputSchema,
30715
+ status: 201,
30716
+ response: { description: "How many tasks and occurrences changed." },
30717
+ errors: { 400: true }
30718
+ }
30719
+ });
30720
+
30721
+ // ../../packages/schema/src/operations/webhooks.ts
30722
+ var webhookOperations = defineOperations({
30723
+ list: {
30724
+ method: "get",
30725
+ path: "/webhooks",
30726
+ scopes: ["webhooks:read"],
30727
+ tag: "Webhooks",
30728
+ summary: "List webhook subscriptions",
30729
+ description: "The Space's outbound webhook endpoints (secrets omitted), newest first.",
30730
+ querySchema: paginationQuerySchema,
30731
+ paginated: true,
30732
+ status: 200,
30733
+ response: { description: "The subscriptions." }
30734
+ },
30735
+ create: {
30736
+ method: "post",
30737
+ path: "/webhooks",
30738
+ scopes: ["webhooks:write"],
30739
+ tag: "Webhooks",
30740
+ summary: "Register a webhook subscription",
30741
+ description: "Register an endpoint + events; returns the signing secret once.",
30742
+ body: createWebhookSubscriptionInputSchema,
30743
+ status: 201,
30744
+ response: { description: "The created subscription (with its one-time secret)." },
30745
+ errors: { 400: true }
30746
+ },
30747
+ get: {
30748
+ method: "get",
30749
+ path: "/webhooks/{id}",
30750
+ scopes: ["webhooks:read"],
30751
+ tag: "Webhooks",
30752
+ summary: "Get a webhook subscription",
30753
+ description: "One subscription (secret omitted).",
30754
+ status: 200,
30755
+ response: { description: "The subscription." },
30756
+ errors: { 404: true }
30757
+ },
30758
+ update: {
30759
+ method: "patch",
30760
+ path: "/webhooks/{id}",
30761
+ scopes: ["webhooks:write"],
30762
+ tag: "Webhooks",
30763
+ summary: "Update a webhook subscription",
30764
+ description: "Patch url/events, or pause with `active: false`.",
30765
+ body: updateWebhookSubscriptionInputSchema,
30766
+ status: 200,
30767
+ response: { description: "The updated subscription." },
30768
+ errors: { 400: true, 404: true }
30769
+ },
30770
+ remove: {
30771
+ method: "delete",
30772
+ path: "/webhooks/{id}",
30773
+ scopes: ["webhooks:write"],
30774
+ tag: "Webhooks",
30775
+ summary: "Delete a webhook subscription",
30776
+ description: "Remove an endpoint.",
30777
+ status: 204,
30778
+ response: { description: "Deleted." },
30779
+ errors: { 404: true }
30780
+ }
30781
+ });
30782
+
29685
30783
  // ../../node_modules/ulidx/dist/node/index.js
29686
30784
  import crypto from "crypto";
29687
30785
 
@@ -29934,7 +31032,7 @@ function parseCapture2(input, now) {
29934
31032
 
29935
31033
  // src/meta.ts
29936
31034
  var CLI_NAME = "do";
29937
- var VERSION = "1.302.0";
31035
+ var VERSION = "1.304.0";
29938
31036
  var API_PREFIX = "/api/v1";
29939
31037
  var DEFAULT_API_URL = "http://localhost:3001";
29940
31038
 
@@ -30717,24 +31815,15 @@ var ENTITIES = [
30717
31815
  canDelete: true
30718
31816
  }
30719
31817
  },
30720
- {
31818
+ entityFromOperations(rsvpQuestionOperations, {
30721
31819
  name: "rsvp-questions",
30722
- path: "/rsvp-questions",
30723
- readScopes: ["contacts:read"],
30724
31820
  returns: "RsvpQuestion",
30725
- canGet: true,
30726
31821
  listFlags: [
30727
31822
  { name: "list", description: "the guest list's id (required)", query: "guestListId" }
30728
31823
  ],
30729
- write: {
30730
- scopes: ["contacts:write"],
30731
- createSchema: createRsvpQuestionInputSchema,
30732
- createInput: "CreateRsvpQuestionInput",
30733
- updateSchema: updateRsvpQuestionInputSchema,
30734
- updateInput: "UpdateRsvpQuestionInput",
30735
- canDelete: true
30736
- }
30737
- },
31824
+ createInput: "CreateRsvpQuestionInput",
31825
+ updateInput: "UpdateRsvpQuestionInput"
31826
+ }),
30738
31827
  {
30739
31828
  name: "guests",
30740
31829
  path: "/guests",
@@ -30843,12 +31932,9 @@ var ENTITIES = [
30843
31932
  canDelete: true
30844
31933
  }
30845
31934
  },
30846
- {
31935
+ entityFromOperations(periodOperations, {
30847
31936
  name: "periods",
30848
- path: "/periods",
30849
- readScopes: ["periods:read"],
30850
31937
  returns: "PeriodView",
30851
- canGet: true,
30852
31938
  listFlags: [
30853
31939
  { name: "node", description: "only the periods on this node id", query: "nodeId" },
30854
31940
  {
@@ -30862,15 +31948,9 @@ var ENTITIES = [
30862
31948
  query: "active"
30863
31949
  }
30864
31950
  ],
30865
- write: {
30866
- scopes: ["periods:write"],
30867
- createSchema: createPeriodInputSchema,
30868
- createInput: "CreatePeriodInput",
30869
- updateSchema: updatePeriodInputSchema,
30870
- updateInput: "UpdatePeriodInput",
30871
- canDelete: true
30872
- }
30873
- },
31951
+ createInput: "CreatePeriodInput",
31952
+ updateInput: "UpdatePeriodInput"
31953
+ }),
30874
31954
  entityFromOperations(tapTagOperations, {
30875
31955
  name: "tap-tags",
30876
31956
  returns: "TapTagView",
@@ -30932,38 +32012,20 @@ var ENTITIES = [
30932
32012
  canDelete: true
30933
32013
  }
30934
32014
  },
30935
- {
32015
+ entityFromOperations(shoppingListOperations, {
30936
32016
  name: "shopping-lists",
30937
- path: "/shopping-lists",
30938
- readScopes: ["shopping:read"],
30939
32017
  returns: "ShoppingList",
30940
- canGet: true,
30941
32018
  listFlags: [],
30942
- write: {
30943
- scopes: ["shopping:write"],
30944
- createSchema: createShoppingListInputSchema,
30945
- createInput: "CreateShoppingListInput",
30946
- updateSchema: updateShoppingListInputSchema,
30947
- updateInput: "UpdateShoppingListInput",
30948
- canDelete: true
30949
- }
30950
- },
30951
- {
32019
+ createInput: "CreateShoppingListInput",
32020
+ updateInput: "UpdateShoppingListInput"
32021
+ }),
32022
+ entityFromOperations(inventoryItemOperations, {
30952
32023
  name: "inventory-items",
30953
- path: "/inventory-items",
30954
- readScopes: ["inventory:read"],
30955
32024
  returns: "InventoryItem",
30956
- canGet: true,
30957
32025
  listFlags: [],
30958
- write: {
30959
- scopes: ["inventory:write"],
30960
- createSchema: createInventoryItemInputSchema,
30961
- createInput: "CreateInventoryItemInput",
30962
- updateSchema: updateInventoryItemInputSchema,
30963
- updateInput: "UpdateInventoryItemInput",
30964
- canDelete: true
30965
- }
30966
- },
32026
+ createInput: "CreateInventoryItemInput",
32027
+ updateInput: "UpdateInventoryItemInput"
32028
+ }),
30967
32029
  {
30968
32030
  name: "recipes",
30969
32031
  path: "/recipes",
@@ -30980,28 +32042,16 @@ var ENTITIES = [
30980
32042
  canDelete: true
30981
32043
  }
30982
32044
  },
30983
- {
32045
+ entityFromOperations(mealPlanOperations, {
30984
32046
  name: "meal-plans",
30985
- path: "/meal-plans",
30986
- readScopes: ["meals:read"],
30987
32047
  returns: "MealPlan",
30988
- canGet: true,
30989
32048
  listFlags: [],
30990
- write: {
30991
- scopes: ["meals:write"],
30992
- createSchema: createMealPlanInputSchema,
30993
- createInput: "CreateMealPlanInput",
30994
- updateSchema: updateMealPlanInputSchema,
30995
- updateInput: "UpdateMealPlanInput",
30996
- canDelete: true
30997
- }
30998
- },
30999
- {
32049
+ createInput: "CreateMealPlanInput",
32050
+ updateInput: "UpdateMealPlanInput"
32051
+ }),
32052
+ entityFromOperations({ list: occurrenceOperations.list }, {
31000
32053
  name: "occurrences",
31001
- path: "/occurrences",
31002
- readScopes: ["occurrences:read"],
31003
32054
  returns: "Occurrence",
31004
- canGet: false,
31005
32055
  listFlags: [
31006
32056
  {
31007
32057
  name: "template-id",
@@ -31010,7 +32060,7 @@ var ENTITIES = [
31010
32060
  query: "templateId"
31011
32061
  }
31012
32062
  ]
31013
- },
32063
+ }),
31014
32064
  {
31015
32065
  name: "time-blocks",
31016
32066
  path: "/time-blocks",
@@ -31034,30 +32084,14 @@ var ENTITIES = [
31034
32084
  canDelete: true
31035
32085
  }
31036
32086
  },
31037
- {
32087
+ entityFromOperations(blockTemplateOperations, {
31038
32088
  name: "block-templates",
31039
- path: "/block-templates",
31040
- readScopes: ["calendar:read"],
31041
32089
  returns: "BlockTemplateView",
31042
- canGet: true,
31043
32090
  listFlags: [],
31044
- write: {
31045
- scopes: ["calendar:write"],
31046
- createSchema: createBlockTemplateInputSchema,
31047
- createInput: "CreateBlockTemplateInput",
31048
- updateSchema: updateBlockTemplateInputSchema,
31049
- updateInput: "UpdateBlockTemplateInput",
31050
- canDelete: true
31051
- }
31052
- },
31053
- {
31054
- name: "webhooks",
31055
- path: "/webhooks",
31056
- readScopes: ["webhooks:read"],
31057
- returns: "WebhookSubscription",
31058
- canGet: true,
31059
- listFlags: []
31060
- }
32091
+ createInput: "CreateBlockTemplateInput",
32092
+ updateInput: "UpdateBlockTemplateInput"
32093
+ }),
32094
+ entityFromOperations({ list: webhookOperations.list, get: webhookOperations.get }, { name: "webhooks", returns: "WebhookSubscription", listFlags: [] })
31061
32095
  ];
31062
32096
  var entityByName = (name) => ENTITIES.find((e) => e.name === name);
31063
32097
  function entityOperations(e) {
@@ -31487,18 +32521,36 @@ var HEALTH_COMMAND_SPECS = [
31487
32521
  summary: "Whether your Google Health is connected, the last sync, and what stops it.",
31488
32522
  args: [],
31489
32523
  flags: [],
31490
- scopes: READ2,
32524
+ scopes: [...googleHealthOperations.status.scopes],
31491
32525
  returns: "GoogleHealthStatus",
31492
- notes: "GET /api/v1/health/google-health. Connecting needs Google's consent in a browser: Settings \u2192 Health in do. `failure` (reauth, not_linked, unavailable, other) says what stops the sync, with a title and a hint. `nutrition` says whether your food is written back to Google Health (turned on in do; each settled day once, one entry per meal type) and the last day written."
32526
+ notes: `${routeOf(googleHealthOperations.status)}. Connecting needs Google's consent in a browser: Settings \u2192 Health in do. \`failure\` (reauth, not_linked, unavailable, other) says what stops the sync, with a title and a hint. \`nutrition\` says whether your food is written back to Google Health (turned on in do; each settled day once, one entry per meal type) and the last day written.`
31493
32527
  },
31494
32528
  {
31495
32529
  path: ["health", "google", "sync"],
31496
32530
  summary: "Sync your Google Health now (sleep, resting heart rate, HRV, steps, workouts), as the morning sync does.",
31497
32531
  args: [],
31498
32532
  flags: [],
31499
- scopes: WRITE3,
32533
+ scopes: [...googleHealthOperations.sync.scopes],
31500
32534
  returns: "GoogleHealthSyncResult",
31501
- notes: "POST /api/v1/health/google-health/sync. Written and skipped per resource, and the status after it; a sync that stops answers 200 with `status.failure`. 409 before Google Health is connected, 403 while the server has it switched off."
32535
+ notes: `${routeOf(googleHealthOperations.sync)}. Written and skipped per resource, and the status after it; a sync that stops answers 200 with \`status.failure\`. 409 before Google Health is connected, 403 while the server has it switched off.`
32536
+ },
32537
+ {
32538
+ path: ["health", "withings", "status"],
32539
+ summary: "Whether your Withings scale is connected, the last sync, and what stops it.",
32540
+ args: [],
32541
+ flags: [],
32542
+ scopes: READ2,
32543
+ returns: "WithingsStatus",
32544
+ notes: "GET /api/v1/health/withings. Connecting needs Withings' consent in a browser: Settings \u2192 Health in do. `failure` (reauth, unavailable, other) says what stops the sync, with a title and a hint."
32545
+ },
32546
+ {
32547
+ path: ["health", "withings", "sync"],
32548
+ summary: "Sync your Withings weigh-ins now, as the morning sync does.",
32549
+ args: [],
32550
+ flags: [],
32551
+ scopes: WRITE3,
32552
+ returns: "WithingsSyncResult",
32553
+ notes: "POST /api/v1/health/withings/sync. Written and skipped, and the status after it; a sync that stops answers 200 with `status.failure`. 409 before Withings is connected, 403 while the server has it switched off."
31502
32554
  },
31503
32555
  {
31504
32556
  path: ["health", "analyte"],
@@ -31764,9 +32816,9 @@ var COMMAND_SPECS = [
31764
32816
  summary: "From earlier: planned tasks of the last 14 days that weren't done, newest first, and the day Later this week lands on.",
31765
32817
  args: [],
31766
32818
  flags: [{ name: "area", type: "string", description: "Only tasks in this Area (id)." }],
31767
- scopes: ["tasks:read"],
32819
+ scopes: [...todayOperations.fromEarlier.scopes],
31768
32820
  returns: "FromEarlierResponse",
31769
- notes: "GET /api/v1/today/from-earlier, from your local day. Nothing here is late: timed tasks, time blocks and agents' work never roll, and a plan older than 14 days just waits where it was filed. Answer with `do today answer`."
32821
+ notes: `${routeOf(todayOperations.fromEarlier)}, from your local day. Nothing here is late: timed tasks, time blocks and agents' work never roll, and a plan older than 14 days just waits where it was filed. Answer with \`do today answer\`.`
31770
32822
  },
31771
32823
  {
31772
32824
  path: ["today", "answer"],
@@ -32276,18 +33328,18 @@ var COMMAND_SPECS = [
32276
33328
  flags: [
32277
33329
  { name: "start", type: "string", description: "The week's Monday (YYYY-MM-DD). Required." }
32278
33330
  ],
32279
- scopes: ["calendar:read"],
33331
+ scopes: [...blockTemplateOperations.week.scopes],
32280
33332
  returns: "BlockTemplateInstance[]",
32281
- notes: "GET /api/v1/block-templates/week?start= (plan vs record, phase 1)."
33333
+ notes: `${routeOf(blockTemplateOperations.week)}?start= (plan vs record, phase 1).`
32282
33334
  },
32283
33335
  {
32284
33336
  path: ["block-templates", "defaults"],
32285
33337
  summary: "Give a member with no block templates the default week (sleep, morning, work, meals, a free evening). A no-op when they have templates.",
32286
33338
  args: [],
32287
33339
  flags: [],
32288
- scopes: ["calendar:write"],
33340
+ scopes: [...blockTemplateOperations.defaults.scopes],
32289
33341
  returns: "BlockTemplateView[]",
32290
- notes: "POST /api/v1/block-templates/defaults (plan vs record, phase 1)."
33342
+ notes: `${routeOf(blockTemplateOperations.defaults)} (plan vs record, phase 1).`
32291
33343
  },
32292
33344
  {
32293
33345
  path: ["time-blocks", "record"],
@@ -32425,18 +33477,18 @@ var COMMAND_SPECS = [
32425
33477
  },
32426
33478
  { name: "for", type: "string", description: "Only this member's plans (user id)." }
32427
33479
  ],
32428
- scopes: ["tasks:read"],
33480
+ scopes: [...planOperations.list.scopes],
32429
33481
  returns: "Plan[]",
32430
- notes: "GET /api/v1/plans (#912): the latest 20."
33482
+ notes: `${routeOf(planOperations.list)} (#912): the latest 20.`
32431
33483
  },
32432
33484
  {
32433
33485
  path: ["plans", "get"],
32434
33486
  summary: "One plan proposal with its items and each item's task as it is now.",
32435
33487
  args: [{ name: "id", required: true, description: "Plan id." }],
32436
33488
  flags: [],
32437
- scopes: ["tasks:read"],
33489
+ scopes: [...planOperations.get.scopes],
32438
33490
  returns: "Plan",
32439
- notes: "GET /api/v1/plans/{id} (#912)."
33491
+ notes: `${routeOf(planOperations.get)} (#912).`
32440
33492
  },
32441
33493
  {
32442
33494
  path: ["plans", "propose"],
@@ -32449,9 +33501,9 @@ var COMMAND_SPECS = [
32449
33501
  description: 'The CreatePlanInput as JSON: {"kind":"week","startsOn":"2026-10-19","note":"\u2026","items":[{"taskId":"\u2026","day":"2026-10-20","reason":"Deadline Friday"},{"taskId":"\u2026","at":"2026-10-21T09:00:00Z","durationMinutes":45},{"taskId":"\u2026","action":"someday"},{"taskId":"\u2026","action":"let_go"}],"blocks":[{"templateId":"\u2026","lifeKind":"meal","startAt":"2026-10-19T10:30:00Z","endAt":"2026-10-19T11:30:00Z"}]}. Blocks are life blocks (lay a week out with `do block-templates week`); a plan needs at least one item or block.'
32450
33502
  }
32451
33503
  ],
32452
- scopes: ["tasks:write"],
33504
+ scopes: [...planOperations.create.scopes],
32453
33505
  returns: "Plan",
32454
- notes: "POST /api/v1/plans (#912). A week starts on a Monday; days are the member's, times are ISO instants inside the week. `forUserId` names the member (default: the member the key acts for). A newer plan for the same week or day replaces the open one."
33506
+ notes: `${routeOf(planOperations.create)} (#912). A week starts on a Monday; days are the member's, times are ISO instants inside the week. \`forUserId\` names the member (default: the member the key acts for). A newer plan for the same week or day replaces the open one.`
32455
33507
  },
32456
33508
  {
32457
33509
  path: ["plans", "accept"],
@@ -32461,9 +33513,9 @@ var COMMAND_SPECS = [
32461
33513
  { name: "items", required: false, description: "Item ids (default: every proposed item)." }
32462
33514
  ],
32463
33515
  flags: [],
32464
- scopes: ["tasks:write"],
33516
+ scopes: [...planOperations.accept.scopes],
32465
33517
  returns: "Plan",
32466
- notes: "POST /api/v1/plans/{id}/accept (#912). Undo with `do plans undo`."
33518
+ notes: `${routeOf(planOperations.accept)} (#912). Undo with \`do plans undo\`.`
32467
33519
  },
32468
33520
  {
32469
33521
  path: ["plans", "dismiss"],
@@ -32473,9 +33525,9 @@ var COMMAND_SPECS = [
32473
33525
  { name: "items", required: false, description: "Item ids (default: every proposed item)." }
32474
33526
  ],
32475
33527
  flags: [],
32476
- scopes: ["tasks:write"],
33528
+ scopes: [...planOperations.dismiss.scopes],
32477
33529
  returns: "Plan",
32478
- notes: "POST /api/v1/plans/{id}/dismiss (#912). Nothing is written to a task."
33530
+ notes: `${routeOf(planOperations.dismiss)} (#912). Nothing is written to a task.`
32479
33531
  },
32480
33532
  {
32481
33533
  path: ["plans", "undo"],
@@ -32485,9 +33537,9 @@ var COMMAND_SPECS = [
32485
33537
  { name: "items", required: false, description: "Item ids (default: every answered item)." }
32486
33538
  ],
32487
33539
  flags: [],
32488
- scopes: ["tasks:write"],
33540
+ scopes: [...planOperations.undo.scopes],
32489
33541
  returns: "Plan",
32490
- notes: "POST /api/v1/plans/{id}/undo (#912). The items read as proposed again."
33542
+ notes: `${routeOf(planOperations.undo)} (#912). The items read as proposed again.`
32491
33543
  },
32492
33544
  {
32493
33545
  path: ["plan", "rules", "list"],
@@ -32501,9 +33553,9 @@ var COMMAND_SPECS = [
32501
33553
  description: "fixed_time, keep_free, meaning, whose, limit or note."
32502
33554
  }
32503
33555
  ],
32504
- scopes: ["planning:read"],
33556
+ scopes: [...planningRuleOperations.list.scopes],
32505
33557
  returns: "PlanningRule[]",
32506
- notes: "GET /api/v1/planning/rules. Oldest first; `addedByAgent` marks the ones an agent added."
33558
+ notes: `${routeOf(planningRuleOperations.list)}. Oldest first; \`addedByAgent\` marks the ones an agent added.`
32507
33559
  },
32508
33560
  {
32509
33561
  path: ["plan", "rules", "add"],
@@ -32521,9 +33573,9 @@ var COMMAND_SPECS = [
32521
33573
  description: "The member it is for (user id; default: the member the key acts for)."
32522
33574
  }
32523
33575
  ],
32524
- scopes: ["planning:write"],
33576
+ scopes: [...planningRuleOperations.create.scopes],
32525
33577
  returns: "PlanningRule",
32526
- notes: "POST /api/v1/planning/rules. `forUserId` (--for) names the member; a rule without --rule is a note."
33578
+ notes: `${routeOf(planningRuleOperations.create)}. \`forUserId\` (--for) names the member; a rule without --rule is a note.`
32527
33579
  },
32528
33580
  {
32529
33581
  path: ["plan", "rules", "update"],
@@ -32537,18 +33589,18 @@ var COMMAND_SPECS = [
32537
33589
  description: "The new structured part as JSON (see `plan rules add`), or `none` to make it a note."
32538
33590
  }
32539
33591
  ],
32540
- scopes: ["planning:write"],
33592
+ scopes: [...planningRuleOperations.update.scopes],
32541
33593
  returns: "PlanningRule",
32542
- notes: "PATCH /api/v1/planning/rules/{id}."
33594
+ notes: `${routeOf(planningRuleOperations.update)}.`
32543
33595
  },
32544
33596
  {
32545
33597
  path: ["plan", "rules", "remove"],
32546
33598
  summary: "Remove a planning rule (to the trash).",
32547
33599
  args: [{ name: "id", required: true, description: "Rule id." }],
32548
33600
  flags: [],
32549
- scopes: ["planning:write"],
33601
+ scopes: [...planningRuleOperations.remove.scopes],
32550
33602
  returns: "void",
32551
- notes: "DELETE /api/v1/planning/rules/{id}. Destructive: needs --allow-destructive."
33603
+ notes: `${routeOf(planningRuleOperations.remove)}. Destructive: needs --allow-destructive.`
32552
33604
  },
32553
33605
  {
32554
33606
  path: ["tasks", "ready"],
@@ -33669,7 +34721,7 @@ var COMMAND_SPECS = [
33669
34721
  summary: "List the Space's Agents (id + name).",
33670
34722
  args: [],
33671
34723
  flags: [],
33672
- scopes: ["agents:read"],
34724
+ scopes: [...agentOperations.list.scopes],
33673
34725
  returns: "AgentSummary",
33674
34726
  notes: "Creating an Agent and minting its keys stays a member's job in the app."
33675
34727
  },
@@ -33986,7 +35038,7 @@ var COMMAND_SPECS = [
33986
35038
  summary: "Show the caller's settings for the Space.",
33987
35039
  args: [],
33988
35040
  flags: [],
33989
- scopes: ["settings:read"],
35041
+ scopes: [...settingsOperations.get.scopes],
33990
35042
  returns: "UserSettings",
33991
35043
  notes: "Needs a key minted while logged in (it carries the user identity)."
33992
35044
  },
@@ -33995,7 +35047,7 @@ var COMMAND_SPECS = [
33995
35047
  summary: "Patch the caller's settings.",
33996
35048
  args: [],
33997
35049
  flags: [{ name: "data", type: "string", description: "JSON of fields to change." }],
33998
- scopes: ["settings:write"],
35050
+ scopes: [...settingsOperations.update.scopes],
33999
35051
  returns: "UserSettings"
34000
35052
  },
34001
35053
  {
@@ -34153,9 +35205,9 @@ var COMMAND_SPECS = [
34153
35205
  summary: "An asset's upkeep and repair Tasks, open ones first.",
34154
35206
  args: [{ name: "id", required: true, description: "Asset (Collection Item) ULID." }],
34155
35207
  flags: [],
34156
- scopes: ["collections:read"],
35208
+ scopes: [...collectionUpkeepOperations.list.scopes],
34157
35209
  returns: "AssetUpkeep",
34158
- notes: "GET /api/v1/collection/{id}/upkeep (LifeOS House). Returns { itemId, upkeep, repairs }."
35210
+ notes: `${routeOf(collectionUpkeepOperations.list)} (LifeOS House). Returns { itemId, upkeep, repairs }.`
34159
35211
  },
34160
35212
  {
34161
35213
  path: ["collection", "link-task"],
@@ -34165,9 +35217,9 @@ var COMMAND_SPECS = [
34165
35217
  { name: "taskId", required: true, description: "Task ULID." }
34166
35218
  ],
34167
35219
  flags: [{ name: "repair", type: "boolean", description: "A one-off repair, not upkeep." }],
34168
- scopes: ["collections:write"],
35220
+ scopes: [...collectionUpkeepOperations.link.scopes],
34169
35221
  returns: "AssetUpkeep",
34170
- notes: "POST /api/v1/collection/{id}/upkeep. Make the Task (recurrence, Place, Area) first. Finishing it writes the asset's service / repair Entry by itself."
35222
+ notes: `${routeOf(collectionUpkeepOperations.link)}. Make the Task (recurrence, Place, Area) first. Finishing it writes the asset's service / repair Entry by itself.`
34171
35223
  },
34172
35224
  {
34173
35225
  path: ["collection", "unlink-task"],
@@ -34177,7 +35229,7 @@ var COMMAND_SPECS = [
34177
35229
  { name: "taskId", required: true, description: "Task ULID." }
34178
35230
  ],
34179
35231
  flags: [],
34180
- scopes: ["collections:write"],
35232
+ scopes: [...collectionUpkeepOperations.unlink.scopes],
34181
35233
  returns: "AssetUpkeep"
34182
35234
  },
34183
35235
  {
@@ -35024,6 +36076,35 @@ var COMMAND_SPECS = [
35024
36076
  returns: "MoneyYear",
35025
36077
  notes: "GET /api/v1/money/year. totals (income, spent, saved) and lastYear (the same months a year earlier, null with nothing booked then); months Jan\u2013Dec; spending and income by category, biggest first, each with lastYear; recurring streams with thisYear, lastYear and change (new \xB7 stopped \xB7 up \xB7 down \xB7 same); goals with what went toward them this year. A year under way counts to this month (through)."
35026
36078
  },
36079
+ {
36080
+ path: ["money", "net-worth"],
36081
+ summary: "Net worth now and its month-end line (the accounts that count in it).",
36082
+ args: [],
36083
+ flags: [
36084
+ { name: "scope", type: "string", description: "all (default) | space | owner." },
36085
+ {
36086
+ name: "months",
36087
+ type: "string",
36088
+ description: "How many months the line covers (2\u201360, default 12)."
36089
+ }
36090
+ ],
36091
+ scopes: ["finance:read"],
36092
+ returns: "MoneyNetWorth",
36093
+ notes: "GET /api/v1/money/net-worth. total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed."
36094
+ },
36095
+ {
36096
+ path: ["money", "net-worth-set"],
36097
+ summary: "Set an account's value for a month by hand (a house, a car); again replaces it.",
36098
+ args: [{ name: "accountId", required: true, description: "Account ULID." }],
36099
+ flags: [
36100
+ { name: "value", type: "string", description: "The value, e.g. 310000 or 1234.56." },
36101
+ { name: "month", type: "string", description: "The month, YYYY-MM (default this month)." },
36102
+ { name: "data", type: "string", description: "SetMoneyNetWorthValueInput JSON instead." }
36103
+ ],
36104
+ scopes: ["finance:write"],
36105
+ returns: "MoneySnapshot",
36106
+ notes: "PUT /api/v1/money/net-worth/values. A month ahead is refused. This month's value also becomes the account's balance unless a bank link keeps it. `do money net-worth-set <accountId> --value 310000`."
36107
+ },
35027
36108
  {
35028
36109
  path: ["money", "goals"],
35029
36110
  summary: "List money goals: what's saved toward each, soonest first.",
@@ -35773,7 +36854,7 @@ var COMMAND_SPECS = [
35773
36854
  summary: "Your Google Contacts, to pick from: who they'd merge into and what importing adds.",
35774
36855
  args: [],
35775
36856
  flags: [{ name: "q", type: "string", description: "A name or email to narrow to." }],
35776
- scopes: ["contacts:read"],
36857
+ scopes: [...googleContactsOperations.list.scopes],
35777
36858
  returns: "GoogleContactsList",
35778
36859
  notes: "Connecting needs a browser (People \u2192 Import). Not connected: candidates is empty and status.connected false. A key with no member is forbidden."
35779
36860
  },
@@ -35788,7 +36869,7 @@ var COMMAND_SPECS = [
35788
36869
  }
35789
36870
  ],
35790
36871
  flags: [],
35791
- scopes: ["contacts:write"],
36872
+ scopes: [...googleContactsOperations.import.scopes],
35792
36873
  returns: "GoogleContactsImportResult",
35793
36874
  notes: "Each merges into the contact it matches (email, then phone, then a unique name) and fills in only a missing email, phone or birthday; the rest become new contacts. Ids Google no longer has come back as missing. Not connected is a conflict."
35794
36875
  },
@@ -35897,7 +36978,7 @@ var COMMAND_SPECS = [
35897
36978
  description: 'JSON AddRsvpPresetsInput, e.g. {"guestListId":"01\u2026","presets":["diet","message"]}'
35898
36979
  }
35899
36980
  ],
35900
- scopes: ["contacts:write"],
36981
+ scopes: [...rsvpQuestionOperations.presets.scopes],
35901
36982
  returns: "RsvpQuestion"
35902
36983
  },
35903
36984
  {
@@ -35908,7 +36989,7 @@ var COMMAND_SPECS = [
35908
36989
  { name: "list", type: "string", description: "The guest list's id (required)." },
35909
36990
  { name: "household", type: "string", description: "Only this household's answers." }
35910
36991
  ],
35911
- scopes: ["contacts:read"],
36992
+ scopes: [...rsvpAnswerOperations.list.scopes],
35912
36993
  returns: "RsvpAnswer"
35913
36994
  },
35914
36995
  {
@@ -35922,7 +37003,7 @@ var COMMAND_SPECS = [
35922
37003
  description: 'JSON SetRsvpAnswersInput, e.g. {"guestListId":"01\u2026","householdId":"01\u2026","answers":[{"questionId":"01\u2026","value":true},{"questionId":"01\u2026","guestId":"01\u2026","value":["vegan"]}]}'
35923
37004
  }
35924
37005
  ],
35925
- scopes: ["contacts:write"],
37006
+ scopes: [...rsvpAnswerOperations.set.scopes],
35926
37007
  returns: "RsvpAnswer"
35927
37008
  },
35928
37009
  {
@@ -35969,7 +37050,7 @@ var COMMAND_SPECS = [
35969
37050
  summary: "A guest list's mail campaigns (save-the-dates, invitations), newest first.",
35970
37051
  args: [],
35971
37052
  flags: [{ name: "list", type: "string", description: "The guest list's id (required)." }],
35972
- scopes: ["contacts:read"],
37053
+ scopes: [...mailCampaignOperations.list.scopes],
35973
37054
  returns: "MailCampaignView",
35974
37055
  notes: "Each campaign carries the households it reaches (a draft: who it would go to) with their status, its mails by status, and readyToSend."
35975
37056
  },
@@ -35978,7 +37059,7 @@ var COMMAND_SPECS = [
35978
37059
  summary: "One mail campaign, with its households and how far it got.",
35979
37060
  args: [{ name: "id", required: true, description: "The campaign's id." }],
35980
37061
  flags: [],
35981
- scopes: ["contacts:read"],
37062
+ scopes: [...mailCampaignOperations.get.scopes],
35982
37063
  returns: "MailCampaignView"
35983
37064
  },
35984
37065
  {
@@ -35992,7 +37073,7 @@ var COMMAND_SPECS = [
35992
37073
  description: 'JSON CreateMailCampaignInput, e.g. {"guestListId":"01\u2026","kind":"save_the_date","template":"message","data":{"nl":{"subject":"\u2026","heading":"\u2026","paragraphs":["\u2026"]},"pt":{\u2026},"footer":{"deleteBy":"2027-12-31"}},"replyTo":"hallo@example.com"}'
35993
37074
  }
35994
37075
  ],
35995
- scopes: ["contacts:write"],
37076
+ scopes: [...mailCampaignOperations.create.scopes],
35996
37077
  returns: "MailCampaignView",
35997
37078
  notes: "Drafting only: a member previews it, sends a test to the Space's members and sends it from the Mail tab. An agent can never test or send."
35998
37079
  },
@@ -36007,7 +37088,7 @@ var COMMAND_SPECS = [
36007
37088
  description: 'JSON UpdateMailCampaignInput, e.g. {"data":{"nl":{\u2026}}}'
36008
37089
  }
36009
37090
  ],
36010
- scopes: ["contacts:write"],
37091
+ scopes: [...mailCampaignOperations.update.scopes],
36011
37092
  returns: "MailCampaignView",
36012
37093
  notes: "Only a draft changes (409 otherwise); an edit makes the last test stale."
36013
37094
  },
@@ -36016,7 +37097,7 @@ var COMMAND_SPECS = [
36016
37097
  summary: "Draw a campaign's mail as its first household would get it.",
36017
37098
  args: [{ name: "id", required: true, description: "The campaign's id." }],
36018
37099
  flags: [{ name: "language", type: "string", description: "nl (default) or pt." }],
36019
- scopes: ["contacts:read"],
37100
+ scopes: [...mailCampaignOperations.preview.scopes],
36020
37101
  returns: "MailPreview"
36021
37102
  },
36022
37103
  {
@@ -36030,23 +37111,23 @@ var COMMAND_SPECS = [
36030
37111
  },
36031
37112
  {
36032
37113
  path: ["guest-lists", "export"],
36033
- summary: "Export for the caterer, the venue, the church or the RSVP answers (CSV) to stdout, or save the floor plan (PDF) to a file.",
37114
+ summary: "Export for the caterer, the venue, the church or the RSVP answers (CSV) to stdout, or save the floor plan or the paper invitations (PDF) to a file.",
36034
37115
  args: [{ name: "id", required: true, description: "The guest list's id." }],
36035
37116
  flags: [
36036
37117
  {
36037
37118
  name: "kind",
36038
37119
  type: "string",
36039
- description: "caterer (meals and diets per table), venue (who sits where), church (who comes to the ceremony), answers (every RSVP answer, a row per guest) or floor-plan (PDF)."
37120
+ description: "caterer (meals and diets per table), venue (who sits where), church (who comes to the ceremony), answers (every RSVP answer, a row per guest), floor-plan (PDF), invitations (PDF: a card per household with a live RSVP link, its QR and code) or printer (CSV of those cards: household, address lines, country, language, code, URL)."
36040
37121
  },
36041
37122
  {
36042
37123
  name: "out",
36043
37124
  type: "string",
36044
- description: "Where to save the floor plan (default: the name the API gives it)."
37125
+ description: "Where to save a PDF (default: the name the API gives it)."
36045
37126
  }
36046
37127
  ],
36047
37128
  scopes: ["contacts:read"],
36048
37129
  returns: "string",
36049
- notes: "A CSV goes to stdout (`> tables.csv`); the floor plan is saved: `do guest-lists export <id> --kind floor-plan --out plan.pdf`."
37130
+ notes: "A CSV goes to stdout (`> tables.csv`); a PDF is saved: `do guest-lists export <id> --kind invitations --out invitations.pdf`. Mint the links first (`do rsvp-links create`); the QR opens the list's `siteUrl` + /i/<token>, and its `sitePassword` is printed only when set."
36050
37131
  },
36051
37132
  {
36052
37133
  path: ["rsvp-links", "list"],
@@ -38608,7 +39689,7 @@ var HANDLERS = {
38608
39689
  "today earlier": async ({ flags, io, requireClient }) => {
38609
39690
  const area = flagString(flags, "area");
38610
39691
  return {
38611
- data: await requireClient().get("/today/from-earlier", {
39692
+ data: await requireClient().get(pathOf(todayOperations.fromEarlier), {
38612
39693
  query: { from: localDayStart(io.now()).toISOString(), areaId: area }
38613
39694
  }),
38614
39695
  render: renderFromEarlier
@@ -39548,12 +40629,12 @@ var HANDLERS = {
39548
40629
  if (start === undefined)
39549
40630
  throw usageError("block-templates week needs --start");
39550
40631
  return {
39551
- data: await requireClient().get("/block-templates/week", { query: { start } }),
40632
+ data: await requireClient().get(pathOf(blockTemplateOperations.week), { query: { start } }),
39552
40633
  render: renderRecord
39553
40634
  };
39554
40635
  },
39555
40636
  "block-templates defaults": async ({ requireClient }) => ({
39556
- data: await requireClient().post("/block-templates/defaults", {}),
40637
+ data: await requireClient().post(pathOf(blockTemplateOperations.defaults), {}),
39557
40638
  render: renderRecord
39558
40639
  }),
39559
40640
  "time-blocks record": async ({ args, requireClient }) => {
@@ -39638,7 +40719,7 @@ var HANDLERS = {
39638
40719
  };
39639
40720
  },
39640
40721
  "plans list": async ({ flags, requireClient }) => {
39641
- const plans = await requireClient().get("/plans", {
40722
+ const plans = await requireClient().get(pathOf(planOperations.list), {
39642
40723
  query: {
39643
40724
  kind: flagString(flags, "kind"),
39644
40725
  status: flagString(flags, "status"),
@@ -39650,19 +40731,25 @@ var HANDLERS = {
39650
40731
  },
39651
40732
  "plans get": async ({ args, requireClient }) => {
39652
40733
  const id = requireArg(args, 0, "plan id");
39653
- return { data: await requireClient().get(`/plans/${id}`), render: renderPlan };
40734
+ return {
40735
+ data: await requireClient().get(pathOf(planOperations.get, { id })),
40736
+ render: renderPlan
40737
+ };
39654
40738
  },
39655
40739
  "plans propose": async ({ flags, requireClient }) => {
39656
40740
  if (flagString(flags, "data") === undefined)
39657
40741
  throw usageError("plans propose needs --data");
39658
40742
  const body = parseInput(createPlanInputSchema, readDataFlag(flags));
39659
- return { data: await requireClient().post("/plans", body), render: renderPlan };
40743
+ return {
40744
+ data: await requireClient().post(pathOf(planOperations.create), body),
40745
+ render: renderPlan
40746
+ };
39660
40747
  },
39661
40748
  "plans accept": async (ctx) => planDecision(ctx, "accept"),
39662
40749
  "plans dismiss": async (ctx) => planDecision(ctx, "dismiss"),
39663
40750
  "plans undo": async (ctx) => planDecision(ctx, "undo"),
39664
40751
  "plan rules list": async ({ flags, requireClient }) => {
39665
- const rules = await requireClient().get("/planning/rules", {
40752
+ const rules = await requireClient().get(pathOf(planningRuleOperations.list), {
39666
40753
  query: { forUserId: flagString(flags, "for"), kind: flagString(flags, "kind") }
39667
40754
  });
39668
40755
  return { data: rules, render: renderPlanningRules };
@@ -39679,7 +40766,7 @@ var HANDLERS = {
39679
40766
  ...flagString(flags, "for") ? { forUserId: flagString(flags, "for") } : {}
39680
40767
  });
39681
40768
  return {
39682
- data: await requireClient().post("/planning/rules", body),
40769
+ data: await requireClient().post(pathOf(planningRuleOperations.create), body),
39683
40770
  render: renderPlanningRule
39684
40771
  };
39685
40772
  },
@@ -39691,14 +40778,14 @@ var HANDLERS = {
39691
40778
  ...rule !== undefined ? { rule } : {}
39692
40779
  });
39693
40780
  return {
39694
- data: await requireClient().patch(`/planning/rules/${id}`, body),
40781
+ data: await requireClient().patch(pathOf(planningRuleOperations.update, { id }), body),
39695
40782
  render: renderPlanningRule
39696
40783
  };
39697
40784
  },
39698
40785
  "plan rules remove": async ({ args, flags, requireClient }) => {
39699
40786
  const id = requireArg(args, 0, "rule id");
39700
40787
  assertDestructiveAllowed(flags, "remove a planning rule");
39701
- await requireClient().delete(`/planning/rules/${id}`);
40788
+ await requireClient().delete(pathOf(planningRuleOperations.remove, { id }));
39702
40789
  return { data: { id, removed: true }, render: () => `Removed rule ${id}.` };
39703
40790
  },
39704
40791
  "tasks ready": async ({ args, flags, requireClient }) => {
@@ -39940,7 +41027,7 @@ var HANDLERS = {
39940
41027
  };
39941
41028
  },
39942
41029
  "agents list": async ({ requireClient }) => ({
39943
- data: await requireClient().getPage("/agents"),
41030
+ data: await requireClient().getPage(pathOf(agentOperations.list)),
39944
41031
  render: renderList
39945
41032
  }),
39946
41033
  "agent connection get": async ({ requireClient }) => ({
@@ -40056,12 +41143,15 @@ var HANDLERS = {
40056
41143
  };
40057
41144
  },
40058
41145
  "settings get": async ({ requireClient }) => ({
40059
- data: await requireClient().get("/settings"),
41146
+ data: await requireClient().get(pathOf(settingsOperations.get)),
40060
41147
  render: renderRecord
40061
41148
  }),
40062
41149
  "settings update": async ({ flags, requireClient }) => {
40063
41150
  const body = parseInput(updateUserSettingsInputSchema, readDataFlag(flags));
40064
- return { data: await requireClient().patch("/settings", body), render: renderRecord };
41151
+ return {
41152
+ data: await requireClient().patch(pathOf(settingsOperations.update), body),
41153
+ render: renderRecord
41154
+ };
40065
41155
  },
40066
41156
  "modules list": async ({ requireClient }) => {
40067
41157
  const settings = await requireClient().get("/settings");
@@ -40155,7 +41245,10 @@ var HANDLERS = {
40155
41245
  },
40156
41246
  "collection upkeep": async ({ args, requireClient }) => {
40157
41247
  const id = requireArg(args, 0, "item id");
40158
- return { data: await requireClient().get(`/collection/${id}/upkeep`), render: renderRecord };
41248
+ return {
41249
+ data: await requireClient().get(pathOf(collectionUpkeepOperations.list, { id })),
41250
+ render: renderRecord
41251
+ };
40159
41252
  },
40160
41253
  "collection link-task": async ({ args, flags, requireClient }) => {
40161
41254
  const id = requireArg(args, 0, "item id");
@@ -40165,7 +41258,7 @@ var HANDLERS = {
40165
41258
  kind: flagBool(flags, "repair") ? "repairs" : "maintains"
40166
41259
  });
40167
41260
  return {
40168
- data: await requireClient().post(`/collection/${id}/upkeep`, body),
41261
+ data: await requireClient().post(pathOf(collectionUpkeepOperations.link, { id }), body),
40169
41262
  render: renderRecord
40170
41263
  };
40171
41264
  },
@@ -40173,7 +41266,7 @@ var HANDLERS = {
40173
41266
  const id = requireArg(args, 0, "item id");
40174
41267
  const taskId = requireArg(args, 1, "task id");
40175
41268
  return {
40176
- data: await requireClient().delete(`/collection/${id}/upkeep/${taskId}`),
41269
+ data: await requireClient().delete(pathOf(collectionUpkeepOperations.unlink, { id, taskId })),
40177
41270
  render: renderRecord
40178
41271
  };
40179
41272
  },
@@ -40738,6 +41831,23 @@ var HANDLERS = {
40738
41831
  });
40739
41832
  return { data: await requireClient().post("/money/budget/copy", body), render: renderRecord };
40740
41833
  },
41834
+ "money net-worth": async ({ flags, requireClient }) => ({
41835
+ data: await requireClient().get("/money/net-worth", {
41836
+ query: { scope: flagString(flags, "scope"), months: flagString(flags, "months") }
41837
+ }),
41838
+ render: renderRecord
41839
+ }),
41840
+ "money net-worth-set": async ({ args, flags, requireClient }) => {
41841
+ const body = flagString(flags, "data") !== undefined ? parseInput(setMoneyNetWorthValueInputSchema, readDataFlag(flags)) : parseInput(setMoneyNetWorthValueInputSchema, {
41842
+ accountId: requireArg(args, 0, "account id"),
41843
+ value: requireFlag(flags, "value", "money net-worth-set needs --value <amount>"),
41844
+ month: flagString(flags, "month")
41845
+ });
41846
+ return {
41847
+ data: await requireClient().put("/money/net-worth/values", body),
41848
+ render: renderRecord
41849
+ };
41850
+ },
40741
41851
  "money goals": async ({ requireClient }) => ({
40742
41852
  data: await requireClient().get("/money/goals"),
40743
41853
  render: renderList
@@ -41192,12 +42302,20 @@ var HANDLERS = {
41192
42302
  data: await requireClient().delete(`/health/shares/${requireArg(args, 0, "id")}`),
41193
42303
  render: renderRecord
41194
42304
  }),
42305
+ "health withings status": async ({ requireClient }) => ({
42306
+ data: await requireClient().get("/health/withings"),
42307
+ render: renderRecord
42308
+ }),
42309
+ "health withings sync": async ({ requireClient }) => ({
42310
+ data: await requireClient().post("/health/withings/sync", {}),
42311
+ render: renderRecord
42312
+ }),
41195
42313
  "health google status": async ({ requireClient }) => ({
41196
- data: await requireClient().get("/health/google-health"),
42314
+ data: await requireClient().get(pathOf(googleHealthOperations.status)),
41197
42315
  render: renderRecord
41198
42316
  }),
41199
42317
  "health google sync": async ({ requireClient }) => ({
41200
- data: await requireClient().post("/health/google-health/sync", {}),
42318
+ data: await requireClient().post(pathOf(googleHealthOperations.sync), {}),
41201
42319
  render: renderRecord
41202
42320
  }),
41203
42321
  "health consent": async ({ requireClient }) => ({
@@ -41428,7 +42546,9 @@ var HANDLERS = {
41428
42546
  "people google": async ({ flags, requireClient }) => {
41429
42547
  const q = flagString(flags, "q");
41430
42548
  return {
41431
- data: await requireClient().get("/people/google-contacts", { query: q ? { q } : {} }),
42549
+ data: await requireClient().get(pathOf(googleContactsOperations.list), {
42550
+ query: q ? { q } : {}
42551
+ }),
41432
42552
  render: renderRecord
41433
42553
  };
41434
42554
  },
@@ -41438,7 +42558,7 @@ var HANDLERS = {
41438
42558
  }
41439
42559
  const body = parseInput(importGoogleContactsInputSchema, { resourceNames: args });
41440
42560
  return {
41441
- data: await requireClient().post("/people/google-contacts/import", body),
42561
+ data: await requireClient().post(pathOf(googleContactsOperations.import), body),
41442
42562
  render: renderRecord
41443
42563
  };
41444
42564
  },
@@ -41586,7 +42706,7 @@ var HANDLERS = {
41586
42706
  "rsvp-questions presets": async ({ flags, requireClient }) => {
41587
42707
  const body = parseInput(addRsvpPresetsInputSchema, readDataFlag(flags));
41588
42708
  return {
41589
- data: await requireClient().post("/rsvp-questions/presets", body),
42709
+ data: await requireClient().post(pathOf(rsvpQuestionOperations.presets), body),
41590
42710
  render: renderRecord
41591
42711
  };
41592
42712
  },
@@ -41599,11 +42719,17 @@ var HANDLERS = {
41599
42719
  guestListId: list,
41600
42720
  ...household ? { householdId: household } : {}
41601
42721
  });
41602
- return { data: await requireClient().get("/rsvp-answers", { query }), render: renderRecord };
42722
+ return {
42723
+ data: await requireClient().get(pathOf(rsvpAnswerOperations.list), { query }),
42724
+ render: renderRecord
42725
+ };
41603
42726
  },
41604
42727
  "rsvp-answers set": async ({ flags, requireClient }) => {
41605
42728
  const body = parseInput(setRsvpAnswersInputSchema, readDataFlag(flags));
41606
- return { data: await requireClient().put("/rsvp-answers", body), render: renderRecord };
42729
+ return {
42730
+ data: await requireClient().put(pathOf(rsvpAnswerOperations.set), body),
42731
+ render: renderRecord
42732
+ };
41607
42733
  },
41608
42734
  "seating-proposals list": async ({ flags, requireClient }) => {
41609
42735
  const list = flagString(flags, "list");
@@ -41642,23 +42768,31 @@ var HANDLERS = {
41642
42768
  throw usageError("mail-campaigns list needs --list <guest list id>");
41643
42769
  parseInput(mailCampaignsQuerySchema, { guestListId: list });
41644
42770
  return {
41645
- data: await requireClient().get("/mail-campaigns", { query: { guestListId: list } }),
42771
+ data: await requireClient().get(pathOf(mailCampaignOperations.list), {
42772
+ query: { guestListId: list }
42773
+ }),
41646
42774
  render: renderRecord
41647
42775
  };
41648
42776
  },
41649
42777
  "mail-campaigns get": async ({ args, requireClient }) => {
41650
42778
  const id = requireArg(args, 0, "campaign id");
41651
- return { data: await requireClient().get(`/mail-campaigns/${id}`), render: renderRecord };
42779
+ return {
42780
+ data: await requireClient().get(pathOf(mailCampaignOperations.get, { id })),
42781
+ render: renderRecord
42782
+ };
41652
42783
  },
41653
42784
  "mail-campaigns create": async ({ flags, requireClient }) => {
41654
42785
  const body = parseInput(createMailCampaignInputSchema, { id: ulid3(), ...readDataFlag(flags) });
41655
- return { data: await requireClient().post("/mail-campaigns", body), render: renderRecord };
42786
+ return {
42787
+ data: await requireClient().post(pathOf(mailCampaignOperations.create), body),
42788
+ render: renderRecord
42789
+ };
41656
42790
  },
41657
42791
  "mail-campaigns update": async ({ args, flags, requireClient }) => {
41658
42792
  const id = requireArg(args, 0, "campaign id");
41659
42793
  const body = parseInput(updateMailCampaignInputSchema, readDataFlag(flags));
41660
42794
  return {
41661
- data: await requireClient().patch(`/mail-campaigns/${id}`, body),
42795
+ data: await requireClient().patch(pathOf(mailCampaignOperations.update, { id }), body),
41662
42796
  render: renderRecord
41663
42797
  };
41664
42798
  },
@@ -41668,7 +42802,7 @@ var HANDLERS = {
41668
42802
  language: flagString(flags, "language") ?? "nl"
41669
42803
  });
41670
42804
  return {
41671
- data: await requireClient().get(`/mail-campaigns/${id}/preview`, { query }),
42805
+ data: await requireClient().get(pathOf(mailCampaignOperations.preview, { id }), { query }),
41672
42806
  render: renderRecord
41673
42807
  };
41674
42808
  },
@@ -41683,9 +42817,9 @@ var HANDLERS = {
41683
42817
  const id = requireArg(args, 0, "guest list id");
41684
42818
  const kind = parseInput(guestExportKindSchema, flagString(flags, "kind") ?? "venue");
41685
42819
  const path = `/guest-lists/${id}/exports/${kind}`;
41686
- if (kind === "floor-plan") {
42820
+ if (kind === "floor-plan" || kind === "invitations") {
41687
42821
  const file = await requireClient().getBytes(path);
41688
- const out = flagString(flags, "out") ?? basename2(file.name ?? "floor-plan.pdf");
42822
+ const out = flagString(flags, "out") ?? basename2(file.name ?? `${kind}.pdf`);
41689
42823
  writeFileSync2(out, file.bytes);
41690
42824
  return {
41691
42825
  data: { id, kind, saved: out, size: file.bytes.byteLength, type: file.contentType },
@@ -42143,7 +43277,10 @@ async function planDecision({ args, requireClient }, verb) {
42143
43277
  const id = requireArg(args, 0, "plan id");
42144
43278
  const itemIds = args.slice(1);
42145
43279
  const body = parseInput(planDecisionInputSchema, itemIds.length > 0 ? { itemIds } : {});
42146
- return { data: await requireClient().post(`/plans/${id}/${verb}`, body), render: renderPlan };
43280
+ return {
43281
+ data: await requireClient().post(pathOf(planOperations[verb], { id }), body),
43282
+ render: renderPlan
43283
+ };
42147
43284
  }
42148
43285
  function parseInput(schema, value) {
42149
43286
  const result = schema.safeParse(value);