@robbeverhelst/do 1.343.0 → 1.345.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 +1152 -82
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -14420,7 +14420,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14420
14420
 
14421
14421
  // src/meta.ts
14422
14422
  var CLI_NAME = "do";
14423
- var VERSION = "1.343.0";
14423
+ var VERSION = "1.345.0";
14424
14424
  var API_PREFIX = "/api/v1";
14425
14425
  var DEFAULT_API_URL = "http://localhost:3001";
14426
14426
 
@@ -17984,9 +17984,13 @@ var tasks = pgTable("tasks", {
17984
17984
  readyAt: timestamp("ready_at", { withTimezone: true }),
17985
17985
  readyNote: text("ready_note"),
17986
17986
  moveCount: integer2("move_count").notNull().default(0),
17987
- movedFrom: timestamp("moved_from", { withTimezone: true })
17987
+ movedFrom: timestamp("moved_from", { withTimezone: true }),
17988
+ seriesId: text("series_id"),
17989
+ seriesLabel: text("series_label"),
17990
+ seriesSlot: date5("series_slot", { mode: "string" })
17988
17991
  }, (t) => [
17989
17992
  index("tasks_tenant_project_idx").on(t.tenantId, t.projectId),
17993
+ uniqueIndex("tasks_series_slot_uniq").on(t.seriesId, t.seriesSlot).where(sql`${t.seriesId} IS NOT NULL`),
17990
17994
  unique("tasks_tenant_number_uniq").on(t.tenantId, t.number),
17991
17995
  index("tasks_tenant_place_idx").on(t.tenantId, t.placeId),
17992
17996
  index("tasks_milestone_idx").on(t.milestoneId),
@@ -18025,11 +18029,13 @@ var habits = pgTable("habits", {
18025
18029
  retiredAt: timestamp("retired_at", { withTimezone: true }),
18026
18030
  eventSourceId: text("event_source_id"),
18027
18031
  eventMatch: text("event_match"),
18028
- eventOffset: text("event_offset")
18032
+ eventOffset: text("event_offset"),
18033
+ durationMinutes: integer2("duration_minutes")
18029
18034
  }, (t) => [
18030
18035
  index("habits_tenant_idx").on(t.tenantId),
18031
18036
  index("habits_tenant_area_idx").on(t.tenantId, t.areaId),
18032
- check2("habits_time_of_day_chk", sql`${t.timeOfDay} IS NULL OR ${t.timeOfDay} IN ('morning', 'midday', 'evening') OR ${t.timeOfDay} ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'`)
18037
+ check2("habits_time_of_day_chk", sql`${t.timeOfDay} IS NULL OR ${t.timeOfDay} IN ('morning', 'midday', 'evening') OR ${t.timeOfDay} ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'`),
18038
+ check2("habits_duration_minutes_chk", sql`${t.durationMinutes} IS NULL OR ${t.durationMinutes} BETWEEN 1 AND 600`)
18033
18039
  ]);
18034
18040
  var habitEntries = pgTable("habit_entries", {
18035
18041
  id: text("id").primaryKey(),
@@ -18083,7 +18089,7 @@ var occurrences = pgTable("occurrences", {
18083
18089
 
18084
18090
  // ../../packages/schema/src/db/core.ts
18085
18091
  var routineKind = pgEnum("routine_kind", ["sequence", "time_blocked", "checklist"]);
18086
- var routineStepRef = pgEnum("routine_step_ref", ["free", "habit", "task"]);
18092
+ var routineStepRef = pgEnum("routine_step_ref", ["free", "habit", "task", "routine"]);
18087
18093
  var projectStatus = pgEnum("project_status", [
18088
18094
  "planning",
18089
18095
  "active",
@@ -18093,6 +18099,12 @@ var projectStatus = pgEnum("project_status", [
18093
18099
  ]);
18094
18100
  var settingsTheme = pgEnum("settings_theme", ["system", "light", "dark"]);
18095
18101
  var settingsDensity = pgEnum("settings_density", ["comfortable", "compact"]);
18102
+ var settingsGlass = pgEnum("settings_glass", ["liquid", "frosted", "solid"]);
18103
+ var settingsThemePack = pgEnum("settings_theme_pack", ["do", "skyrim", "pokemon"]);
18104
+ var settingsThemePackScope = pgEnum("settings_theme_pack_scope", [
18105
+ "section",
18106
+ "everywhere"
18107
+ ]);
18096
18108
  var settingsAccent = pgEnum("settings_accent", ["teal", "purple", "amber", "rose"]);
18097
18109
  var routineRunStatus = pgEnum("routine_run_status", [
18098
18110
  "in_progress",
@@ -18126,6 +18138,8 @@ var routineSteps = pgTable("routine_steps", {
18126
18138
  refKind: routineStepRef("ref_kind").notNull().default("free"),
18127
18139
  habitId: text("habit_id").references(() => habits.id),
18128
18140
  taskId: text("task_id").references(() => tasks.id),
18141
+ childRoutineId: text("child_routine_id").references(() => routines.id),
18142
+ optional: boolean4("optional").notNull().default(false),
18129
18143
  sortOrder: integer2("sort_order").notNull().default(0),
18130
18144
  durationMinutes: integer2("duration_minutes"),
18131
18145
  notes: text("notes"),
@@ -18133,11 +18147,14 @@ var routineSteps = pgTable("routine_steps", {
18133
18147
  }, (t) => [
18134
18148
  index("routine_steps_routine_order_idx").on(t.routineId, t.sortOrder),
18135
18149
  index("routine_steps_habit_idx").on(t.habitId),
18150
+ index("routine_steps_child_routine_idx").on(t.childRoutineId),
18136
18151
  check2("routine_steps_ref_kind_chk", sql`(
18137
- (${t.refKind} = 'free' AND ${t.habitId} IS NULL AND ${t.taskId} IS NULL) OR
18138
- (${t.refKind} = 'habit' AND ${t.habitId} IS NOT NULL AND ${t.taskId} IS NULL) OR
18139
- (${t.refKind} = 'task' AND ${t.taskId} IS NOT NULL AND ${t.habitId} IS NULL)
18140
- )`)
18152
+ (${t.refKind} = 'free' AND ${t.habitId} IS NULL AND ${t.taskId} IS NULL AND ${t.childRoutineId} IS NULL) OR
18153
+ (${t.refKind} = 'habit' AND ${t.habitId} IS NOT NULL AND ${t.taskId} IS NULL AND ${t.childRoutineId} IS NULL) OR
18154
+ (${t.refKind} = 'task' AND ${t.taskId} IS NOT NULL AND ${t.habitId} IS NULL AND ${t.childRoutineId} IS NULL) OR
18155
+ (${t.refKind} = 'routine' AND ${t.childRoutineId} IS NOT NULL AND ${t.habitId} IS NULL AND ${t.taskId} IS NULL)
18156
+ )`),
18157
+ check2("routine_steps_not_self_chk", sql`${t.childRoutineId} IS NULL OR ${t.childRoutineId} <> ${t.routineId}`)
18141
18158
  ]);
18142
18159
  var routineRuns = pgTable("routine_runs", {
18143
18160
  id: text("id").primaryKey(),
@@ -18157,6 +18174,9 @@ var routineRunSteps = pgTable("routine_run_steps", {
18157
18174
  refKind: routineStepRef("ref_kind").notNull().default("free"),
18158
18175
  habitId: text("habit_id").references(() => habits.id),
18159
18176
  taskId: text("task_id").references(() => tasks.id),
18177
+ childRoutineId: text("child_routine_id").references(() => routines.id),
18178
+ groupStepId: text("group_step_id").references(() => routineRunSteps.id),
18179
+ optional: boolean4("optional").notNull().default(false),
18160
18180
  reason: text("reason"),
18161
18181
  habitEntryId: text("habit_entry_id").references(() => habitEntries.id),
18162
18182
  sortOrder: integer2("sort_order").notNull().default(0),
@@ -18167,7 +18187,8 @@ var routineRunSteps = pgTable("routine_run_steps", {
18167
18187
  completedAt: timestamp("completed_at", { withTimezone: true })
18168
18188
  }, (t) => [
18169
18189
  index("routine_run_steps_run_order_idx").on(t.runId, t.sortOrder),
18170
- check2("routine_run_steps_ref_kind_chk", sql`(${t.refKind} <> 'habit' OR ${t.habitId} IS NOT NULL)`)
18190
+ check2("routine_run_steps_ref_kind_chk", sql`(${t.refKind} <> 'habit' OR ${t.habitId} IS NOT NULL)`),
18191
+ check2("routine_run_steps_routine_chk", sql`(${t.refKind} <> 'routine' OR ${t.childRoutineId} IS NOT NULL)`)
18171
18192
  ]);
18172
18193
  var projects = pgTable("projects", {
18173
18194
  ...leafColumns(),
@@ -18222,6 +18243,14 @@ var userSettings = pgTable("user_settings", {
18222
18243
  calendarZoom: integer2("calendar_zoom"),
18223
18244
  lifeBlockSharing: jsonb("life_block_sharing").$type(),
18224
18245
  recentSearches: jsonb("recent_searches").$type().notNull().default([]),
18246
+ glass: settingsGlass("glass").notNull().default("liquid"),
18247
+ followSky: boolean4("follow_sky").notNull().default(true),
18248
+ nightUntilWake: boolean4("night_until_wake").notNull().default(false),
18249
+ largerText: boolean4("larger_text").notNull().default(false),
18250
+ reduceMotion: boolean4("reduce_motion").notNull().default(false),
18251
+ showStreakNumbers: boolean4("show_streak_numbers").notNull().default(true),
18252
+ themePack: settingsThemePack("theme_pack").notNull().default("do"),
18253
+ themePackScope: settingsThemePackScope("theme_pack_scope").notNull().default("section"),
18225
18254
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18226
18255
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
18227
18256
  }, (t) => [unique("user_settings_tenant_user_uniq").on(t.tenantId, t.userId)]);
@@ -19000,14 +19029,24 @@ var recipeIngredients = pgTable("recipe_ingredients", {
19000
19029
  index("recipe_ingredients_tenant_idx").on(t.tenantId),
19001
19030
  index("recipe_ingredients_product_idx").on(t.productId)
19002
19031
  ]);
19032
+ var MEAL_PLAN_STATES = ["planned", "cooked", "out", "unsure", "draft"];
19003
19033
  var mealPlans = pgTable("meal_plans", {
19004
19034
  ...leafColumns(),
19005
19035
  recipeId: text("recipe_id").references(() => recipes.id),
19006
19036
  plannedFor: timestamp("planned_for", { withTimezone: true }).notNull(),
19007
19037
  mealType: mealType("meal_type").notNull().default("dinner"),
19008
19038
  servings: integer2("servings"),
19009
- notes: text("notes")
19010
- }, (t) => [index("meal_plans_tenant_planned_idx").on(t.tenantId, t.plannedFor)]);
19039
+ notes: text("notes"),
19040
+ state: text("state", { enum: MEAL_PLAN_STATES }).notNull().default("planned"),
19041
+ cookUserId: text("cook_user_id").references(() => users.id, { onDelete: "set null" }),
19042
+ company: text("company"),
19043
+ leftoverPortions: integer2("leftover_portions"),
19044
+ leftoverEatBy: date5("leftover_eat_by", { mode: "string" }),
19045
+ leftoverFrozen: boolean4("leftover_frozen").notNull().default(false)
19046
+ }, (t) => [
19047
+ index("meal_plans_tenant_planned_idx").on(t.tenantId, t.plannedFor),
19048
+ check2("meal_plans_state_check", sql`${t.state} IN ('planned', 'cooked', 'out', 'unsure', 'draft')`)
19049
+ ]);
19011
19050
 
19012
19051
  // ../../packages/schema/src/db/reflections.ts
19013
19052
  var reflectionKind = pgEnum("reflection_kind", ["daily", "weekly"]);
@@ -21357,7 +21396,8 @@ var habitSchema = createSelectSchema(habits, {
21357
21396
  retiredAt: exports_coerce.date().nullable().default(null),
21358
21397
  eventSourceId: string2().nullable().default(null),
21359
21398
  eventMatch: string2().nullable().default(null),
21360
- eventOffset: string2().nullable().default(null)
21399
+ eventOffset: string2().nullable().default(null),
21400
+ durationMinutes: number2().int().nullable().default(null)
21361
21401
  });
21362
21402
  var habitEntrySchema = createSelectSchema(habitEntries, {
21363
21403
  occurredAt: exports_coerce.date(),
@@ -21374,6 +21414,8 @@ var habitTimeOfDaySchema = union([
21374
21414
  _enum2(HABIT_PARTS_OF_DAY),
21375
21415
  string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use a 24-hour time, like 07:30")
21376
21416
  ]);
21417
+ var HABIT_DURATION_MAX_MINUTES = 600;
21418
+ var habitDurationMinutesSchema = number2().int().min(1).max(HABIT_DURATION_MAX_MINUTES);
21377
21419
  var habitAliasSchema = string2().trim().min(1).max(60);
21378
21420
  var habitAliasesSchema = array(habitAliasSchema).max(12).transform((aliases) => aliases.filter((a, i) => aliases.findIndex((b) => b.toLowerCase() === a.toLowerCase()) === i));
21379
21421
  var habitScheduleSchema = discriminatedUnion("kind", [
@@ -21412,7 +21454,8 @@ var createHabitInputSchema = createInsertSchema(habits, {
21412
21454
  areaId: ulidSchema.nullish(),
21413
21455
  aliases: habitAliasesSchema.optional(),
21414
21456
  timeOfDay: habitTimeOfDaySchema.nullish(),
21415
- remind: boolean2().optional()
21457
+ remind: boolean2().optional(),
21458
+ durationMinutes: habitDurationMinutesSchema.nullish()
21416
21459
  }).omit({ tenantId: true, recurrenceRuleId: true, retiredAt: true, ...eventColumns }).extend({
21417
21460
  title: string2().min(1),
21418
21461
  schedule: habitScheduleSchema.optional(),
@@ -21428,7 +21471,8 @@ var updateHabitInputSchema = createUpdateSchema(habits, {
21428
21471
  areaId: ulidSchema.nullish(),
21429
21472
  aliases: habitAliasesSchema.optional(),
21430
21473
  timeOfDay: habitTimeOfDaySchema.nullish(),
21431
- remind: boolean2().optional()
21474
+ remind: boolean2().optional(),
21475
+ durationMinutes: habitDurationMinutesSchema.nullish()
21432
21476
  }).omit({
21433
21477
  id: true,
21434
21478
  tenantId: true,
@@ -21479,7 +21523,7 @@ var habitLastCheckInSchema = object({
21479
21523
  note: string2().nullish()
21480
21524
  });
21481
21525
  var habitPauseSchema = object({
21482
- until: exports_iso.date(),
21526
+ until: exports_iso.date().nullable(),
21483
21527
  by: _enum2(["habit", "away"])
21484
21528
  });
21485
21529
  var habitTodaySchema = object({
@@ -21495,6 +21539,7 @@ var habitSummarySchema = habitSchema.extend({
21495
21539
  inRoutines: array(habitRoutineRefSchema).default([]),
21496
21540
  lastCheckIn: habitLastCheckInSchema.nullable().default(null),
21497
21541
  pause: habitPauseSchema.nullable().default(null),
21542
+ paused: boolean2().default(false),
21498
21543
  today: habitTodaySchema.default({ due: true, done: false }),
21499
21544
  schedule: habitScheduleSchema.default({ kind: "daily" }),
21500
21545
  nextDue: array(exports_iso.date()).default([])
@@ -21522,6 +21567,20 @@ var habitYearSchema = object({
21522
21567
  marks: array(habitDayMarkSchema)
21523
21568
  }))
21524
21569
  });
21570
+ var HABIT_DUE_MAX_DAYS = 62;
21571
+ var habitDueQuerySchema = object({ from: exports_iso.date(), to: exports_iso.date() }).strict().refine((q) => q.from <= q.to, { message: "`to` is before `from`", path: ["to"] }).refine((q) => (Date.parse(q.to) - Date.parse(q.from)) / 86400000 + 1 <= HABIT_DUE_MAX_DAYS, {
21572
+ message: `at most ${HABIT_DUE_MAX_DAYS} days`,
21573
+ path: ["to"]
21574
+ });
21575
+ var habitDueDateSchema = object({
21576
+ habitId: ulidSchema,
21577
+ title: string2(),
21578
+ date: exports_iso.date(),
21579
+ time: string2().regex(/^\d{2}:\d{2}$/).nullable(),
21580
+ scheduleLabel: string2().min(1),
21581
+ daily: boolean2(),
21582
+ streakLabel: string2().min(1)
21583
+ });
21525
21584
  var habitEntriesQuerySchema = object({
21526
21585
  before: exports_coerce.date().optional(),
21527
21586
  limit: exports_coerce.number().int().min(1).max(200).default(50)
@@ -21552,7 +21611,7 @@ var habitSkipInputSchema = object({
21552
21611
  date: exports_iso.date().optional(),
21553
21612
  note: habitEntryNoteSchema.nullish()
21554
21613
  }).strict();
21555
- var habitPauseInputSchema = object({ until: exports_iso.date() }).strict();
21614
+ var habitPauseInputSchema = object({ until: exports_iso.date().nullish() }).strict();
21556
21615
  var awayInputSchema = object({ until: exports_iso.date().nullable() }).strict();
21557
21616
  var awayStateSchema = object({
21558
21617
  until: exports_iso.date().nullable()
@@ -21896,6 +21955,78 @@ var taskBlockerSchema = object({
21896
21955
  dueAt: exports_coerce.date().nullable()
21897
21956
  });
21898
21957
 
21958
+ // ../../packages/schema/src/db/task-series.ts
21959
+ var taskSeries = pgTable("task_series", {
21960
+ id: text("id").primaryKey(),
21961
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
21962
+ title: text("title").notNull(),
21963
+ recurrenceRuleId: text("recurrence_rule_id").notNull().references(() => recurrenceRules.id),
21964
+ startsOn: date5("starts_on", { mode: "string" }).notNull(),
21965
+ occurrenceLabel: text("occurrence_label").notNull().default("date"),
21966
+ leadDays: integer2("lead_days").notNull().default(14),
21967
+ areaId: text("area_id").references(() => areas.id, { onDelete: "set null" }),
21968
+ projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),
21969
+ tags: text("tags").array().notNull().default([]),
21970
+ priority: workPriority("priority").notNull().default("none"),
21971
+ durationMinutes: integer2("duration_minutes"),
21972
+ ownerUserId: text("owner_user_id").references(() => users.id, { onDelete: "set null" }),
21973
+ ownerAgentId: text("owner_agent_id").references(() => agents.id, { onDelete: "set null" }),
21974
+ subtaskTitles: text("subtask_titles").array().notNull().default([]),
21975
+ active: boolean4("active").notNull().default(true),
21976
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
21977
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
21978
+ }, (t) => [
21979
+ index("task_series_tenant_idx").on(t.tenantId, t.active)
21980
+ ]);
21981
+
21982
+ // ../../packages/schema/src/contracts/task-series.ts
21983
+ var taskSeriesRuleSchema = object({
21984
+ freq: _enum2(["weekly", "monthly", "yearly"]),
21985
+ interval: number2().int().min(1).max(52).default(1)
21986
+ }).strict();
21987
+ var taskSeriesLabelSchema = _enum2(["quarter", "month", "date"]);
21988
+ var leadDaysSchema2 = number2().int().min(0).max(365);
21989
+ var taskSeriesRowSchema = createSelectSchema(taskSeries, {
21990
+ occurrenceLabel: taskSeriesLabelSchema,
21991
+ startsOn: exports_iso.date(),
21992
+ createdAt: exports_coerce.date(),
21993
+ updatedAt: exports_coerce.date()
21994
+ });
21995
+ var makeTaskSeriesInputSchema = object({
21996
+ rule: taskSeriesRuleSchema,
21997
+ occurrenceLabel: taskSeriesLabelSchema.default("date"),
21998
+ leadDays: leadDaysSchema2.default(14)
21999
+ }).strict();
22000
+ var updateTaskSeriesInputSchema = object({
22001
+ title: string2().trim().min(1).max(500).optional(),
22002
+ rule: taskSeriesRuleSchema.optional(),
22003
+ occurrenceLabel: taskSeriesLabelSchema.optional(),
22004
+ leadDays: leadDaysSchema2.optional(),
22005
+ active: boolean2().optional()
22006
+ }).strict();
22007
+ var taskSeriesItemSchema = object({
22008
+ taskId: ulidSchema.nullable(),
22009
+ label: string2(),
22010
+ slot: exports_iso.date(),
22011
+ status: taskStatusSchema.nullable(),
22012
+ completedAt: exports_coerce.date().nullable()
22013
+ });
22014
+ var taskSeriesStripSchema = object({
22015
+ id: ulidSchema,
22016
+ title: string2(),
22017
+ rhythm: string2(),
22018
+ items: array(taskSeriesItemSchema),
22019
+ nextMadeOn: exports_iso.date().nullable()
22020
+ });
22021
+ var taskSeriesViewSchema = taskSeriesStripSchema.extend({
22022
+ rule: taskSeriesRuleSchema,
22023
+ occurrenceLabel: taskSeriesLabelSchema,
22024
+ leadDays: number2().int(),
22025
+ startsOn: exports_iso.date(),
22026
+ active: boolean2(),
22027
+ made: number2().int().nonnegative()
22028
+ });
22029
+
21899
22030
  // ../../packages/schema/src/contracts/task-source.ts
21900
22031
  var taskSourceSchema = object({
21901
22032
  kind: string2().trim().min(1).max(40),
@@ -22068,7 +22199,10 @@ var taskSchema = createSelectSchema(tasks, {
22068
22199
  movedFrom: exports_coerce.date().nullable().default(null),
22069
22200
  waitingFor: string2().nullable().default(null),
22070
22201
  nudgedAt: exports_coerce.date().nullable().default(null),
22071
- nudgeCount: number2().int().nonnegative().default(0)
22202
+ nudgeCount: number2().int().nonnegative().default(0),
22203
+ seriesId: string2().nullable().default(null),
22204
+ seriesLabel: string2().nullable().default(null),
22205
+ seriesSlot: exports_iso.date().nullable().default(null)
22072
22206
  });
22073
22207
  var PARTY_RE = /^(?:(me)|(member|agent|contact):([^/]+?))(?:\/(.+))?$/;
22074
22208
  function partySchema(allowDesk, allowContact = false) {
@@ -22207,7 +22341,8 @@ var taskViewSchema = taskSchema.omit({
22207
22341
  blockedBy: array(taskBlockerSchema).default([]),
22208
22342
  blocks: number2().int().nonnegative().default(0),
22209
22343
  isBlocked: boolean2().default(false),
22210
- recurrence: taskRecurrenceSchema.nullable().default(null)
22344
+ recurrence: taskRecurrenceSchema.nullable().default(null),
22345
+ series: taskSeriesStripSchema.nullable().default(null)
22211
22346
  });
22212
22347
  var taskProjectIdSchema = ulidSchema.nullish();
22213
22348
  var taskAreaIdSchema = ulidSchema.nullish();
@@ -22243,6 +22378,9 @@ var createTaskInputSchema = createInsertSchema(tasks, {
22243
22378
  completedByUserId: true,
22244
22379
  completedByAgentId: true,
22245
22380
  filingSuggestion: true,
22381
+ seriesId: true,
22382
+ seriesLabel: true,
22383
+ seriesSlot: true,
22246
22384
  proposedAt: true,
22247
22385
  proposedDesk: true,
22248
22386
  notNowUntil: true,
@@ -22297,6 +22435,9 @@ var updateTaskInputSchema = createUpdateSchema(tasks, {
22297
22435
  completedByUserId: true,
22298
22436
  completedByAgentId: true,
22299
22437
  filingSuggestion: true,
22438
+ seriesId: true,
22439
+ seriesLabel: true,
22440
+ seriesSlot: true,
22300
22441
  proposedAt: true,
22301
22442
  proposedDesk: true,
22302
22443
  readyAt: true,
@@ -22426,7 +22567,9 @@ var attachTaskInputSchema = object({ taskId: ulidSchema });
22426
22567
  var routineDayTimesSchema = partialRecord(_enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]), string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/));
22427
22568
  var routineSchema = createSelectSchema(routines);
22428
22569
  var routineStepSchema = createSelectSchema(routineSteps, {
22429
- createdAt: exports_coerce.date()
22570
+ createdAt: exports_coerce.date(),
22571
+ childRoutineId: string2().nullish(),
22572
+ optional: boolean2().optional()
22430
22573
  });
22431
22574
  var routineStepInputSchema = object({
22432
22575
  id: ulidSchema,
@@ -22434,12 +22577,36 @@ var routineStepInputSchema = object({
22434
22577
  refKind: routineStepRefSchema.default("free"),
22435
22578
  habitId: ulidSchema.nullish(),
22436
22579
  taskId: ulidSchema.nullish(),
22580
+ childRoutineId: ulidSchema.nullish(),
22437
22581
  newHabit: createHabitInputSchema.optional(),
22582
+ optional: boolean2().optional(),
22438
22583
  sortOrder: number2().int().min(0).optional(),
22439
22584
  durationMinutes: number2().int().positive().nullish(),
22440
22585
  notes: string2().nullish()
22441
22586
  }).superRefine((step, ctx) => {
22442
- if (step.refKind === "habit") {
22587
+ if (step.refKind !== "routine" && step.childRoutineId) {
22588
+ ctx.addIssue({
22589
+ code: ZodIssueCode.custom,
22590
+ message: "only a routine step sets childRoutineId",
22591
+ path: ["childRoutineId"]
22592
+ });
22593
+ }
22594
+ if (step.refKind === "routine") {
22595
+ if (!step.childRoutineId) {
22596
+ ctx.addIssue({
22597
+ code: ZodIssueCode.custom,
22598
+ message: "a routine step needs childRoutineId",
22599
+ path: ["childRoutineId"]
22600
+ });
22601
+ }
22602
+ if (step.habitId || step.taskId || step.newHabit) {
22603
+ ctx.addIssue({
22604
+ code: ZodIssueCode.custom,
22605
+ message: "a routine step cannot set a habit/task link",
22606
+ path: ["refKind"]
22607
+ });
22608
+ }
22609
+ } else if (step.refKind === "habit") {
22443
22610
  const linkId = step.newHabit?.id ?? step.habitId;
22444
22611
  if (!linkId) {
22445
22612
  ctx.addIssue({
@@ -22495,6 +22662,16 @@ var createRoutineInputSchema = createInsertSchema(routines, {
22495
22662
  parentId: ulidSchema.nullish(),
22496
22663
  steps: array(routineStepInputSchema).optional(),
22497
22664
  dayTimes: routineDayTimesSchema.optional()
22665
+ }).superRefine((input, ctx) => {
22666
+ for (const [i, step] of (input.steps ?? []).entries()) {
22667
+ if (step.childRoutineId && step.childRoutineId === input.id) {
22668
+ ctx.addIssue({
22669
+ code: ZodIssueCode.custom,
22670
+ message: "a routine cannot contain itself",
22671
+ path: ["steps", i, "childRoutineId"]
22672
+ });
22673
+ }
22674
+ }
22498
22675
  });
22499
22676
  var updateRoutineInputSchema = createUpdateSchema(routines, {
22500
22677
  areaId: ulidSchema.nullish()
@@ -22525,7 +22702,10 @@ var routineRunSchema = createSelectSchema(routineRuns, {
22525
22702
  var routineRunStepSchema = createSelectSchema(routineRunSteps, {
22526
22703
  completedAt: exports_coerce.date().nullable(),
22527
22704
  taskId: string2().nullable().default(null),
22528
- reason: string2().nullable().default(null)
22705
+ reason: string2().nullable().default(null),
22706
+ childRoutineId: string2().nullish(),
22707
+ groupStepId: string2().nullish(),
22708
+ optional: boolean2().optional()
22529
22709
  });
22530
22710
  var routineRunStepViewSchema = routineRunStepSchema.extend({
22531
22711
  habit: habitSummarySchema.nullable().default(null),
@@ -22615,14 +22795,16 @@ var addRoutineTaskStepInputSchema = object({
22615
22795
  var reorderRoutineStepsInputSchema = object({
22616
22796
  stepIds: array(ulidSchema).min(1).max(200).refine((ids) => new Set(ids).size === ids.length, "stepIds must not repeat")
22617
22797
  }).strict();
22618
- var routineTodayStepKindSchema = _enum2(["step", "habit", "task"]);
22798
+ var routineTodayStepKindSchema = _enum2(["step", "habit", "task", "routine"]);
22619
22799
  var routineTodayStepSchema = object({
22620
22800
  n: number2().int().positive(),
22621
22801
  title: string2(),
22622
22802
  kind: routineTodayStepKindSchema,
22623
22803
  meta: string2().nullable(),
22624
22804
  minutes: number2().int().nullable(),
22625
- done: boolean2()
22805
+ done: boolean2(),
22806
+ depth: number2().int().nonnegative().default(0),
22807
+ optional: boolean2().default(false)
22626
22808
  });
22627
22809
  var routineTodayStateSchema = _enum2(["done", "running", "upcoming", "resting"]);
22628
22810
  var routineTodaySchema = object({
@@ -24009,7 +24191,7 @@ var timeBlockViewSchema = timeBlockSchema.omit({
24009
24191
  var timeBlockStepSchema = object({
24010
24192
  id: string2(),
24011
24193
  title: string2(),
24012
- refKind: _enum2(["free", "habit", "task"]),
24194
+ refKind: routineStepRefSchema,
24013
24195
  status: _enum2(["done", "skipped", "open"]),
24014
24196
  at: exports_coerce.date().nullable(),
24015
24197
  habitId: string2().nullable().optional()
@@ -24229,6 +24411,7 @@ var calendarEventLinks = pgTable("calendar_event_links", {
24229
24411
  connectionId: text("connection_id").notNull().references(() => connections.id, { onDelete: "cascade" }),
24230
24412
  nodeId: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }),
24231
24413
  purpose: calendarLinkPurpose("purpose").notNull().default("block"),
24414
+ slot: text("slot").notNull().default(""),
24232
24415
  externalCalendarId: text("external_calendar_id").notNull(),
24233
24416
  externalEventId: text("external_event_id").notNull(),
24234
24417
  etag: text("etag"),
@@ -24237,7 +24420,7 @@ var calendarEventLinks = pgTable("calendar_event_links", {
24237
24420
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
24238
24421
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
24239
24422
  }, (t) => [
24240
- unique("calendar_event_links_connection_node_uniq").on(t.connectionId, t.nodeId, t.purpose),
24423
+ unique("calendar_event_links_connection_node_uniq").on(t.connectionId, t.nodeId, t.purpose, t.slot),
24241
24424
  unique("calendar_event_links_connection_event_uniq").on(t.connectionId, t.externalEventId),
24242
24425
  index("calendar_event_links_tenant_connection_idx").on(t.tenantId, t.connectionId),
24243
24426
  index("calendar_event_links_node_idx").on(t.nodeId)
@@ -24299,7 +24482,8 @@ var calendarConnectionCalendars = pgTable("calendar_connection_calendars", {
24299
24482
  var calendarEventLinkSchema = createSelectSchema(calendarEventLinks, {
24300
24483
  createdAt: exports_coerce.date(),
24301
24484
  updatedAt: exports_coerce.date(),
24302
- lastSyncedAt: exports_coerce.date()
24485
+ lastSyncedAt: exports_coerce.date(),
24486
+ slot: string2().default("")
24303
24487
  });
24304
24488
  var calendarSyncResultSchema = object({
24305
24489
  connectionId: string2(),
@@ -29876,6 +30060,27 @@ var habitOperations = defineOperations({
29876
30060
  response: { description: "Today's habits and routines." },
29877
30061
  operationId: "habits_today"
29878
30062
  },
30063
+ due: {
30064
+ method: "get",
30065
+ path: "/habits/due",
30066
+ scopes: ["habits:read"],
30067
+ tag: "Habits",
30068
+ summary: "Habit due dates in a range",
30069
+ description: "Plan's Month and agendas (V3-22): every growing habit's due days from `from` through `to` (the key's member's civil days, both included, at most 62), by its schedule alone, the same rule the list and today read. A day it rests (paused, away) or before it began isn't one; quit and event-scheduled habits are left out. Each row is `{ habitId, title, date, time, scheduleLabel, daily, streakLabel }`: `time` its exact `HH:MM` or null, `daily` true for a habit due every day, `streakLabel` `N days` for a daily habit and `N in a row` otherwise. Sorted by date, then time.",
30070
+ query: [
30071
+ { name: "from", description: "The first day (YYYY-MM-DD).", format: "date", required: true },
30072
+ {
30073
+ name: "to",
30074
+ description: "The last day (YYYY-MM-DD), at most 62 days on.",
30075
+ format: "date",
30076
+ required: true
30077
+ }
30078
+ ],
30079
+ querySchema: habitDueQuerySchema,
30080
+ status: 200,
30081
+ response: { description: "The habits' due days." },
30082
+ errors: { 400: true }
30083
+ },
29879
30084
  getAway: {
29880
30085
  method: "get",
29881
30086
  path: "/habits/away",
@@ -30052,8 +30257,9 @@ var habitOperations = defineOperations({
30052
30257
  scopes: ["habits:write"],
30053
30258
  tag: "Habits",
30054
30259
  summary: "Pause a habit",
30055
- description: '"Pause gym till the 26th": the habit rests until `until` (the day it\'s back, after today): off Now, no push, streak and plant wait. Pausing a paused habit moves its end. Returns the refreshed habit with `pause`; fires `habit.updated`.',
30260
+ description: '"Pause gym till the 26th": the habit rests until `until` (the day it\'s back, after today): off Now, no push, streak and plant wait. No body, or `until: null`, pauses open-ended: it rests until `/resume` (`paused` true, `pausedUntil` null). Pausing a paused habit moves its end. Returns the refreshed habit with `pause` and `paused`; fires `habit.updated`.',
30056
30261
  body: habitPauseInputSchema,
30262
+ bodyOptional: true,
30057
30263
  status: 201,
30058
30264
  response: { description: "The refreshed habit." },
30059
30265
  errors: {
@@ -31291,6 +31497,12 @@ var houseValueSchema = object({
31291
31497
  }).nullable()
31292
31498
  });
31293
31499
  var upkeepMonthMarkSchema = _enum2(["done", "due", "planned", "late"]).nullable();
31500
+ var upkeepRoutineKindSchema = _enum2(["habit", "series"]);
31501
+ var upkeepSeriesRunSchema = object({
31502
+ year: number2().int(),
31503
+ mark: _enum2(["done", "running", "late"]),
31504
+ taskId: string2()
31505
+ });
31294
31506
  var houseUpkeepRoutineSchema = object({
31295
31507
  taskId: string2(),
31296
31508
  title: string2(),
@@ -31300,7 +31512,14 @@ var houseUpkeepRoutineSchema = object({
31300
31512
  months: array(upkeepMonthMarkSchema).length(12),
31301
31513
  bookedOn: day.nullable().default(null),
31302
31514
  itemState: string2().nullable().default(null),
31303
- certified: boolean2().default(false)
31515
+ certified: boolean2().default(false),
31516
+ kind: upkeepRoutineKindSchema.default("habit"),
31517
+ placeTitle: string2().nullable().default(null),
31518
+ cadenceWords: string2().default(""),
31519
+ nextOn: day.nullable().default(null),
31520
+ nextWords: string2().default(""),
31521
+ late: boolean2().default(false),
31522
+ seriesRuns: array(upkeepSeriesRunSchema).default([])
31304
31523
  });
31305
31524
  var houseUpkeepNeedSchema = object({
31306
31525
  id: string2(),
@@ -31317,6 +31536,12 @@ var houseUpkeepYearSchema = object({
31317
31536
  year: number2().int(),
31318
31537
  currentMonth: number2().int().min(1).max(12).nullable(),
31319
31538
  routines: array(houseUpkeepRoutineSchema),
31539
+ counts: object({
31540
+ all: number2().int().min(0),
31541
+ habits: number2().int().min(0),
31542
+ series: number2().int().min(0),
31543
+ late: number2().int().min(0)
31544
+ }).default({ all: 0, habits: 0, series: 0, late: 0 }),
31320
31545
  needsMonth: number2().int().min(1).max(12),
31321
31546
  needs: array(houseUpkeepNeedSchema),
31322
31547
  spent: object({
@@ -33148,6 +33373,7 @@ var apiKeys = pgTable("api_keys", {
33148
33373
  scopes: text("scopes").array().notNull().default([]),
33149
33374
  lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
33150
33375
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
33376
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
33151
33377
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
33152
33378
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
33153
33379
  }, (t) => [index("api_keys_tenant_idx").on(t.tenantId)]);
@@ -33244,6 +33470,22 @@ var API_KEY_SCOPE_VALUES = [
33244
33470
  "private:*",
33245
33471
  "*"
33246
33472
  ];
33473
+ var API_TOKEN_SECTION_VALUES = [
33474
+ "today",
33475
+ "plan",
33476
+ "work",
33477
+ "habits",
33478
+ "journal",
33479
+ "home",
33480
+ "food",
33481
+ "collection",
33482
+ "money",
33483
+ "health",
33484
+ "people",
33485
+ "animals",
33486
+ "agents",
33487
+ "settings"
33488
+ ];
33247
33489
  var FULL_ACCESS_SCOPE = "*";
33248
33490
  var CONNECTIONS_WRITE_SCOPE = "connections:write";
33249
33491
  function scopeGranted(granted, required) {
@@ -33263,16 +33505,25 @@ var apiKeyScopesInputSchema = array(apiKeyScopeSchema).refine((scopes) => !scope
33263
33505
  });
33264
33506
  var apiKeySchema = createSelectSchema(apiKeys, {
33265
33507
  scopes: array(apiKeyScopeSchema),
33508
+ expiresAt: exports_coerce.date().nullable().default(null),
33266
33509
  agentId: string2().nullable().default(null),
33267
33510
  lastUsedAt: exports_coerce.date().nullable(),
33268
33511
  revokedAt: exports_coerce.date().nullable(),
33269
33512
  createdAt: exports_coerce.date(),
33270
33513
  updatedAt: exports_coerce.date()
33271
33514
  });
33272
- var apiKeyPublicSchema = apiKeySchema.omit({ keyHash: true });
33515
+ var apiKeySectionAccessSchema = object({
33516
+ section: _enum2(API_TOKEN_SECTION_VALUES),
33517
+ access: _enum2(["read", "write"])
33518
+ }).strict();
33519
+ var apiKeyPublicSchema = apiKeySchema.omit({ keyHash: true }).extend({
33520
+ sections: array(apiKeySectionAccessSchema).default([]),
33521
+ scopeLine: string2().default("")
33522
+ });
33273
33523
  var createApiKeyInputSchema = createInsertSchema(apiKeys, {
33274
33524
  name: string2().min(1).max(120),
33275
33525
  scopes: apiKeyScopesInputSchema.optional(),
33526
+ expiresAt: exports_coerce.date().nullish(),
33276
33527
  agentId: ulidSchema.nullish()
33277
33528
  }).omit({
33278
33529
  id: true,
@@ -33284,7 +33535,27 @@ var createApiKeyInputSchema = createInsertSchema(apiKeys, {
33284
33535
  revokedAt: true,
33285
33536
  createdAt: true,
33286
33537
  updatedAt: true
33287
- }).strict();
33538
+ }).extend({
33539
+ sections: array(apiKeySectionAccessSchema).min(1).max(API_TOKEN_SECTION_VALUES.length).optional()
33540
+ }).strict().refine((v) => v.scopes === undefined || v.sections === undefined, {
33541
+ message: "give scopes or sections, not both",
33542
+ path: ["sections"]
33543
+ }).refine((v) => !v.expiresAt || v.expiresAt.getTime() > Date.now(), {
33544
+ message: "an expiry must be in the future",
33545
+ path: ["expiresAt"]
33546
+ });
33547
+ var createKeyFromKeyInputSchema = object({
33548
+ name: string2().min(1).max(120),
33549
+ scopes: apiKeyScopesInputSchema.optional(),
33550
+ sections: array(apiKeySectionAccessSchema).min(1).max(API_TOKEN_SECTION_VALUES.length).optional(),
33551
+ expiresAt: exports_coerce.date().nullish()
33552
+ }).strict().refine((v) => v.scopes === undefined || v.sections === undefined, {
33553
+ message: "give scopes or sections, not both",
33554
+ path: ["sections"]
33555
+ }).refine((v) => !v.expiresAt || v.expiresAt.getTime() > Date.now(), {
33556
+ message: "an expiry must be in the future",
33557
+ path: ["expiresAt"]
33558
+ });
33288
33559
  var createApiKeyResultSchema = apiKeyPublicSchema.extend({ key: string2() });
33289
33560
  var updateApiKeyInputSchema = object({
33290
33561
  name: string2().min(1).max(120).optional(),
@@ -33330,6 +33601,20 @@ var keyOperations = defineOperations({
33330
33601
  response: { description: "The keys." },
33331
33602
  errors: { 403: true }
33332
33603
  },
33604
+ create: {
33605
+ method: "post",
33606
+ path: "/keys",
33607
+ scopes: ["keys:write"],
33608
+ tag: "Identity",
33609
+ summary: "Mint an API key",
33610
+ description: 'A key for the member behind this key, by `sections` (each `read` or `write`: Today/Inbox, Plan, Work, Habits, Journal, Home, Food, Collection, Money, Health, People, Animals, Agents, Settings; the server keeps the scopes they stand for) or by `scopes`, not both, with an optional `expiresAt` (null never expires; past it the key gets 401 "This token expired on <date>"). Never more than the calling key holds (403), never an Agent\'s key. The plaintext `key` is in the answer once; the key carries its `scopeLine` ("Work, Plan: read and write").',
33611
+ body: createKeyFromKeyInputSchema,
33612
+ status: 201,
33613
+ response: { description: "The key, with its one-time secret." },
33614
+ errors: { 400: true, 403: true },
33615
+ throttle: "strict",
33616
+ mcp: false
33617
+ },
33333
33618
  update: {
33334
33619
  method: "patch",
33335
33620
  path: "/keys/{id}",
@@ -33892,19 +34177,37 @@ var mailCampaignOperations = defineOperations({
33892
34177
  });
33893
34178
 
33894
34179
  // ../../packages/schema/src/contracts/meal-plans.ts
34180
+ var mealPlanStateSchema = _enum2(MEAL_PLAN_STATES);
34181
+ var civilDate2 = string2().regex(/^\d{4}-\d{2}-\d{2}$/, "a date is YYYY-MM-DD");
34182
+ var weekMenuFields = {
34183
+ state: mealPlanStateSchema.optional(),
34184
+ cookUserId: ulidSchema.nullish(),
34185
+ company: string2().max(120).nullish(),
34186
+ leftoverPortions: number2().int().positive().nullish(),
34187
+ leftoverEatBy: civilDate2.nullish(),
34188
+ leftoverFrozen: boolean2().optional()
34189
+ };
33895
34190
  var mealPlanSchema = createSelectSchema(mealPlans, {
33896
- plannedFor: exports_coerce.date()
34191
+ plannedFor: exports_coerce.date(),
34192
+ state: mealPlanStateSchema.default("planned"),
34193
+ cookUserId: string2().nullable().default(null),
34194
+ company: string2().nullable().default(null),
34195
+ leftoverPortions: number2().int().nullable().default(null),
34196
+ leftoverEatBy: string2().nullable().default(null),
34197
+ leftoverFrozen: boolean2().default(false)
33897
34198
  });
33898
34199
  var createMealPlanInputSchema = createInsertSchema(mealPlans, {
33899
34200
  id: ulidSchema,
33900
34201
  recipeId: ulidSchema.nullish(),
33901
34202
  plannedFor: exports_coerce.date(),
33902
- servings: number2().int().positive().nullish()
34203
+ servings: number2().int().positive().nullish(),
34204
+ ...weekMenuFields
33903
34205
  }).omit({ tenantId: true }).extend({ title: string2().min(1) });
33904
34206
  var updateMealPlanInputSchema = createUpdateSchema(mealPlans, {
33905
34207
  recipeId: ulidSchema.nullish(),
33906
34208
  plannedFor: exports_coerce.date().optional(),
33907
- servings: number2().int().positive().nullish()
34209
+ servings: number2().int().positive().nullish(),
34210
+ ...weekMenuFields
33908
34211
  }).omit({ id: true, tenantId: true }).extend({ title: string2().min(1).optional() }).strict();
33909
34212
  var mealPlanViewSchema = mealPlanSchema.extend({
33910
34213
  title: string2(),
@@ -33914,6 +34217,10 @@ var mealPlanViewSchema = mealPlanSchema.extend({
33914
34217
  deletedAt: exports_coerce.date().nullable(),
33915
34218
  recipeTitle: string2().nullable()
33916
34219
  });
34220
+ var mealPlanListQuerySchema = object({
34221
+ recipeId: ulidSchema.optional(),
34222
+ state: mealPlanStateSchema.optional()
34223
+ });
33917
34224
  var generateGroceryListInputSchema = object({
33918
34225
  id: ulidSchema,
33919
34226
  title: string2().min(1),
@@ -33935,6 +34242,59 @@ var mealPlanCookResultSchema = object({
33935
34242
  consumedCount: number2().int().min(0),
33936
34243
  ingredients: array(cookedIngredientSchema)
33937
34244
  });
34245
+ var cookMealPlanInputSchema = object({
34246
+ leftoverPortions: number2().int().positive().nullish(),
34247
+ leftoverEatBy: civilDate2.nullish()
34248
+ }).strict();
34249
+ var mealWeekQuerySchema = object({ start: civilDate2.optional() });
34250
+ var acceptMealDraftInputSchema = object({ start: civilDate2 }).strict();
34251
+ var mealWeekCookSchema = object({
34252
+ userId: string2(),
34253
+ name: string2(),
34254
+ initial: string2()
34255
+ });
34256
+ var mealWeekSlotSchema = object({
34257
+ id: ulidSchema,
34258
+ title: string2(),
34259
+ recipeId: ulidSchema.nullable(),
34260
+ state: mealPlanStateSchema,
34261
+ cook: mealWeekCookSchema.nullable(),
34262
+ company: string2().nullable(),
34263
+ notes: string2().nullable(),
34264
+ time: string2().nullable()
34265
+ });
34266
+ var mealWeekDaySchema = object({
34267
+ date: civilDate2,
34268
+ isToday: boolean2(),
34269
+ dinner: mealWeekSlotSchema.nullable(),
34270
+ lunch: mealWeekSlotSchema.nullable()
34271
+ });
34272
+ var mealWeekLeftoverSchema = object({
34273
+ mealPlanId: ulidSchema,
34274
+ title: string2(),
34275
+ portions: number2().int(),
34276
+ cookedOn: civilDate2,
34277
+ eatBy: civilDate2.nullable(),
34278
+ frozen: boolean2()
34279
+ });
34280
+ var mealWeekDraftDaySchema = object({
34281
+ date: civilDate2,
34282
+ dinner: mealWeekSlotSchema.nullable(),
34283
+ why: string2().nullable()
34284
+ });
34285
+ var mealWeekSchema = object({
34286
+ week: number2().int().min(1).max(53),
34287
+ start: civilDate2,
34288
+ end: civilDate2,
34289
+ days: array(mealWeekDaySchema).length(7),
34290
+ summary: object({
34291
+ planned: number2().int().min(0),
34292
+ cooked: number2().int().min(0),
34293
+ out: number2().int().min(0)
34294
+ }),
34295
+ leftovers: array(mealWeekLeftoverSchema),
34296
+ draft: array(mealWeekDraftDaySchema).length(7)
34297
+ });
33938
34298
 
33939
34299
  // ../../packages/schema/src/operations/meal-plans.ts
33940
34300
  var mealPlanOperations = defineOperations({
@@ -33945,8 +34305,43 @@ var mealPlanOperations = defineOperations({
33945
34305
  read: "meals:read",
33946
34306
  write: "meals:write",
33947
34307
  create: createMealPlanInputSchema,
33948
- update: updateMealPlanInputSchema
33949
- })
34308
+ update: updateMealPlanInputSchema,
34309
+ listQuery: [
34310
+ { name: "recipeId", description: "Only the plans of this recipe (its ULID)." },
34311
+ {
34312
+ name: "state",
34313
+ description: "Only plans in this state (`planned`, `cooked`, `out`, `unsure`, `draft`); `cooked` with `recipeId` is who cooked a recipe when."
34314
+ }
34315
+ ],
34316
+ listQuerySchema: mealPlanListQuerySchema
34317
+ }),
34318
+ week: {
34319
+ method: "get",
34320
+ path: "/meal-plans/week",
34321
+ scopes: ["meals:read"],
34322
+ tag: "Meals",
34323
+ summary: "Show the week menu",
34324
+ description: "The week (Monday to Sunday) from `start` (any day is moved to its Monday; this week by default): each day's dinner and lunch with its state (planned \xB7 cooked \xB7 out \xB7 unsure), who cooks and the company of a meal out; the dinner counts (planned, cooked, out); the leftovers still to eat; and next week's `draft` dinners, each with its one-line why (the draft plan's notes).",
34325
+ query: [{ name: "start", description: "A day of the week, YYYY-MM-DD.", format: "date" }],
34326
+ querySchema: mealWeekQuerySchema,
34327
+ status: 200,
34328
+ response: { description: "The week menu." },
34329
+ errors: { 400: true },
34330
+ operationId: "meal_week"
34331
+ },
34332
+ acceptDraft: {
34333
+ method: "post",
34334
+ path: "/meal-plans/week/accept-draft",
34335
+ scopes: ["meals:write"],
34336
+ tag: "Meals",
34337
+ summary: "Accept a week's draft",
34338
+ description: "Turn every `draft` meal plan in the week from `start` into a `planned` one and return that week's menu. Accepting a week with no drafts left changes nothing.",
34339
+ body: acceptMealDraftInputSchema,
34340
+ status: 200,
34341
+ response: { description: "The week menu, its drafts planned." },
34342
+ errors: { 400: true },
34343
+ operationId: "meal_week_accept_draft"
34344
+ }
33950
34345
  });
33951
34346
 
33952
34347
  // ../../packages/schema/src/operations/members.ts
@@ -37554,7 +37949,8 @@ var recipeLibrarySchema = object({
37554
37949
  recipes: array(object({
37555
37950
  id: ulidSchema,
37556
37951
  lastCookedAt: exports_coerce.date().nullable(),
37557
- ingredients: array(string2())
37952
+ ingredients: array(string2()),
37953
+ cookedCount: number2().int().min(0).optional()
37558
37954
  })),
37559
37955
  menu: array(object({
37560
37956
  planId: ulidSchema,
@@ -37563,7 +37959,9 @@ var recipeLibrarySchema = object({
37563
37959
  plannedFor: exports_coerce.date(),
37564
37960
  mealType: mealTypeSchema,
37565
37961
  cooked: boolean2(),
37566
- onList: array(string2())
37962
+ onList: array(string2()),
37963
+ state: mealPlanStateSchema.optional(),
37964
+ cook: mealWeekCookSchema.nullable().optional()
37567
37965
  }))
37568
37966
  });
37569
37967
  var recipeIngredientMatchInputSchema = object({ foodId: string2().min(1) }).strict();
@@ -39039,6 +39437,9 @@ var PILL_PAGES_MAX = 4;
39039
39437
  var CALENDAR_ZOOM_MIN = 75;
39040
39438
  var CALENDAR_ZOOM_MAX = 300;
39041
39439
  var calendarZoomSchema = number2().int().min(CALENDAR_ZOOM_MIN).max(CALENDAR_ZOOM_MAX);
39440
+ var settingsGlassSchema = _enum2(settingsGlass.enumValues);
39441
+ var themePackSchema = _enum2(settingsThemePack.enumValues);
39442
+ var themePackScopeSchema = _enum2(settingsThemePackScope.enumValues);
39042
39443
  var userSettingsSchema = createSelectSchema(userSettings, {
39043
39444
  pinnedAreaIds: array(string2()).default([]),
39044
39445
  focusAreaId: string2().nullable().default(null),
@@ -39049,6 +39450,14 @@ var userSettingsSchema = createSelectSchema(userSettings, {
39049
39450
  pillPages: array(string2()).default([]).transform((ids) => ids.filter((id) => pillPageSchema.safeParse(id).success)),
39050
39451
  foodDiaryMode: boolean2().default(false),
39051
39452
  calendarZoom: number2().int().nullable().default(null),
39453
+ glass: settingsGlassSchema.default("liquid"),
39454
+ followSky: boolean2().default(true),
39455
+ nightUntilWake: boolean2().default(false),
39456
+ largerText: boolean2().default(false),
39457
+ reduceMotion: boolean2().default(false),
39458
+ showStreakNumbers: boolean2().default(true),
39459
+ themePack: themePackSchema.default("do"),
39460
+ themePackScope: themePackScopeSchema.default("section"),
39052
39461
  createdAt: exports_coerce.date(),
39053
39462
  updatedAt: exports_coerce.date()
39054
39463
  }).omit({ autoRollOverdue: true });
@@ -39068,7 +39477,15 @@ var updateUserSettingsInputSchema = createUpdateSchema(userSettings, {
39068
39477
  quietHours: quietHoursSchema.nullish(),
39069
39478
  lifeBlockSharing: lifeBlockSharingSchema.nullish(),
39070
39479
  calendarZoom: calendarZoomSchema.nullish(),
39071
- pillPages: array(pillPageSchema).transform((ids) => [...new Set(ids)]).pipe(array(pillPageSchema).max(PILL_PAGES_MAX)).optional()
39480
+ pillPages: array(pillPageSchema).transform((ids) => [...new Set(ids)]).pipe(array(pillPageSchema).max(PILL_PAGES_MAX)).optional(),
39481
+ glass: settingsGlassSchema.optional(),
39482
+ followSky: boolean2().optional(),
39483
+ nightUntilWake: boolean2().optional(),
39484
+ largerText: boolean2().optional(),
39485
+ reduceMotion: boolean2().optional(),
39486
+ showStreakNumbers: boolean2().optional(),
39487
+ themePack: themePackSchema.optional(),
39488
+ themePackScope: themePackScopeSchema.optional()
39072
39489
  }).omit({
39073
39490
  id: true,
39074
39491
  tenantId: true,
@@ -39528,6 +39945,66 @@ var taskBlockOperations = defineOperations({
39528
39945
  }
39529
39946
  });
39530
39947
 
39948
+ // ../../packages/schema/src/operations/task-series.ts
39949
+ var taskSeriesOperations = defineOperations({
39950
+ make: {
39951
+ method: "post",
39952
+ path: "/tasks/{id}/make-series",
39953
+ scopes: ["tasks:write"],
39954
+ tag: "Tasks",
39955
+ summary: "Make a task the first of a series",
39956
+ description: "The task (by id or ref, `DO-150`) becomes slot 0 of a new series on `rule` (every N weeks, months or years; a quarter is 3 months), its Deadline (else its planned day, else today) the first slot. Its Area, Project, labels, priority, estimate, Owner and sub-task titles are the template each next task copies; the next is made `leadDays` before its slot and named by `occurrenceLabel` (`quarter`, `month`, `date`). A task already in a series is a 409.",
39957
+ body: makeTaskSeriesInputSchema,
39958
+ status: 201,
39959
+ response: { description: "The series, with its strip." },
39960
+ errors: { 400: true, 404: true, 409: true }
39961
+ },
39962
+ list: {
39963
+ method: "get",
39964
+ path: "/task-series",
39965
+ scopes: ["tasks:read"],
39966
+ tag: "Tasks",
39967
+ summary: "List task series",
39968
+ description: "The Space's task series, stopped ones too: each with its rhythm in words, its settings, how many tasks it made and its strip (up to two past occurrences, the latest, and the next with the day it's made).",
39969
+ status: 200,
39970
+ response: { description: "The series." }
39971
+ },
39972
+ get: {
39973
+ method: "get",
39974
+ path: "/task-series/{id}",
39975
+ scopes: ["tasks:read"],
39976
+ tag: "Tasks",
39977
+ summary: "Show a task series",
39978
+ description: "One series with its settings and its strip.",
39979
+ status: 200,
39980
+ response: { description: "The series." },
39981
+ errors: { 404: true }
39982
+ },
39983
+ update: {
39984
+ method: "patch",
39985
+ path: "/task-series/{id}",
39986
+ scopes: ["tasks:write"],
39987
+ tag: "Tasks",
39988
+ summary: "Update a task series",
39989
+ description: "Rename it, change its rhythm, naming or lead days, or stop (`active: false`) and resume it.",
39990
+ body: updateTaskSeriesInputSchema,
39991
+ status: 200,
39992
+ response: { description: "The series." },
39993
+ errors: { 400: true, 404: true }
39994
+ },
39995
+ stop: {
39996
+ method: "delete",
39997
+ path: "/task-series/{id}",
39998
+ scopes: ["tasks:write"],
39999
+ tag: "Tasks",
40000
+ summary: "Stop a task series",
40001
+ description: "No more tasks are made; every task it made stays.",
40002
+ status: 204,
40003
+ response: { description: "Stopped." },
40004
+ errors: { 404: true }
40005
+ }
40006
+ });
40007
+
39531
40008
  // ../../packages/schema/src/operations/tasks.ts
39532
40009
  var taskListQuery = taskListQuerySchema.extend(paginationQuerySchema.shape).extend({ filter: taskListFilterSchema.optional().default("all") });
39533
40010
  var taskOperations = defineOperations({
@@ -39825,6 +40302,67 @@ var todayOperations = defineOperations({
39825
40302
  }
39826
40303
  });
39827
40304
 
40305
+ // ../../packages/schema/src/contracts/waste-calendar.ts
40306
+ var WASTE_KIND_VALUES = ["rest", "pmd", "paper", "garden", "glass", "other"];
40307
+ var wasteKindSchema = _enum2(WASTE_KIND_VALUES);
40308
+ var civilDate3 = exports_iso.date();
40309
+ var wasteCollectionSchema = object({
40310
+ date: civilDate3,
40311
+ kinds: array(wasteKindSchema).min(1),
40312
+ movedBy: string2().min(1).optional()
40313
+ });
40314
+ var wasteCalendarConnectInputSchema = object({
40315
+ feedUrl: url({ protocol: /^https?$/ }).max(2000)
40316
+ }).strict();
40317
+ var wasteCalendarNextQuerySchema = object({
40318
+ limit: exports_coerce.number().int().min(1).max(12).default(4)
40319
+ });
40320
+ var wasteCalendarNextItemSchema = wasteCollectionSchema.extend({
40321
+ kindsWords: string2().min(1),
40322
+ binsOutAt: string2().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
40323
+ });
40324
+ var wasteCalendarNextSchema = object({
40325
+ connected: boolean2(),
40326
+ collections: array(wasteCalendarNextItemSchema),
40327
+ lastSyncAt: exports_coerce.date().nullable(),
40328
+ lastSyncLine: string2().nullable(),
40329
+ lastError: string2().nullable()
40330
+ });
40331
+
40332
+ // ../../packages/schema/src/operations/waste-calendar.ts
40333
+ var CONNECT3 = ["connections:write"];
40334
+ var AFTER3 = "`*` and `private:*` never grant `connections:write`: a key holds it by name, and a guest's key never does.";
40335
+ var wasteCalendarOperations = defineOperations({
40336
+ connect: {
40337
+ method: "post",
40338
+ path: "/integrations/waste-calendar",
40339
+ scopes: CONNECT3,
40340
+ afterScopes: AFTER3,
40341
+ tag: "Connections",
40342
+ summary: "Connect the waste calendar",
40343
+ description: "Keep the household's waste-collection iCal `feedUrl` (the municipality's or the waste app's), sealed, and read it now; it's read again every morning at 06:00. Connecting again replaces the URL. The URL is never returned. Answers what's next, as `next` does: a feed that can't be read still connects, with `lastError` saying why.",
40344
+ body: wasteCalendarConnectInputSchema,
40345
+ status: 200,
40346
+ response: { description: "The next collections." },
40347
+ errors: { 400: true, 403: true },
40348
+ throttle: "strict",
40349
+ mcp: false
40350
+ },
40351
+ next: {
40352
+ method: "get",
40353
+ path: "/integrations/waste-calendar/next",
40354
+ scopes: ["settings:read"],
40355
+ tag: "Connections",
40356
+ summary: "The next waste collections",
40357
+ description: 'The next `limit` (1\u201312, 4 by default) collection days from today in the connecting member\'s zone: each its `date`, `kinds` (`rest`, `pmd`, `paper`, `garden`, `glass`, `other`), `kindsWords` ("Rest and PMD", "Garden, moved by the holiday"), `movedBy` when the feed says why it moved, and `binsOutAt` (the evening before at 20:00, local). With `lastSyncAt`, `lastSyncLine` ("Today 06:00, every morning") and the last read\'s `lastError`; `connected: false` with none set up.',
40358
+ query: [{ name: "limit", description: "How many collection days (1\u201312, default 4)." }],
40359
+ querySchema: wasteCalendarNextQuerySchema,
40360
+ status: 200,
40361
+ response: { description: "The next collections." },
40362
+ errors: { 403: true }
40363
+ }
40364
+ });
40365
+
39828
40366
  // ../../packages/schema/src/db/webhooks.ts
39829
40367
  var webhookSubscriptions = pgTable("webhook_subscriptions", {
39830
40368
  id: text("id").primaryKey(),
@@ -40805,6 +41343,14 @@ function addMonths(d, months) {
40805
41343
  var TAG_RE = /(?:^|\s)#([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;
40806
41344
  var STAR_RE = /(?:^|\s)!(star|flag|s|f)?(?=\s|$)/giu;
40807
41345
  var AREA_RE = /(?:^|\s)~([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;
41346
+ var PRIORITY_RE = /(?:^|\s)(?:p([123])|!(high|medium|low))(?=\s|$)/giu;
41347
+ var PRIORITY_OF_P = { "1": "high", "2": "medium", "3": "low" };
41348
+ function capturePriority(token) {
41349
+ const m = /^(?:p([123])|!(high|medium|low))$/iu.exec(token.trim());
41350
+ if (!m)
41351
+ return null;
41352
+ return m[1] ? PRIORITY_OF_P[m[1]] ?? null : m[2]?.toLowerCase();
41353
+ }
40808
41354
  function parseCapture(input, now) {
40809
41355
  const tags = [];
40810
41356
  for (const match of input.matchAll(TAG_RE)) {
@@ -40825,7 +41371,12 @@ function parseCapture(input, now) {
40825
41371
  break;
40826
41372
  }
40827
41373
  }
40828
- let stripped = blankMatches(input, [TAG_RE, STAR_RE, AREA_RE]);
41374
+ let priority;
41375
+ for (const match of input.matchAll(PRIORITY_RE)) {
41376
+ priority = capturePriority(match[0]) ?? undefined;
41377
+ break;
41378
+ }
41379
+ let stripped = blankMatches(input, [TAG_RE, STAR_RE, AREA_RE, PRIORITY_RE]);
40829
41380
  const when = now ? parseWhen(stripped, now) : undefined;
40830
41381
  if (when) {
40831
41382
  for (const span of [...when.spans].sort((a, b) => b.start - a.start)) {
@@ -40838,6 +41389,7 @@ function parseCapture(input, now) {
40838
41389
  tags,
40839
41390
  ...starred ? { starred } : {},
40840
41391
  ...area ? { area } : {},
41392
+ ...priority ? { priority } : {},
40841
41393
  ...when?.scheduledAt ? { scheduledAt: when.scheduledAt, allDay: when.allDay } : {},
40842
41394
  ...when?.dueAt ? { dueAt: when.dueAt, ...when.scheduledAt ? {} : { allDay: when.dueAllDay } } : {},
40843
41395
  ...when?.durationMinutes ? { durationMinutes: when.durationMinutes } : {},
@@ -41532,7 +42084,7 @@ function renderHabit(data, mode) {
41532
42084
  const pct = typeof streak?.completionRate === "number" ? ` \xB7 ${Math.round(streak.completionRate * 100)}% lately` : "";
41533
42085
  const recovering = streak?.state === "recovering" ? paint(mode, "yellow", " \xB7 recovering") : "";
41534
42086
  const counts = streak ? ` \xB7 streak ${streak.current ?? 0} (best ${streak.longest ?? 0})` : "";
41535
- const rest = h.pause ? ` \xB7 ${h.pause.by === "away" ? "away" : "paused"}, back ${h.pause.until}` : streak?.skippedToday ? " \xB7 rest day" : "";
42087
+ const rest = h.pause ? ` \xB7 ${h.pause.by === "away" ? "away" : "paused"}${h.pause.until ? `, back ${h.pause.until}` : " until resumed"}` : streak?.skippedToday ? " \xB7 rest day" : "";
41536
42088
  const last = h.lastCheckIn?.id ? `
41537
42089
  ${paint(mode, "dim", `last check-in ${h.lastCheckIn.id}`)}${h.lastCheckIn.note ? ` "${h.lastCheckIn.note}"` : ""}` : "";
41538
42090
  return `${h.title ?? "(habit)"} \u2014 ${flame}${pct}${counts}${recovering}${rest}
@@ -41574,10 +42126,11 @@ function renderKeys(data, mode) {
41574
42126
  return paint(mode, "dim", "(no keys)");
41575
42127
  return keys.map((key) => {
41576
42128
  const scopes = key.scopes ?? [];
41577
- const reach = sayFullAccess(scopes) ?? (scopes.join(", ") || "(no scopes)");
42129
+ const reach = key.scopeLine || sayFullAccess(scopes) || scopes.join(", ") || "(no scopes)";
42130
+ const expires = key.expiresAt ? paint(mode, "dim", ` \xB7 expires ${key.expiresAt.slice(0, 10)}`) : "";
41578
42131
  const changed = key.changes?.[0] ? paint(mode, "dim", ` \xB7 changed ${key.changes[0].at.slice(0, 10)}`) : "";
41579
42132
  return `${key.name} ${paint(mode, "dim", key.id)}
41580
- ${reach}${changed}`;
42133
+ ${reach}${expires}${changed}`;
41581
42134
  }).join(`
41582
42135
  `);
41583
42136
  }
@@ -44978,6 +45531,32 @@ var CONNECTION_COMMAND_SPECS = [
44978
45531
  returns: "MoneyKrakenLinkStatus",
44979
45532
  notes: `${routeOf(krakenLinkOperations.remove)}. Gated behind --allow-destructive.`
44980
45533
  },
45534
+ {
45535
+ path: ["connections", "waste", "next"],
45536
+ summary: "The next waste collections: each day's kinds in words and when the bins go out (the evening before, at 20:00).",
45537
+ args: [],
45538
+ flags: [
45539
+ { name: "limit", type: "string", description: "How many collection days (1\u201312, default 4)." }
45540
+ ],
45541
+ scopes: scopesOf(wasteCalendarOperations.next),
45542
+ returns: "WasteCalendarNext",
45543
+ notes: `${routeOf(wasteCalendarOperations.next)}. A pickup that moved says why ("Garden, moved by the holiday").`
45544
+ },
45545
+ {
45546
+ path: ["connections", "waste", "connect"],
45547
+ summary: "Connect the household's waste calendar: its iCal feed URL, read from a file, then read it.",
45548
+ args: [],
45549
+ flags: [
45550
+ {
45551
+ name: "feed-file",
45552
+ type: "string",
45553
+ description: "Path to a file holding the feed's URL (one line), or - to read it from stdin."
45554
+ }
45555
+ ],
45556
+ scopes: scopesOf(wasteCalendarOperations.connect),
45557
+ returns: "WasteCalendarNext",
45558
+ notes: `${routeOf(wasteCalendarOperations.connect)}. The URL can carry a private token, so it's read from the file and sealed; it is never a flag, never printed and never read back. It's read again every morning at 06:00; connecting again replaces it.`
45559
+ },
44981
45560
  {
44982
45561
  path: ["doccle", "connect"],
44983
45562
  summary: "Give do your Doccle login so the nightly import can run. do checks it with Doccle first and keeps it sealed.",
@@ -45110,6 +45689,24 @@ var CONNECTION_HANDLERS = {
45110
45689
  data: await guarded([], () => requireClient().get(pathOf(krakenLinkOperations.status))),
45111
45690
  render: renderKraken
45112
45691
  }),
45692
+ "connections waste next": async ({ flags, requireClient }) => {
45693
+ const limit = flagString(flags, "limit");
45694
+ if (limit !== undefined && !/^\d+$/.test(limit))
45695
+ throw usageError("--limit takes a number (1\u201312)");
45696
+ const path = `${pathOf(wasteCalendarOperations.next)}${limit ? `?limit=${limit}` : ""}`;
45697
+ return {
45698
+ data: await guarded([], () => requireClient().get(path)),
45699
+ render: renderWaste
45700
+ };
45701
+ },
45702
+ "connections waste connect": async ({ io, flags, requireClient }) => {
45703
+ refuseSecretFlags(flags, ["url", "feed", "feed-url"], "--feed-file <path>");
45704
+ const feedUrl = (await readSecret(io, flags, "feed-file", "the feed's URL", "line")).trim();
45705
+ return {
45706
+ data: await guarded([feedUrl], () => requireClient().post(pathOf(wasteCalendarOperations.connect), { feedUrl })),
45707
+ render: renderWaste
45708
+ };
45709
+ },
45113
45710
  "money kraken-link set": async ({ io, flags, requireClient }) => {
45114
45711
  refuseSecretFlags(flags, ["key", "api-key", "secret", "api-secret", "private-key"], "--key-file <path>");
45115
45712
  const text = await readSecret(io, flags, "key-file", "the Kraken key", "all");
@@ -45196,6 +45793,30 @@ function refuseSecretFlags(flags, names, instead) {
45196
45793
  }
45197
45794
  }
45198
45795
  }
45796
+ function renderWaste(data, mode) {
45797
+ const next = data;
45798
+ if (!next || !Array.isArray(next.collections))
45799
+ return renderRecord(data, mode);
45800
+ if (!next.connected)
45801
+ return "No waste calendar connected. Run do connections waste connect.";
45802
+ const day = (iso) => new Date(`${iso.slice(0, 10)}T12:00:00Z`).toLocaleDateString("en-GB", {
45803
+ weekday: "short",
45804
+ day: "numeric",
45805
+ month: "short",
45806
+ timeZone: "UTC"
45807
+ });
45808
+ const weekday = (iso) => new Date(`${iso.slice(0, 10)}T12:00:00Z`).toLocaleDateString("en-GB", {
45809
+ weekday: "short",
45810
+ timeZone: "UTC"
45811
+ });
45812
+ const lines = next.collections.map((c) => `${day(c.date)} \xB7 ${c.kindsWords} \xB7 bins out ${weekday(c.binsOutAt)} ${c.binsOutAt.slice(11)}`);
45813
+ return [
45814
+ ...lines.length > 0 ? lines : ["No collections coming up."],
45815
+ ...next.lastSyncLine ? [`Read: ${next.lastSyncLine}`] : [],
45816
+ ...next.lastError ? [`Last read failed: ${next.lastError}`] : []
45817
+ ].join(`
45818
+ `);
45819
+ }
45199
45820
  async function readSecret(io, flags, flag, what, take) {
45200
45821
  const path = flagString(flags, flag);
45201
45822
  if (path === undefined || path.length === 0) {
@@ -46580,6 +47201,45 @@ var commands16 = [
46580
47201
  }
46581
47202
  ];
46582
47203
 
47204
+ // ../../packages/domain/src/date.ts
47205
+ var formatters = new Map;
47206
+ function dateTimeFormat(locale, options) {
47207
+ const key = `${locale ?? ""}|${JSON.stringify(options)}`;
47208
+ let formatter = formatters.get(key);
47209
+ if (!formatter) {
47210
+ formatter = new Intl.DateTimeFormat(locale, options);
47211
+ formatters.set(key, formatter);
47212
+ }
47213
+ return formatter;
47214
+ }
47215
+ var STYLES = ["dateStyle", "timeStyle"];
47216
+ var DATE_FIELDS = ["weekday", "year", "month", "day", ...STYLES];
47217
+ var CLOCK_PARTS = new Set(["hour", "minute", "second", "fractionalSecond", "dayPeriod"]);
47218
+ function dayInZone(now, zone) {
47219
+ return dateTimeFormat("en-CA", {
47220
+ timeZone: zone,
47221
+ year: "numeric",
47222
+ month: "2-digit",
47223
+ day: "2-digit"
47224
+ }).format(now);
47225
+ }
47226
+ function dayOfUtc(t) {
47227
+ const d = new Date(t);
47228
+ const pad = (n) => String(n).padStart(2, "0");
47229
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
47230
+ }
47231
+ function utcOfDay(day) {
47232
+ const [y, m, d] = day.split("-").map(Number);
47233
+ return Date.UTC(y, m - 1, d);
47234
+ }
47235
+ function shiftDay2(day, n) {
47236
+ return dayOfUtc(utcOfDay(day) + n * 86400000);
47237
+ }
47238
+ function mondayOfDay(day) {
47239
+ const weekday = (new Date(utcOfDay(day)).getUTCDay() + 6) % 7;
47240
+ return shiftDay2(day, -weekday);
47241
+ }
47242
+
46583
47243
  // src/commands/food.ts
46584
47244
  var commands17 = [
46585
47245
  {
@@ -46739,6 +47399,86 @@ var commands17 = [
46739
47399
  };
46740
47400
  }
46741
47401
  },
47402
+ {
47403
+ spec: {
47404
+ path: ["food", "menu"],
47405
+ summary: "Show the week menu: each day's dinner and lunch with who cooks and its state, the counts, the leftovers and next week's draft.",
47406
+ args: [],
47407
+ flags: [
47408
+ {
47409
+ name: "start",
47410
+ type: "string",
47411
+ description: "Any day of the week (YYYY-MM-DD); this week by default."
47412
+ }
47413
+ ],
47414
+ scopes: [...mealPlanOperations.week.scopes],
47415
+ returns: "MealWeek",
47416
+ notes: `${routeOf(mealPlanOperations.week)}. A draft dinner's \`why\` is its plan's notes; write drafts with \`do meal-plans create\` and state "draft".`
47417
+ },
47418
+ run: async ({ flags, requireClient }) => {
47419
+ const start = flagString(flags, "start");
47420
+ return {
47421
+ data: await requireClient().get(pathOf(mealPlanOperations.week), {
47422
+ query: { start }
47423
+ }),
47424
+ render: renderRecord
47425
+ };
47426
+ }
47427
+ },
47428
+ {
47429
+ spec: {
47430
+ path: ["food", "plans"],
47431
+ summary: "List meal plans, by recipe and state: --recipe <id> --state cooked is who cooked it when.",
47432
+ args: [],
47433
+ flags: [
47434
+ { name: "recipe", type: "string", description: "Only this recipe's plans (its ULID)." },
47435
+ {
47436
+ name: "state",
47437
+ type: "string",
47438
+ description: "Only this state: planned, cooked, out, unsure or draft."
47439
+ }
47440
+ ],
47441
+ scopes: [...mealPlanOperations.list.scopes],
47442
+ returns: "MealPlan",
47443
+ notes: `${routeOf(mealPlanOperations.list)}. \`cookUserId\` is who cooked it; \`notes\` is the plan's note.`
47444
+ },
47445
+ run: async ({ flags, requireClient }) => {
47446
+ const query = parseInput(mealPlanListQuerySchema, {
47447
+ recipeId: flagString(flags, "recipe"),
47448
+ state: flagString(flags, "state")
47449
+ });
47450
+ return {
47451
+ data: await requireClient().get(pathOf(mealPlanOperations.list), { query }),
47452
+ render: renderRecord
47453
+ };
47454
+ }
47455
+ },
47456
+ {
47457
+ spec: {
47458
+ path: ["food", "draft", "accept"],
47459
+ summary: "Accept a week's draft menu: its draft dinners become planned ones.",
47460
+ args: [],
47461
+ flags: [
47462
+ {
47463
+ name: "start",
47464
+ type: "string",
47465
+ description: "Any day of the week (YYYY-MM-DD); next week by default."
47466
+ }
47467
+ ],
47468
+ scopes: [...mealPlanOperations.acceptDraft.scopes],
47469
+ returns: "MealWeek",
47470
+ notes: `${routeOf(mealPlanOperations.acceptDraft)}. Idempotent: a week with no drafts left is unchanged.`
47471
+ },
47472
+ run: async ({ flags, requireClient }) => {
47473
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
47474
+ const start = flagString(flags, "start") ?? shiftDay2(mondayOfDay(dayInZone(new Date, zone)), 7);
47475
+ const body = parseInput(acceptMealDraftInputSchema, { start });
47476
+ return {
47477
+ data: await requireClient().post(pathOf(mealPlanOperations.acceptDraft), body),
47478
+ render: renderRecord
47479
+ };
47480
+ }
47481
+ },
46742
47482
  {
46743
47483
  spec: {
46744
47484
  path: ["food", "week-read"],
@@ -47185,9 +47925,19 @@ var SCHEDULE_FLAGS = [
47185
47925
  type: "string",
47186
47926
  description: "Due monthly on this day of the month: 1\u201331 (a 31 falls on a shorter month's last day), or last."
47187
47927
  },
47928
+ {
47929
+ name: "time",
47930
+ type: "string",
47931
+ description: "When in the day: a 24-hour time (16:30) or a part of day (morning, midday, evening); none clears it."
47932
+ },
47933
+ {
47934
+ name: "for",
47935
+ type: "string",
47936
+ description: "How long it takes (15m, 1h, 1h30m; 1\u2013600 minutes); none clears it back to 15m."
47937
+ },
47188
47938
  { name: "data", type: "string", description: "JSON for any other field of the contract." }
47189
47939
  ];
47190
- var SCHEDULE_NOTES = "One of --every-day, --on mo,we,fr, --every 3d|2w [--from YYYY-MM-DD], --monthly 1|last sets its schedule; days it isn't due never break the streak. Without one, it keeps `period` + `frequencyCount` (3\xD7 a week). The reply's `nextDue` lists its next 4 due days.";
47940
+ var SCHEDULE_NOTES = "One of --every-day, --on mo,we,fr, --every 3d|2w [--from YYYY-MM-DD], --monthly 1|last sets its schedule; days it isn't due never break the streak. Without one, it keeps `period` + `frequencyCount` (3\xD7 a week). The reply's `nextDue` lists its next 4 due days. --time 16:30|morning|midday|evening and --for 20m put it on your do calendar at that time on each due day (a part of day at 08:00, 12:30 or 19:00; 15 minutes without --for).";
47191
47941
  function localToday() {
47192
47942
  const now = new Date;
47193
47943
  const pad = (n) => String(n).padStart(2, "0");
@@ -47227,11 +47977,31 @@ function scheduleFromFlags(flags, today = localToday()) {
47227
47977
  }
47228
47978
  return;
47229
47979
  }
47980
+ function minutesFromFlag(raw) {
47981
+ const text = raw.trim().toLowerCase();
47982
+ const m = /^(?:(\d+)\s*h)?\s*(?:(\d+)\s*m?)?$/.exec(text);
47983
+ if (!text || !m || m[1] === undefined && m[2] === undefined)
47984
+ throw usageError("--for takes a length like 15m, 1h or 1h30m");
47985
+ return Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0);
47986
+ }
47987
+ function timingFromFlags(flags) {
47988
+ const time = flagString(flags, "time");
47989
+ const length = flagString(flags, "for");
47990
+ const out = {};
47991
+ if (time !== undefined) {
47992
+ const t = time.trim().toLowerCase();
47993
+ out.timeOfDay = t === "none" ? null : /^\d:\d\d$/.test(t) ? `0${t}` : t;
47994
+ }
47995
+ if (length !== undefined) {
47996
+ out.durationMinutes = length.trim().toLowerCase() === "none" ? null : minutesFromFlag(length);
47997
+ }
47998
+ return out;
47999
+ }
47230
48000
  var commands20 = [
47231
48001
  {
47232
48002
  spec: {
47233
48003
  path: ["habits", "create"],
47234
- summary: "Create a habit, with an optional schedule (every day, weekdays, every N, monthly).",
48004
+ summary: "Create a habit, with an optional schedule (every day, weekdays, every N, monthly), time and length.",
47235
48005
  args: [{ name: "title", required: false, description: "The habit's title." }],
47236
48006
  flags: SCHEDULE_FLAGS,
47237
48007
  scopes: [...habitOperations.create.scopes],
@@ -47246,7 +48016,8 @@ var commands20 = [
47246
48016
  measurementType: "binary",
47247
48017
  ...title ? { title } : {},
47248
48018
  ...readDataFlag(flags),
47249
- ...schedule ? { schedule } : {}
48019
+ ...schedule ? { schedule } : {},
48020
+ ...timingFromFlags(flags)
47250
48021
  });
47251
48022
  if (flagBool(flags, "dry-run")) {
47252
48023
  return {
@@ -47263,7 +48034,7 @@ var commands20 = [
47263
48034
  {
47264
48035
  spec: {
47265
48036
  path: ["habits", "update"],
47266
- summary: "Change a habit: its schedule (--every-day, --on, --every, --monthly) or --data.",
48037
+ summary: "Change a habit: its schedule (--every-day, --on, --every, --monthly), --time, --for or --data.",
47267
48038
  args: [{ name: "id", required: true, description: "Habit ULID." }],
47268
48039
  flags: SCHEDULE_FLAGS,
47269
48040
  scopes: [...habitOperations.update.scopes],
@@ -47275,10 +48046,11 @@ var commands20 = [
47275
48046
  const schedule = scheduleFromFlags(flags);
47276
48047
  const body = parseInput(updateHabitInputSchema, {
47277
48048
  ...readDataFlag(flags),
47278
- ...schedule ? { schedule } : {}
48049
+ ...schedule ? { schedule } : {},
48050
+ ...timingFromFlags(flags)
47279
48051
  });
47280
48052
  if (Object.keys(body).length === 0)
47281
- throw usageError("nothing to change \u2014 pass a schedule flag such as --on mo,we, or --data");
48053
+ throw usageError("nothing to change \u2014 pass a schedule flag such as --on mo,we, --time, --for, or --data");
47282
48054
  const client = requireClient();
47283
48055
  if (flagBool(flags, "dry-run")) {
47284
48056
  const before = await client.get(pathOf(habitOperations.get, { id }));
@@ -47459,6 +48231,35 @@ var commands20 = [
47459
48231
  },
47460
48232
  run: readOperation(habitOperations.today, renderHabitsToday)
47461
48233
  },
48234
+ {
48235
+ spec: {
48236
+ path: ["habits", "due"],
48237
+ summary: "Each habit's due days in a range, by its schedule (Plan's Month and agendas).",
48238
+ args: [],
48239
+ flags: [
48240
+ { name: "from", type: "string", description: "The first day (YYYY-MM-DD). Required." },
48241
+ {
48242
+ name: "to",
48243
+ type: "string",
48244
+ description: "The last day (YYYY-MM-DD), at most 62 days on. Required."
48245
+ }
48246
+ ],
48247
+ scopes: [...habitOperations.due.scopes],
48248
+ returns: "HabitDueDate[]",
48249
+ notes: `${routeOf(habitOperations.due)}?from=&to=. Your saved time zone's days, both ends included. One row per habit per due day: \`time\` (HH:MM or null), \`scheduleLabel\`, \`daily\` (due every day) and \`streakLabel\` (\`N days\` daily, \`N in a row\` otherwise). Paused days, quit and event-scheduled habits are left out.`
48250
+ },
48251
+ run: async ({ flags, requireClient }) => {
48252
+ const from = flagString(flags, "from");
48253
+ const to = flagString(flags, "to");
48254
+ if (from === undefined || to === undefined) {
48255
+ throw usageError("habits due needs --from and --to");
48256
+ }
48257
+ return {
48258
+ data: await requireClient().get(pathOf(habitOperations.due), { query: { from, to } }),
48259
+ render: renderList
48260
+ };
48261
+ }
48262
+ },
47462
48263
  {
47463
48264
  spec: {
47464
48265
  path: ["habit", "check-ins"],
@@ -47640,25 +48441,23 @@ var commands20 = [
47640
48441
  {
47641
48442
  spec: {
47642
48443
  path: ["habit", "pause"],
47643
- summary: "Pause a habit until the day it's back: off Now, no push, the plant waits.",
48444
+ summary: "Pause a habit until the day it's back, or until resumed: off Now, no push, the plant waits.",
47644
48445
  args: [{ name: "id", required: true, description: "Habit ULID." }],
47645
48446
  flags: [
47646
48447
  {
47647
48448
  name: "until",
47648
48449
  type: "string",
47649
- description: "The day it's back (YYYY-MM-DD), after today. Required."
48450
+ description: "The day it's back (YYYY-MM-DD), after today. Leave it out to pause until `habit resume`."
47650
48451
  }
47651
48452
  ],
47652
48453
  scopes: [...habitOperations.pause.scopes],
47653
48454
  returns: "HabitSummary",
47654
- notes: `${routeOf(habitOperations.pause)}. Pausing a paused habit moves the day it's back. Paused days are neutral: the streak is frozen, never broken.`
48455
+ notes: `${routeOf(habitOperations.pause)}. Without --until the pause is open-ended: it lasts until \`habit resume\`. Pausing a paused habit moves the day it's back. Paused days are neutral: the streak is frozen, never broken.`
47655
48456
  },
47656
48457
  run: async ({ args, flags, requireClient }) => {
47657
48458
  const id = requireArg(args, 0, "habit id");
47658
48459
  const until = flagString(flags, "until");
47659
- if (!until)
47660
- throw usageError("habit pause needs --until <YYYY-MM-DD>, the day it's back");
47661
- const input = parseInput(habitPauseInputSchema, { until });
48460
+ const input = parseInput(habitPauseInputSchema, until ? { until } : {});
47662
48461
  return {
47663
48462
  data: await requireClient().post(pathOf(habitOperations.pause, { id }), input),
47664
48463
  render: renderHabit
@@ -49755,6 +50554,17 @@ var commands25 = [
49755
50554
  ];
49756
50555
 
49757
50556
  // src/commands/keys.ts
50557
+ function renderCreatedKey(data, mode) {
50558
+ const key = data.data ?? data;
50559
+ if (!key?.key)
50560
+ return renderRecord(data, mode);
50561
+ return [
50562
+ `${key.name}: ${key.scopeLine || "(no scopes)"}`,
50563
+ key.expiresAt ? `Expires ${key.expiresAt.slice(0, 10)}.` : "Never expires.",
50564
+ `Key (shown once): ${key.key}`
50565
+ ].join(`
50566
+ `);
50567
+ }
49758
50568
  var commands26 = [
49759
50569
  {
49760
50570
  spec: {
@@ -49768,6 +50578,57 @@ var commands26 = [
49768
50578
  },
49769
50579
  run: readOperation(keyOperations.list, renderKeys, {}, { paged: true })
49770
50580
  },
50581
+ {
50582
+ spec: {
50583
+ path: ["keys", "create"],
50584
+ summary: "Mint an API key by section, each read or write, with an optional expiry. Prints the key once.",
50585
+ args: [],
50586
+ flags: [
50587
+ { name: "name", type: "string", description: "What the key is for (e.g. 'CI deploy')." },
50588
+ {
50589
+ name: "section",
50590
+ type: "string",
50591
+ description: "A section and its access, <section>:<read|write>, repeated: today, plan, work, habits, journal, home, food, collection, money, health, people, animals, agents, settings."
50592
+ },
50593
+ {
50594
+ name: "expires",
50595
+ type: "string",
50596
+ description: "When it stops working, a date (YYYY-MM-DD). Left out: never."
50597
+ }
50598
+ ],
50599
+ scopes: [...keyOperations.create.scopes],
50600
+ returns: "ApiKeyCreated",
50601
+ notes: "The key is for the member behind this key, never more than this key holds (exit 3), never an Agent's. The secret is printed once, then never again; past its expiry the key gets 401 'This token expired on <date>'."
50602
+ },
50603
+ run: async ({ flags, requireClient }) => {
50604
+ const name = flagString(flags, "name");
50605
+ if (!name)
50606
+ throw usageError("keys create needs --name <name>");
50607
+ const sections = flagStrings(flags, "section").map((raw) => {
50608
+ const [section, access] = raw.split(":").map((part) => part.trim());
50609
+ if (!section || access !== "read" && access !== "write") {
50610
+ throw usageError(`--section takes <section>:<read|write>, not '${raw}'`);
50611
+ }
50612
+ return { section, access };
50613
+ });
50614
+ if (sections.length === 0) {
50615
+ throw usageError("keys create needs at least one --section <section>:<read|write>");
50616
+ }
50617
+ const expires = flagString(flags, "expires");
50618
+ if (expires !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
50619
+ throw usageError("--expires takes a date, YYYY-MM-DD");
50620
+ }
50621
+ const body = {
50622
+ name,
50623
+ sections,
50624
+ ...expires ? { expiresAt: `${expires}T00:00:00.000Z` } : {}
50625
+ };
50626
+ return {
50627
+ data: await requireClient().post(pathOf(keyOperations.create), body),
50628
+ render: renderCreatedKey
50629
+ };
50630
+ }
50631
+ },
49771
50632
  {
49772
50633
  spec: {
49773
50634
  path: ["keys", "update"],
@@ -49788,7 +50649,7 @@ var commands26 = [
49788
50649
  ],
49789
50650
  scopes: [...keyOperations.update.scopes],
49790
50651
  returns: "ApiKey",
49791
- notes: "A key never grants more than it holds: it can't widen its own scopes or change a key that reaches further (exit 3). Every change is recorded on the key. Minting and revoking stay in the app."
50652
+ notes: "A key never grants more than it holds: it can't widen its own scopes or change a key that reaches further (exit 3). Every change is recorded on the key. Revoking stays in the app."
49792
50653
  },
49793
50654
  run: async ({ args, flags, requireClient }) => {
49794
50655
  const id = requireArg(args, 0, "key id");
@@ -53781,7 +54642,9 @@ function renderRoutinesToday(data, mode) {
53781
54642
  const steps = r.steps.map((s) => {
53782
54643
  const meta = s.meta ? paint(mode, "dim", ` \xB7 ${s.meta}`) : "";
53783
54644
  const kind = s.kind === "step" ? "" : ` [${s.kind}]`;
53784
- return ` ${s.done ? "\u2713" : "\u25CB"} ${s.n}. ${s.title}${kind}${meta}`;
54645
+ const optional = s.optional ? " (optional)" : "";
54646
+ const indent = " ".repeat(s.depth ?? 0);
54647
+ return ` ${indent}${s.done ? "\u2713" : "\u25CB"} ${s.n}. ${s.title}${kind}${optional}${meta}`;
53785
54648
  });
53786
54649
  return [head, ...steps].join(`
53787
54650
  `);
@@ -53793,7 +54656,7 @@ function renderRoutineTaskAdded(data) {
53793
54656
  const last = run.steps?.at(-1)?.title ?? "the task";
53794
54657
  return `${run.routineTitle ?? "(routine)"}: ${last} is the last step of today's run.`;
53795
54658
  }
53796
- var STEP_GRAMMAR = "A step is `habit:<habit id>` (a habit step, titled after the habit), `task:<task id>` (a Work task step, titled after the task) or any other text (a plain step with that title).";
54659
+ var STEP_GRAMMAR = "A step is `habit:<habit id>` (a habit step, titled after the habit), `task:<task id>` (a Work task step, titled after the task), `routine:<routine id>` (a nested routine, titled after it: a run walks its steps in this place) or any other text (a plain step with that title).";
53797
54660
  var ROUTINE_FLAGS = [
53798
54661
  { name: "kind", type: "string", description: "sequence, time_blocked or checklist." },
53799
54662
  { name: "area", type: "string", description: "The Area's ULID (`none` unfiles it)." },
@@ -53816,8 +54679,13 @@ function routineFields(flags) {
53816
54679
  out.notes = notes;
53817
54680
  return out;
53818
54681
  }
54682
+ var LINK_PATH = {
54683
+ habit: habitOperations.get,
54684
+ task: taskOperations.get,
54685
+ routine: routineOperations.get
54686
+ };
53819
54687
  async function stepFrom(client, item, title) {
53820
- const link = /^(habit|task):(\S+)$/.exec(item.trim());
54688
+ const link = /^(habit|task|routine):(\S+)$/.exec(item.trim());
53821
54689
  if (!link) {
53822
54690
  const text = (title ?? item).trim();
53823
54691
  if (!text)
@@ -53826,8 +54694,12 @@ async function stepFrom(client, item, title) {
53826
54694
  }
53827
54695
  const kind = link[1];
53828
54696
  const id = link[2];
53829
- const named = title ?? (await client.get(kind === "habit" ? pathOf(habitOperations.get, { id }) : pathOf(taskOperations.get, { id }))).title ?? kind;
53830
- return kind === "habit" ? { id: ulid3(), title: named, refKind: "habit", habitId: id } : { id: ulid3(), title: named, refKind: "task", taskId: id };
54697
+ const named = title ?? (await client.get(pathOf(LINK_PATH[kind], { id }))).title ?? kind;
54698
+ if (kind === "habit")
54699
+ return { id: ulid3(), title: named, refKind: "habit", habitId: id };
54700
+ if (kind === "task")
54701
+ return { id: ulid3(), title: named, refKind: "task", taskId: id };
54702
+ return { id: ulid3(), title: named, refKind: "routine", childRoutineId: id };
53831
54703
  }
53832
54704
  function stepInput(step, sortOrder) {
53833
54705
  return {
@@ -53836,6 +54708,8 @@ function stepInput(step, sortOrder) {
53836
54708
  refKind: step.refKind,
53837
54709
  habitId: step.refKind === "habit" ? step.habitId : null,
53838
54710
  taskId: step.refKind === "task" ? step.taskId : null,
54711
+ childRoutineId: step.refKind === "routine" ? step.childRoutineId : null,
54712
+ optional: step.optional,
53839
54713
  sortOrder,
53840
54714
  durationMinutes: step.durationMinutes,
53841
54715
  notes: step.notes
@@ -53861,7 +54735,8 @@ function renderRoutine(data) {
53861
54735
  const r = data;
53862
54736
  const steps = (r.steps ?? []).map((s, i) => {
53863
54737
  const kind = s.refKind === "free" ? "" : ` [${s.refKind}]`;
53864
- return ` ${i + 1}. ${s.title}${kind} ${s.id}`;
54738
+ const optional = s.optional ? " (optional)" : "";
54739
+ return ` ${i + 1}. ${s.title}${kind}${optional} ${s.id}`;
53865
54740
  });
53866
54741
  return [
53867
54742
  `${r.title ?? "(routine)"} ${r.id}`,
@@ -53875,6 +54750,30 @@ function putSteps(client, id, steps) {
53875
54750
  });
53876
54751
  return client.patch(pathOf(routineOperations.update, { id }), body);
53877
54752
  }
54753
+ function ruleAt(recurrence, at, timezone) {
54754
+ if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(at))
54755
+ throw usageError("--at takes a 24-hour time: 07:30");
54756
+ if (!recurrence)
54757
+ return { mode: "rigid", freq: "daily", interval: 1, timezone, timeOfDay: at };
54758
+ const keep = {
54759
+ mode: recurrence.mode,
54760
+ timezone: recurrence.timezone,
54761
+ interval: recurrence.interval,
54762
+ freq: recurrence.freq,
54763
+ byDay: recurrence.byDay,
54764
+ until: recurrence.until,
54765
+ count: recurrence.count,
54766
+ unit: recurrence.unit,
54767
+ anchor: recurrence.anchor
54768
+ };
54769
+ return {
54770
+ ...Object.fromEntries(Object.entries(keep).filter(([, v]) => v !== null && v !== undefined)),
54771
+ timeOfDay: at
54772
+ };
54773
+ }
54774
+ function localZone() {
54775
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
54776
+ }
53878
54777
  function readRoutine(client, id) {
53879
54778
  return client.get(pathOf(routineOperations.get, { id }));
53880
54779
  }
@@ -53920,20 +54819,54 @@ var routineCommands = [
53920
54819
  {
53921
54820
  spec: {
53922
54821
  path: ["routines", "update"],
53923
- summary: "Change a routine: its title, kind, Area, notes, or --data.",
54822
+ summary: "Change a routine: its title, kind, Area, notes, start time (--at), or --data.",
53924
54823
  args: [{ name: "id", required: true, description: "Routine ULID." }],
53925
- flags: [{ name: "title", type: "string", description: "A new title." }, ...ROUTINE_FLAGS],
54824
+ flags: [
54825
+ { name: "title", type: "string", description: "A new title." },
54826
+ {
54827
+ name: "at",
54828
+ type: "string",
54829
+ description: "Its start time, 24-hour (07:30); one that doesn't repeat starts daily."
54830
+ },
54831
+ ...ROUTINE_FLAGS
54832
+ ],
53926
54833
  scopes: [...routineOperations.update.scopes],
53927
54834
  returns: "RoutineView",
53928
- notes: `${routeOf(routineOperations.update)}. Only what you pass changes; its steps change with \`do routines steps\`.`
54835
+ notes: `${routeOf(routineOperations.update)}; --at is ${routeOf(routineOperations.attachRecurrence)} with the routine's rule as it is and its time moved (a routine that doesn't repeat yet becomes daily at that time, in this machine's zone). Only what you pass changes; its steps change with \`do routines steps\`. A routine at a time lands on your do calendar, each child routine and own step as its own event.`
53929
54836
  },
53930
54837
  run: async ({ args, flags, requireClient }) => {
53931
54838
  const id = requireArg(args, 0, "routine id");
54839
+ const at = flagString(flags, "at");
53932
54840
  const body = parseInput(updateRoutineInputSchema, routineFields(flags));
53933
- if (Object.keys(body).length === 0) {
53934
- throw usageError("nothing to change \u2014 pass --title, --kind, --area, --notes or --data");
54841
+ if (Object.keys(body).length === 0 && at === undefined) {
54842
+ throw usageError("nothing to change \u2014 pass --title, --kind, --area, --notes, --at or --data");
53935
54843
  }
53936
54844
  const client = requireClient();
54845
+ if (at !== undefined) {
54846
+ const before = await readRoutine(client, id);
54847
+ const rule = parseInput(updateRecurrenceRuleInputSchema, ruleAt(before.recurrence, at.trim(), localZone()));
54848
+ if (flagBool(flags, "dry-run")) {
54849
+ return {
54850
+ data: {
54851
+ dryRun: true,
54852
+ op: "update",
54853
+ entity: "routines",
54854
+ id,
54855
+ before,
54856
+ recurrence: rule,
54857
+ changes: body
54858
+ },
54859
+ render: renderRecord
54860
+ };
54861
+ }
54862
+ const timed = await client.put(pathOf(routineOperations.attachRecurrence, { id }), rule);
54863
+ if (Object.keys(body).length === 0)
54864
+ return { data: timed, render: renderRoutine };
54865
+ return {
54866
+ data: await client.patch(pathOf(routineOperations.update, { id }), body),
54867
+ render: renderRoutine
54868
+ };
54869
+ }
53937
54870
  if (flagBool(flags, "dry-run")) {
53938
54871
  const before = await readRoutine(client, id);
53939
54872
  return {
@@ -53988,31 +54921,52 @@ var routineCommands = [
53988
54921
  {
53989
54922
  spec: {
53990
54923
  path: ["routines", "steps", "add"],
53991
- summary: "Add a step to a routine, at the end or --at a position.",
54924
+ summary: "Add a step to a routine (or a nested routine), at the end or --at a position.",
53992
54925
  args: [
53993
54926
  { name: "id", required: true, description: "Routine ULID." },
53994
- { name: "step", required: true, description: "The step. See grammar." }
54927
+ {
54928
+ name: "step",
54929
+ required: false,
54930
+ description: "The step. See grammar. Leave it out with --routine."
54931
+ }
53995
54932
  ],
53996
54933
  flags: [
53997
54934
  { name: "at", type: "string", description: "Its position, 1-based (default: last)." },
53998
- { name: "title", type: "string", description: "Its title (instead of the linked name)." }
54935
+ { name: "title", type: "string", description: "Its title (instead of the linked name)." },
54936
+ {
54937
+ name: "routine",
54938
+ type: "string",
54939
+ description: "Nest this routine (its ULID) as the step; same as `routine:<id>`."
54940
+ },
54941
+ {
54942
+ name: "optional",
54943
+ type: "boolean",
54944
+ description: "An optional step: it never keeps the routine from being done."
54945
+ }
53999
54946
  ],
54000
54947
  scopes: [...routineOperations.get.scopes, ...routineOperations.update.scopes],
54001
54948
  returns: "RoutineView",
54002
54949
  grammar: { step: STEP_GRAMMAR },
54003
- notes: `${routeOf(routineOperations.get)}, then ${routeOf(routineOperations.update)} with every step (the others keep their ids).`
54950
+ notes: `${routeOf(routineOperations.get)}, then ${routeOf(routineOperations.update)} with every step (the others keep their ids). A nested routine (\`--routine <id>\`) walks its steps in this place when the routine runs, to any depth; one that would contain itself is a 400. e.g. \`do routines steps add <morning> --routine <teeth> --optional\`.`
54004
54951
  },
54005
54952
  run: async ({ args, flags, requireClient }) => {
54006
54953
  const id = requireArg(args, 0, "routine id");
54007
- const item = args.slice(1).join(" ").trim();
54954
+ const child = flagString(flags, "routine");
54955
+ const words = args.slice(1).join(" ").trim();
54956
+ if (child && words)
54957
+ throw usageError("--routine is the step; pass no other step with it");
54958
+ const item = child ? `routine:${child}` : words;
54008
54959
  if (!item)
54009
- throw usageError("routines steps add needs the step: `habit:<id>`, `task:<id>` or a title");
54960
+ throw usageError("routines steps add needs the step: `habit:<id>`, `task:<id>`, `routine:<id>` (or --routine <id>) or a title");
54010
54961
  const client = requireClient();
54011
54962
  const routine = await readRoutine(client, id);
54012
54963
  const steps = routine.steps.map((s, i) => stepInput(s, i));
54013
54964
  const at = flagString(flags, "at");
54014
54965
  const index = at === undefined ? steps.length : positionOf(at, steps.length + 1, "--at");
54015
- steps.splice(index, 0, await stepFrom(client, item, flagString(flags, "title")));
54966
+ const step = await stepFrom(client, item, flagString(flags, "title"));
54967
+ if (flagBool(flags, "optional"))
54968
+ step.optional = true;
54969
+ steps.splice(index, 0, step);
54016
54970
  return { data: await putSteps(client, id, steps), render: renderRoutine };
54017
54971
  }
54018
54972
  },
@@ -54122,7 +55076,7 @@ var commands42 = [
54122
55076
  flags: [],
54123
55077
  scopes: [...routineOperations.today.scopes],
54124
55078
  returns: "RoutinesTodayResult",
54125
- notes: `${routeOf(routineOperations.today)}. Today is your saved time zone's day. A routine is on it when its rule has it due, it has a slot today, or it ran today. Each step's \`kind\` is step, habit or task (a Work task); \`rhythm\` is its last 14 days, oldest first (all, some, none, off).`
55079
+ notes: `${routeOf(routineOperations.today)}. Today is your saved time zone's day. A routine is on it when its rule has it due, it has a slot today, or it ran today. Each step's \`kind\` is step, habit, task (a Work task) or routine (a nested routine's heading, its steps one \`depth\` deeper; V3-121); \`rhythm\` is its last 14 days, oldest first (all, some, none, off).`
54126
55080
  },
54127
55081
  run: async ({ requireClient }) => ({
54128
55082
  data: await requireClient().get(pathOf(routineOperations.today)),
@@ -55534,7 +56488,8 @@ var commands50 = [
55534
56488
  args: [],
55535
56489
  flags: [{ name: "data", type: "string", description: "JSON of fields to change." }],
55536
56490
  scopes: [...settingsOperations.update.scopes],
55537
- returns: "UserSettings"
56491
+ returns: "UserSettings",
56492
+ notes: `Appearance keys: glass (liquid|frosted|solid), followSky, nightUntilWake, largerText, reduceMotion, showStreakNumbers, themePack (do|skyrim|pokemon), themePackScope (section|everywhere). E.g. --data '{"showStreakNumbers":false}'.`
55538
56493
  },
55539
56494
  run: async ({ flags, requireClient }) => {
55540
56495
  const body = parseInput(updateUserSettingsInputSchema, readDataFlag(flags));
@@ -55743,6 +56698,119 @@ var commands53 = [
55743
56698
  })
55744
56699
  ];
55745
56700
 
56701
+ // ../../packages/domain/src/task-series.ts
56702
+ function rhythmFromWord(word) {
56703
+ const map = {
56704
+ week: { freq: "weekly", interval: 1 },
56705
+ weekly: { freq: "weekly", interval: 1 },
56706
+ fortnight: { freq: "weekly", interval: 2 },
56707
+ month: { freq: "monthly", interval: 1 },
56708
+ monthly: { freq: "monthly", interval: 1 },
56709
+ quarter: { freq: "monthly", interval: 3 },
56710
+ quarterly: { freq: "monthly", interval: 3 },
56711
+ "half-year": { freq: "monthly", interval: 6 },
56712
+ year: { freq: "yearly", interval: 1 },
56713
+ yearly: { freq: "yearly", interval: 1 }
56714
+ };
56715
+ return map[word.trim().toLowerCase()] ?? null;
56716
+ }
56717
+
56718
+ // src/commands/task-series.ts
56719
+ function renderSeries(data, mode) {
56720
+ const page = data;
56721
+ const list = Array.isArray(page) ? page : ("data" in page) ? page.data : [page];
56722
+ if (list.length === 0)
56723
+ return paint(mode, "dim", "(no task series)");
56724
+ return list.map((s) => {
56725
+ const next = s.active ? s.nextMadeOn ? `next made ${s.nextMadeOn}` : "next made" : "stopped";
56726
+ const items = s.items.map((i) => i.taskId ? `${i.label} (${i.status ?? "?"})` : `${i.label} (to come)`).join(", ");
56727
+ return ` ${s.title} ${paint(mode, "dim", s.rhythm)} ${next} ${paint(mode, "dim", s.id)}
56728
+ ${items}`;
56729
+ }).join(`
56730
+ `);
56731
+ }
56732
+ var commands54 = [
56733
+ defineCommand({
56734
+ operation: taskSeriesOperations.make,
56735
+ path: ["tasks", "make-series"],
56736
+ summary: "Make a task the first of a series: each next one is its own task.",
56737
+ args: [{ name: "task", required: true, description: "The task's ref (DO-150) or id." }],
56738
+ flags: [
56739
+ {
56740
+ name: "every",
56741
+ type: "string",
56742
+ description: "week, fortnight, month, quarter, half-year or year."
56743
+ },
56744
+ {
56745
+ name: "label",
56746
+ type: "string",
56747
+ description: "How each is named: quarter (Q1\u2026), month (October) or date (Mon 26 Oct)."
56748
+ },
56749
+ {
56750
+ name: "lead",
56751
+ type: "string",
56752
+ description: "Make each next task this many days before it's due (default 14)."
56753
+ }
56754
+ ],
56755
+ returns: "TaskSeries",
56756
+ run: async ({ args, flags, requireClient }) => {
56757
+ const task = requireArg(args, 0, "task");
56758
+ const every = flagString(flags, "every");
56759
+ const rule = every ? rhythmFromWord(every) : null;
56760
+ if (!rule) {
56761
+ throw usageError("tasks make-series needs --every week|fortnight|month|quarter|half-year|year");
56762
+ }
56763
+ const label = flagString(flags, "label");
56764
+ const lead = flagString(flags, "lead");
56765
+ const body = parseInput(makeTaskSeriesInputSchema, {
56766
+ rule,
56767
+ ...label ? { occurrenceLabel: label.trim().toLowerCase() } : {},
56768
+ ...lead !== undefined ? { leadDays: Number(lead) } : {}
56769
+ });
56770
+ return {
56771
+ data: await requireClient().post(pathOf(taskSeriesOperations.make, { id: task }), body),
56772
+ render: renderSeries
56773
+ };
56774
+ }
56775
+ }),
56776
+ defineCommand({
56777
+ operation: taskSeriesOperations.list,
56778
+ path: ["series", "list"],
56779
+ summary: "List the task series, with their rhythm and what's next.",
56780
+ returns: "TaskSeries[]",
56781
+ run: async ({ requireClient }) => ({
56782
+ data: await requireClient().get(pathOf(taskSeriesOperations.list)),
56783
+ render: renderSeries
56784
+ })
56785
+ }),
56786
+ defineCommand({
56787
+ operation: taskSeriesOperations.get,
56788
+ path: ["series", "show"],
56789
+ summary: "Show a task series: its strip of tasks and the next to come.",
56790
+ args: [{ name: "series", required: true, description: "The series' id." }],
56791
+ returns: "TaskSeries",
56792
+ run: async ({ args, requireClient }) => ({
56793
+ data: await requireClient().get(pathOf(taskSeriesOperations.get, { id: requireArg(args, 0, "series") })),
56794
+ render: renderSeries
56795
+ })
56796
+ }),
56797
+ defineCommand({
56798
+ operation: taskSeriesOperations.stop,
56799
+ path: ["series", "stop"],
56800
+ summary: "Stop a task series: no more tasks are made; the ones made stay.",
56801
+ args: [{ name: "series", required: true, description: "The series' id." }],
56802
+ returns: "{ ok, stopped }",
56803
+ run: async ({ args, requireClient }) => {
56804
+ const id = requireArg(args, 0, "series");
56805
+ await requireClient().delete(pathOf(taskSeriesOperations.stop, { id }));
56806
+ return {
56807
+ data: { ok: true, stopped: id },
56808
+ render: () => "Stopped the series. Its tasks stay."
56809
+ };
56810
+ }
56811
+ })
56812
+ ];
56813
+
55746
56814
  // src/capture.ts
55747
56815
  function parseCapture2(input, now) {
55748
56816
  const parsed = parseCapture(input, now);
@@ -55816,7 +56884,7 @@ function repeatFromFlags(flags) {
55816
56884
  out.anchor = "from_completion";
55817
56885
  return out;
55818
56886
  }
55819
- var commands54 = [
56887
+ var commands55 = [
55820
56888
  {
55821
56889
  spec: {
55822
56890
  path: ["capture"],
@@ -55873,6 +56941,7 @@ var commands54 = [
55873
56941
  title: parsed.title,
55874
56942
  tags: parsed.tags,
55875
56943
  ...parsed.starred ? { starred: parsed.starred } : {},
56944
+ ...parsed.priority ? { priority: parsed.priority } : {},
55876
56945
  ...parsed.area ? { areaName: parsed.area } : {},
55877
56946
  ...scheduledAt ? { scheduledAt } : {},
55878
56947
  ...dueAt ? { dueAt } : {},
@@ -56273,7 +57342,7 @@ var commands54 = [
56273
57342
  ];
56274
57343
 
56275
57344
  // src/commands/time-blocks.ts
56276
- var commands55 = [
57345
+ var commands56 = [
56277
57346
  {
56278
57347
  spec: {
56279
57348
  path: ["time-blocks", "record"],
@@ -56454,7 +57523,7 @@ function renderLandedCounts(data, mode) {
56454
57523
  return out.join(`
56455
57524
  `);
56456
57525
  }
56457
- var commands56 = [
57526
+ var commands57 = [
56458
57527
  {
56459
57528
  spec: {
56460
57529
  path: ["today"],
@@ -56826,7 +57895,7 @@ function renderViews(data, mode) {
56826
57895
  function renderView(data, mode) {
56827
57896
  return renderViews([data], mode);
56828
57897
  }
56829
- var commands57 = [
57898
+ var commands58 = [
56830
57899
  defineCommand({
56831
57900
  operation: workViewOperations.list,
56832
57901
  path: ["views", "list"],
@@ -56945,7 +58014,7 @@ var commands57 = [
56945
58014
  ];
56946
58015
 
56947
58016
  // src/commands/worktree-links.ts
56948
- var commands58 = [
58017
+ var commands59 = [
56949
58018
  {
56950
58019
  spec: {
56951
58020
  path: ["worktree-links", "list"],
@@ -57109,7 +58178,8 @@ var FEATURE_COMMANDS = [
57109
58178
  ...commands55,
57110
58179
  ...commands56,
57111
58180
  ...commands57,
57112
- ...commands58
58181
+ ...commands58,
58182
+ ...commands59
57113
58183
  ];
57114
58184
 
57115
58185
  // src/registry.ts