@robbeverhelst/do 1.333.0 → 1.334.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 +492 -36
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -983,6 +983,10 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
983
983
  const positionals = [];
984
984
  const flags = {};
985
985
  let positionalOnly = false;
986
+ const setValue = (name, value) => {
987
+ const was = flags[name];
988
+ flags[name] = typeof was === "string" ? [was, value] : Array.isArray(was) ? [...was, value] : value;
989
+ };
986
990
  for (let i = 0;i < argv.length; i++) {
987
991
  const token = argv[i];
988
992
  if (token === undefined)
@@ -999,7 +1003,7 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
999
1003
  const body = token.slice(2);
1000
1004
  const eq = body.indexOf("=");
1001
1005
  if (eq !== -1) {
1002
- flags[body.slice(0, eq)] = body.slice(eq + 1);
1006
+ setValue(body.slice(0, eq), body.slice(eq + 1));
1003
1007
  continue;
1004
1008
  }
1005
1009
  if (booleans.has(body)) {
@@ -1008,7 +1012,7 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
1008
1012
  }
1009
1013
  const next = argv[i + 1];
1010
1014
  if (next !== undefined && (next === "-" || !next.startsWith("-"))) {
1011
- flags[body] = next;
1015
+ setValue(body, next);
1012
1016
  i++;
1013
1017
  } else {
1014
1018
  flags[body] = true;
@@ -1030,7 +1034,11 @@ function parseArgs(argv, booleanFlags = GLOBAL_BOOLEAN_FLAGS) {
1030
1034
  }
1031
1035
  function flagString(flags, name) {
1032
1036
  const v = flags[name];
1033
- return typeof v === "string" ? v : undefined;
1037
+ return typeof v === "string" ? v : Array.isArray(v) ? v.at(-1) : undefined;
1038
+ }
1039
+ function flagStrings(flags, name) {
1040
+ const v = flags[name];
1041
+ return typeof v === "string" ? [v] : Array.isArray(v) ? v : [];
1034
1042
  }
1035
1043
  function flagBool(flags, name) {
1036
1044
  return flags[name] === true;
@@ -14410,7 +14418,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14410
14418
 
14411
14419
  // src/meta.ts
14412
14420
  var CLI_NAME = "do";
14413
- var VERSION = "1.333.0";
14421
+ var VERSION = "1.334.0";
14414
14422
  var API_PREFIX = "/api/v1";
14415
14423
  var DEFAULT_API_URL = "http://localhost:3001";
14416
14424
 
@@ -17867,6 +17875,7 @@ var automationRuns = pgTable("automation_runs", {
17867
17875
  automationId: text("automation_id").notNull().references(() => automations.id),
17868
17876
  eventType: text("event_type").notNull(),
17869
17877
  eventEntityId: text("event_entity_id").notNull(),
17878
+ eventValue: jsonb("event_value"),
17870
17879
  status: automationRunStatus("status").notNull(),
17871
17880
  detail: text("detail"),
17872
17881
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
@@ -20424,7 +20433,8 @@ var animalDayCountSchema = object({ day: string2(), count: number2().int() });
20424
20433
  var animalWeightPointSchema = object({
20425
20434
  day: string2(),
20426
20435
  grams: number2(),
20427
- at: exports_coerce.date()
20436
+ at: exports_coerce.date(),
20437
+ note: string2().nullish()
20428
20438
  });
20429
20439
  var animalFileSchema = object({
20430
20440
  id: ulidSchema,
@@ -20545,6 +20555,7 @@ var animalLogInputSchema = discriminatedUnion("kind", [
20545
20555
  kind: literal("weight"),
20546
20556
  value: number2().positive().max(200000),
20547
20557
  unit: _enum2(["g", "kg"]).default("g"),
20558
+ note: string2().trim().min(1).max(120).optional(),
20548
20559
  occurredAt: occurredAtSchema
20549
20560
  }).strict(),
20550
20561
  object({
@@ -22030,13 +22041,25 @@ var occurrenceSchema = createSelectSchema(occurrences, {
22030
22041
  completedAt: exports_coerce.date().nullable()
22031
22042
  });
22032
22043
  var byDayCodeSchema = _enum2(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]);
22044
+ var byDayOrdinalSchema = _enum2(["1", "2", "3", "4", "-1"]);
22045
+ var byDaySchema = union([
22046
+ byDayCodeSchema,
22047
+ templateLiteral([byDayOrdinalSchema, byDayCodeSchema])
22048
+ ]);
22049
+ var BY_DAY_RE = /^(-1|[1-4])?(MO|TU|WE|TH|FR|SA|SU)$/;
22050
+ function parseByDay(code) {
22051
+ const m = BY_DAY_RE.exec(code);
22052
+ if (!m)
22053
+ return null;
22054
+ return { ordinal: m[1] ? Number(m[1]) : null, weekday: m[2] };
22055
+ }
22033
22056
  var recurrenceRuleParams = {
22034
22057
  timezone: string2().min(1).default("UTC"),
22035
22058
  interval: number2().int().positive().default(1),
22036
22059
  mode: recurrenceModeSchema,
22037
22060
  timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "time-of-day must be HH:MM (24h)").nullish(),
22038
22061
  freq: recurrenceFreqSchema.optional(),
22039
- byDay: array(byDayCodeSchema).optional(),
22062
+ byDay: array(byDaySchema).optional(),
22040
22063
  until: exports_coerce.date().optional(),
22041
22064
  count: number2().int().positive().optional(),
22042
22065
  unit: recurrenceUnitSchema.optional(),
@@ -22046,6 +22069,14 @@ function refineRecurrenceRule(v, ctx) {
22046
22069
  if (v.mode === "rigid" && !v.freq) {
22047
22070
  ctx.addIssue({ code: "custom", path: ["freq"], message: "rigid rules require a freq" });
22048
22071
  }
22072
+ const ordinal = v.byDay?.findIndex((code) => parseByDay(code)?.ordinal != null) ?? -1;
22073
+ if (ordinal >= 0 && v.freq !== "monthly" && v.freq !== "yearly") {
22074
+ ctx.addIssue({
22075
+ code: "custom",
22076
+ path: ["byDay", ordinal],
22077
+ message: "an ordinal weekday (1SU, -1FR) needs a monthly or yearly freq"
22078
+ });
22079
+ }
22049
22080
  if (v.mode === "flexible") {
22050
22081
  if (!v.unit) {
22051
22082
  ctx.addIssue({ code: "custom", path: ["unit"], message: "flexible rules require a unit" });
@@ -22437,6 +22468,8 @@ var collectionEntries = pgTable("collection_entries", {
22437
22468
  fileIds: text("file_ids").array().notNull().default([]),
22438
22469
  minutes: integer2("minutes"),
22439
22470
  ops: jsonb("ops").$type().notNull().default([]),
22471
+ chapter: text("chapter"),
22472
+ linkedNoteId: text("linked_note_id").references(() => nodes.id, { onDelete: "set null" }),
22440
22473
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
22441
22474
  }, (t) => [
22442
22475
  index("collection_entries_item_occurred_idx").on(t.itemId, t.occurredOn),
@@ -22548,8 +22581,11 @@ var collectionEntrySchema = createSelectSchema(collectionEntries, {
22548
22581
  taskId: string2().nullable().default(null),
22549
22582
  fileIds: array(string2()).default([]),
22550
22583
  minutes: number2().int().nullable().default(null),
22551
- ops: array(unknown()).default([])
22584
+ ops: array(unknown()).default([]),
22585
+ chapter: string2().nullable().default(null),
22586
+ linkedNoteId: string2().nullable().default(null)
22552
22587
  });
22588
+ var entryChapterSchema = string2().trim().min(1).max(60);
22553
22589
  var entryCostSchema = number2().min(0).max(1e7).multipleOf(0.01);
22554
22590
  var entryFileIdsSchema = array(ulidSchema).max(20);
22555
22591
  var createCollectionEntryInputSchema = createInsertSchema(collectionEntries, {
@@ -22561,7 +22597,9 @@ var createCollectionEntryInputSchema = createInsertSchema(collectionEntries, {
22561
22597
  cost: entryCostSchema.nullish(),
22562
22598
  contactId: ulidSchema.nullish(),
22563
22599
  taskId: ulidSchema.nullish(),
22564
- fileIds: entryFileIdsSchema.optional()
22600
+ fileIds: entryFileIdsSchema.optional(),
22601
+ chapter: entryChapterSchema.nullish(),
22602
+ linkedNoteId: ulidSchema.nullish()
22565
22603
  }).omit({ tenantId: true, itemId: true, createdAt: true, minutes: true, ops: true });
22566
22604
  var updateCollectionEntryInputSchema = object({
22567
22605
  occurredOn: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "must be an ISO date (YYYY-MM-DD)").optional(),
@@ -22570,7 +22608,9 @@ var updateCollectionEntryInputSchema = object({
22570
22608
  kind: collectionEntryKindSchema.optional(),
22571
22609
  cost: entryCostSchema.nullish(),
22572
22610
  contactId: ulidSchema.nullish(),
22573
- fileIds: entryFileIdsSchema.optional()
22611
+ fileIds: entryFileIdsSchema.optional(),
22612
+ chapter: entryChapterSchema.nullish(),
22613
+ linkedNoteId: ulidSchema.nullish()
22574
22614
  }).strict();
22575
22615
  var collectionItemViewSchema = collectionItemSchema.extend({
22576
22616
  title: string2(),
@@ -22643,6 +22683,7 @@ var collectionLogInputSchema = object({
22643
22683
  status: string2().trim().min(1).max(40).optional(),
22644
22684
  rating: collectionRatingSchema.nullish(),
22645
22685
  note: string2().max(2000).nullish(),
22686
+ chapter: entryChapterSchema.nullish(),
22646
22687
  said: string2().trim().min(1).max(500).optional(),
22647
22688
  occurredOn: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "must be an ISO date (YYYY-MM-DD)").optional()
22648
22689
  }).strict();
@@ -25566,13 +25607,19 @@ var moneyGoalLineSchema = createSelectSchema(moneyGoalLines, {
25566
25607
  createdAt: exports_coerce.date(),
25567
25608
  updatedAt: exports_coerce.date()
25568
25609
  });
25610
+ var MONEY_GOAL_SAVED_FROM = ["account", "account_share", "contributions"];
25611
+ var moneyGoalSavedFromSchema = _enum2(MONEY_GOAL_SAVED_FROM);
25569
25612
  var moneyGoalViewSchema = moneyGoalSchema.extend({
25570
25613
  title: string2(),
25571
25614
  visibility: _enum2(NODE_VISIBILITY_VALUES),
25572
25615
  accountTitle: string2().nullable(),
25573
25616
  accountBalance: amount.nullable(),
25617
+ accountBalanceKnown: boolean2().nullable(),
25574
25618
  projectTitle: string2().nullable(),
25575
25619
  saved: amount,
25620
+ savedFrom: moneyGoalSavedFromSchema,
25621
+ remaining: amount,
25622
+ monthlyNeed: amount.nullable(),
25576
25623
  linesPlanned: amount,
25577
25624
  linesPaid: amount
25578
25625
  });
@@ -31268,6 +31315,140 @@ var journalExportSchema = object({
31268
31315
  nextSince: exports_coerce.date()
31269
31316
  });
31270
31317
 
31318
+ // ../../packages/schema/src/contracts/journal-private.ts
31319
+ var JOURNAL_PRIVATE_TERMS = {
31320
+ alcohol: [
31321
+ "alcohol*",
31322
+ "drunk*",
31323
+ "drink*",
31324
+ "beer*",
31325
+ "wine*",
31326
+ "whisky",
31327
+ "whiskey",
31328
+ "vodka*",
31329
+ "gin",
31330
+ "rum",
31331
+ "tequila*",
31332
+ "cocktail*",
31333
+ "hangover*",
31334
+ "hungover",
31335
+ "tipsy",
31336
+ "booze*",
31337
+ "sober*",
31338
+ "sobriety",
31339
+ "bier*",
31340
+ "wijn*",
31341
+ "drank*",
31342
+ "dronken*",
31343
+ "borrel*",
31344
+ "pint",
31345
+ "pints",
31346
+ "pintje*",
31347
+ "kater*",
31348
+ "zuipen",
31349
+ "gezopen"
31350
+ ],
31351
+ smoking: [
31352
+ "smok*",
31353
+ "cigar*",
31354
+ "tobacco",
31355
+ "nicotine",
31356
+ "vape*",
31357
+ "vaping",
31358
+ "roken",
31359
+ "rook",
31360
+ "rookte*",
31361
+ "gerookt",
31362
+ "roker*",
31363
+ "sigaar*",
31364
+ "sigaret*",
31365
+ "tabak*",
31366
+ "peuk*",
31367
+ "vapen"
31368
+ ],
31369
+ cannabis: [
31370
+ "cannabis",
31371
+ "weed",
31372
+ "hash",
31373
+ "hashish",
31374
+ "marijuana",
31375
+ "joint",
31376
+ "joints",
31377
+ "jointje*",
31378
+ "stoned",
31379
+ "thc",
31380
+ "cbd",
31381
+ "edibles",
31382
+ "wiet*",
31383
+ "hasj*",
31384
+ "blowen",
31385
+ "blowt",
31386
+ "geblowd"
31387
+ ],
31388
+ drugs: [
31389
+ "drug*",
31390
+ "cocaine",
31391
+ "coca\xEFne",
31392
+ "coke",
31393
+ "mdma",
31394
+ "ecstasy",
31395
+ "xtc",
31396
+ "ketamine",
31397
+ "lsd",
31398
+ "shrooms",
31399
+ "paddo*",
31400
+ "amphetamine*",
31401
+ "heroin*",
31402
+ "hero\xEFne",
31403
+ "opioid*"
31404
+ ],
31405
+ relapse: [
31406
+ "relaps*",
31407
+ "craving*",
31408
+ "rehab",
31409
+ "aa meeting*",
31410
+ "terugval*",
31411
+ "hervallen",
31412
+ "afkick*",
31413
+ "ontwenning*"
31414
+ ],
31415
+ fertility: [
31416
+ "fertil*",
31417
+ "ivf",
31418
+ "iui",
31419
+ "ovulat*",
31420
+ "conceiv*",
31421
+ "conception",
31422
+ "pregnan*",
31423
+ "sperm*",
31424
+ "embryo*",
31425
+ "miscarr*",
31426
+ "ttc",
31427
+ "vruchtbaar*",
31428
+ "ovulatie",
31429
+ "eisprong",
31430
+ "zwanger*",
31431
+ "sperma*",
31432
+ "miskraam",
31433
+ "kinderwens"
31434
+ ]
31435
+ };
31436
+ var LETTER = "[\\p{L}\\p{N}_]";
31437
+ var PRIVATE = new RegExp(`(?<!${LETTER})(?:${Object.values(JOURNAL_PRIVATE_TERMS).flat().map((term) => {
31438
+ const stem = term.replace(/\*$/, "").replace(/\s+/g, "\\s+");
31439
+ return term.endsWith("*") ? `${stem}${LETTER}*` : stem;
31440
+ }).join("|")})(?!${LETTER})`, "iu");
31441
+ function isPrivateLine(line) {
31442
+ return PRIVATE.test(line);
31443
+ }
31444
+ function withoutPrivateLines(text) {
31445
+ const kept = text.split(/\r?\n/).filter((line) => !isPrivateLine(line)).join(`
31446
+ `).replace(/\n{3,}/g, `
31447
+
31448
+ `).trim();
31449
+ return kept || null;
31450
+ }
31451
+
31271
31452
  // ../../packages/schema/src/contracts/journal-import.ts
31272
31453
  var journalImportBatchSchema = string2().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "a batch is a month (YYYY-MM)");
31273
31454
  var journalImportEntrySchema = _enum2(["summary", "retro", "log"]);
@@ -31278,6 +31459,7 @@ var journalImportFoundSchema = object({
31278
31459
  documents: number2().int().nonnegative().optional(),
31279
31460
  habits: number2().int().nonnegative().optional()
31280
31461
  }).strict();
31462
+ var keptLine = (max) => string2().trim().min(1).max(max).transform((v) => isPrivateLine(v) ? null : v).nullish();
31281
31463
  var JOURNAL_IMPORT_MAX_ITEMS = 400;
31282
31464
  var journalImportItemInputSchema = object({
31283
31465
  day: journalDateSchema,
@@ -31285,12 +31467,12 @@ var journalImportItemInputSchema = object({
31285
31467
  sourceHash: string2().trim().min(1).max(128),
31286
31468
  entry: journalImportEntrySchema.default("log"),
31287
31469
  hasCheck: boolean2().default(false),
31288
- title: string2().trim().min(1).max(200).nullish(),
31289
- body: string2().trim().min(1).max(20000).nullish(),
31470
+ title: keptLine(200),
31471
+ body: string2().trim().min(1).max(20000).transform(withoutPrivateLines).nullish(),
31290
31472
  rating: number2().min(1).max(10).nullish(),
31291
- ratingQuestion: string2().trim().min(1).max(80).nullish(),
31473
+ ratingQuestion: keptLine(80),
31292
31474
  sleep: number2().min(0).max(24).nullish(),
31293
- highlight: string2().trim().min(1).max(200).nullish(),
31475
+ highlight: keptLine(200),
31294
31476
  milestoneCandidate: boolean2().default(false),
31295
31477
  health: boolean2().default(false),
31296
31478
  found: journalImportFoundSchema.default({})
@@ -31356,8 +31538,25 @@ var journalImportAnswerInputSchema = object({
31356
31538
  rating: number2().min(1).max(10).nullable().optional(),
31357
31539
  skip: boolean2().optional()
31358
31540
  }).strict().refine((v) => Object.keys(v).length > 0, { message: "nothing to answer" });
31541
+ var journalImportRatingAnswerSchema = object({ day: journalDateSchema, rating: number2().min(1).max(10) }).strict();
31542
+ var journalImportApproveInputSchema = object({
31543
+ batch: journalImportBatchSchema.optional(),
31544
+ all: literal(true).optional(),
31545
+ ratings: array(journalImportRatingAnswerSchema).max(JOURNAL_IMPORT_MAX_ITEMS).default([])
31546
+ }).strict().refine((v) => v.batch !== undefined !== (v.all === true), {
31547
+ message: "approve one batch or all: send batch or all, not both"
31548
+ }).refine((v) => new Set(v.ratings.map((r) => r.day)).size === v.ratings.length, {
31549
+ message: "one rating per day",
31550
+ path: ["ratings"]
31551
+ });
31552
+ var journalImportOpenDaySchema = object({
31553
+ day: journalDateSchema,
31554
+ ratingQuestion: string2()
31555
+ });
31359
31556
  var journalImportApproveSchema = object({
31360
31557
  imported: number2().int(),
31558
+ batches: array(journalImportBatchSchema).default([]),
31559
+ open: array(journalImportOpenDaySchema).default([]),
31361
31560
  overview: journalImportOverviewSchema
31362
31561
  });
31363
31562
 
@@ -31745,12 +31944,25 @@ var journalOperations = defineOperations({
31745
31944
  scopes: ["journal:write"],
31746
31945
  tag: "Journal",
31747
31946
  summary: "Stage daily notes for the backfill",
31748
- description: "Stage parsed daily notes (`do journal import push <dir>`): one item per day (at most 400 a request) with its source file and hash, what becomes its entry (`summary`, `retro` or `log`), a one-liner, the summary's text (health lines left out), a clear `rating` or a `ratingQuestion` (\"~5/10\"), a highlight, whether it may be a milestone or had health lines, and the links counted. Nothing is written into the Journal: the member reviews and approves each month in the app, and a key can't approve. A day still waiting is replaced; one already imported is left alone (`alreadyImported`). Staged days stand behind the Journal's wall.",
31947
+ description: "Stage parsed daily notes (`do journal import push <dir>`): one item per day (at most 400 a request) with its source file and hash, what becomes its entry (`summary`, `retro` or `log`), a one-liner, the summary's text (health lines left out), a clear `rating` or a `ratingQuestion` (\"~5/10\"), a highlight, whether it may be a milestone or had health lines, and the links counted. A line about drinking, smoking, cannabis or other drugs, relapse or fertility is stripped from the title, text, highlight and rating question here, whatever parsed the day. Nothing is written into the Journal until a batch is approved (`journal_import_approve`, or the member's Approve in the app). A day still waiting is replaced; one already imported is left alone (`alreadyImported`). Staged days stand behind the Journal's wall.",
31749
31948
  body: journalImportUploadInputSchema,
31750
31949
  status: 200,
31751
31950
  response: { description: "How the upload went.", view: "JournalImportUpload" },
31752
31951
  errors: { 400: true }
31753
31952
  },
31953
+ approveImport: {
31954
+ method: "post",
31955
+ path: "/journal/imports/approve",
31956
+ scopes: ["journal:write"],
31957
+ tag: "Journal",
31958
+ summary: "Approve staged days into the Journal",
31959
+ description: 'Write staged days into the Journal (`do journal import approve <YYYY-MM|--all>`): one batch (`batch`, a month `YYYY-MM`) or every staged batch (`all: true`), never both. `ratings` answers the rating questions inline, one per day (`[{"day":"2026-10-03","rating":7}]`, 1 to 10). A day whose question has no answer stays staged and comes back in `open` with its question; the rest are written as imports (one-liner, summary, highlight, the `rating` and `sleep` Metrics), and a day the Journal already has keeps what\'s there and only gains a missing rating or sleep. An agent\'s approve goes through the same writes as `journal_write_day`: it shows in the member\'s change list with Undo, and past the bulk limit it waits for the member\'s confirmation (428). Lines about drinking, smoking, drugs, relapse or fertility are never written. 400 when a rating names a day that isn\'t waiting in the batch, or nothing is ready and nothing is open.',
31960
+ body: journalImportApproveInputSchema,
31961
+ status: 200,
31962
+ response: { description: "How the approve went.", view: "JournalImportApprove" },
31963
+ errors: { 400: true },
31964
+ operationId: "journal_import_approve"
31965
+ },
31754
31966
  importOverview: {
31755
31967
  method: "get",
31756
31968
  path: "/journal/imports",
@@ -33674,7 +33886,7 @@ var moneyOperations = defineOperations({
33674
33886
  scopes: ["finance:read"],
33675
33887
  tag: "Money",
33676
33888
  summary: "List money goals",
33677
- description: "The goals this key may see (the household's, and its member's Only me ones), soonest target first: `targetAmount`, `targetOn`, `monthlyPlan`, the pot's account and Project by name, `saved` (`savedBefore` plus the transactions counted toward it) and the lines' `linesPlanned`/`linesPaid`.",
33889
+ description: "The goals this key may see (the household's, and its member's Only me ones), soonest target first: `targetAmount`, `targetOn`, `monthlyPlan`, the pot's account and Project by name, `saved` and where it comes from (`savedFrom`: `account`, the balance of the account it's alone in; `account_share`, its own capped by what a shared account has left, soonest target first; `contributions`, `savedBefore` plus the transactions counted toward it, also when the account's balance is unknown, `accountBalanceKnown: false`), `remaining`, `monthlyNeed` (to `targetOn`) and the lines' `linesPlanned`/`linesPaid`.",
33678
33890
  status: 200,
33679
33891
  response: { description: "The goals." }
33680
33892
  },
@@ -34814,7 +35026,8 @@ var planUnplaceInputSchema = object({ taskId: ulidSchema }).strict();
34814
35026
  var planListQuerySchema = object({
34815
35027
  kind: planKindSchema.optional(),
34816
35028
  status: planStatusSchema.optional(),
34817
- startsOn: civilDaySchema.optional()
35029
+ startsOn: civilDaySchema.optional(),
35030
+ taskId: ulidSchema.optional()
34818
35031
  });
34819
35032
  var planTaskSchema = object({
34820
35033
  id: ulidSchema,
@@ -35034,6 +35247,7 @@ var automationListItemSchema = automationViewSchema.extend({
35034
35247
  madeBy: object({ userId: string2(), name: string2(), you: boolean2() }).nullable(),
35035
35248
  lastRun: automationRunSummarySchema.nullable()
35036
35249
  });
35250
+ var automationRunNowSchema = object({ queued: literal(true) });
35037
35251
 
35038
35252
  // ../../packages/schema/src/db/shopping.ts
35039
35253
  var shoppingLists = pgTable("shopping_lists", {
@@ -38787,6 +39001,18 @@ var WEEKDAY_ALT = "monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|
38787
39001
  var EVERY_N_RE = /\bevery\s+(\d+)\s+(day|days|week|weeks|month|months|year|years)\b/i;
38788
39002
  var EVERY_WEEKDAYS_RE = /\bevery\s+(?:weekday|weekdays|work\s?day|work\s?days)\b/i;
38789
39003
  var EVERY_WEEKDAY_RE = new RegExp(`\\bevery\\s+(${WEEKDAY_ALT})\\b`, "i");
39004
+ var EVERY_NTH_WEEKDAY_RE = new RegExp(`\\bevery\\s+(first|1st|second|2nd|third|3rd|fourth|4th|last)\\s+(${WEEKDAY_ALT}|sun)(?:\\s+of\\s+(?:the|each|every)\\s+month)?\\b`, "i");
39005
+ var NTH = {
39006
+ first: "1",
39007
+ "1st": "1",
39008
+ second: "2",
39009
+ "2nd": "2",
39010
+ third: "3",
39011
+ "3rd": "3",
39012
+ fourth: "4",
39013
+ "4th": "4",
39014
+ last: "-1"
39015
+ };
38790
39016
  var EVERY_UNIT_RE = /\bevery\s+(day|week|month|year)\b/i;
38791
39017
  var ADVERB_RE = /\b(daily|weekly|monthly|yearly|annually)\b/i;
38792
39018
  var RELATIVE_RE = /\b(today|tonight|tomorrow|tmrw|tmr|yesterday)\b/i;
@@ -38918,6 +39144,12 @@ function takeEstimate(scan, take) {
38918
39144
  return;
38919
39145
  }
38920
39146
  function takeRecurrence(scan, take) {
39147
+ const nth = scan.match(EVERY_NTH_WEEKDAY_RE);
39148
+ if (nth && take(nth, "recurrence")) {
39149
+ const ordinal = NTH[(nth[1] ?? "").toLowerCase()] ?? "1";
39150
+ const byDay = `${ordinal}${weekdayByName(nth[2]).code}`;
39151
+ return { mode: "rigid", freq: "monthly", interval: 1, byDay: [byDay] };
39152
+ }
38921
39153
  const everyN = scan.match(EVERY_N_RE);
38922
39154
  if (everyN && take(everyN, "recurrence")) {
38923
39155
  const [, nStr = "1", unitRaw = "day"] = everyN;
@@ -39053,8 +39285,28 @@ function firstOccurrence(rule, now) {
39053
39285
  });
39054
39286
  return addDays(today, Math.min(...deltas));
39055
39287
  }
39288
+ const nth = rule.freq === "monthly" ? rule.byDay?.map(parseByDay) : undefined;
39289
+ if (nth?.length) {
39290
+ const days = [0, 1].flatMap((ahead) => nth.flatMap((p) => {
39291
+ if (!p)
39292
+ return [];
39293
+ const dow = WEEKDAYS.find((w) => w.code === p.weekday)?.dow ?? 0;
39294
+ const day = nthWeekdayOfMonth(today.getFullYear(), today.getMonth() + ahead, dow, p.ordinal ?? 1);
39295
+ return day >= today ? [day] : [];
39296
+ }));
39297
+ if (days.length > 0)
39298
+ return new Date(Math.min(...days.map((d) => d.getTime())));
39299
+ }
39056
39300
  return today;
39057
39301
  }
39302
+ function nthWeekdayOfMonth(year, month, dow, ordinal) {
39303
+ if (ordinal < 0) {
39304
+ const last = new Date(year, month + 1, 0);
39305
+ return addDays(last, -((last.getDay() - dow + 7) % 7));
39306
+ }
39307
+ const first = new Date(year, month, 1);
39308
+ return addDays(first, (dow - first.getDay() + 7) % 7 + (ordinal - 1) * 7);
39309
+ }
39058
39310
  function addMonths(d, months) {
39059
39311
  const next = new Date(d);
39060
39312
  next.setMonth(next.getMonth() + months);
@@ -39125,6 +39377,7 @@ var bookFacetSchema = object({
39125
39377
  coverSources: coverSourcesSchema.nullish(),
39126
39378
  pageCount: number2().int().min(1).max(1e5).nullish(),
39127
39379
  currentPage: number2().int().min(0).max(1e5).nullish(),
39380
+ chapter: string2().trim().min(1).max(60).nullish(),
39128
39381
  isbn: string2().regex(/^[0-9Xx][0-9Xx\s-]{8,20}$/, "must be an ISBN").nullish()
39129
39382
  }).strict();
39130
39383
  var filmFacetSchema = object({
@@ -40342,7 +40595,17 @@ function renderJournalImportScan(data, mode) {
40342
40595
  }
40343
40596
  out.push(` milestone candidates ${count((i) => Boolean(i.milestoneCandidate))}`);
40344
40597
  out.push(` health lines (left for Health) ${count((i) => Boolean(i.health))}`);
40345
- out.push("", d.dryRun ? paint(mode, "dim", "Dry run: nothing uploaded.") : `Staged ${d.staged ?? 0}${d.alreadyImported ? ` (${d.alreadyImported} already in the Journal, left as they are)` : ""}. Approve each month in the app: Journal \u203A Import.`);
40598
+ out.push("", d.dryRun ? paint(mode, "dim", "Dry run: nothing uploaded.") : `Staged ${d.staged ?? 0}${d.alreadyImported ? ` (${d.alreadyImported} already in the Journal, left as they are)` : ""}. Approve each month with do journal import approve <YYYY-MM>, or in the app: Journal \u203A Import.`);
40599
+ return out.join(`
40600
+ `);
40601
+ }
40602
+ function renderJournalImportApprove(data, mode) {
40603
+ const d = data;
40604
+ const batches = d.batches?.length ? paint(mode, "dim", ` (${d.batches.join(", ")})`) : "";
40605
+ const out = [`Imported ${d.imported ?? 0} days into the Journal${batches}`];
40606
+ if (d.open?.length) {
40607
+ out.push("", "Still asking for a rating, left staged (answer with --rating YYYY-MM-DD=N):", ...d.open.map((o) => ` ${o.day} \u201C${o.ratingQuestion}\u201D`));
40608
+ }
40346
40609
  return out.join(`
40347
40610
  `);
40348
40611
  }
@@ -41471,11 +41734,16 @@ var commands2 = [
41471
41734
  ],
41472
41735
  flags: [
41473
41736
  { name: "unit", type: "string", description: "For a weight: g (default) or kg." },
41737
+ {
41738
+ name: "note",
41739
+ type: "string",
41740
+ description: 'For a weight: a short note drawn under its date ("vet check done").'
41741
+ },
41474
41742
  { name: "at", type: "string", description: "When (ISO 8601); default now." }
41475
41743
  ],
41476
41744
  scopes: [...animalOperations.log.scopes],
41477
41745
  returns: "AnimalTimelineEntry",
41478
- notes: `${routeOf(animalOperations.log)} (\`animal_log\`). \`do animals log <hens> eggs 2\`, \`do animals log <dog> weight 37.8 --unit kg\`, \`do animals log <hen> note "eating again"\`. Eggs are the day's count: logging again that day replaces it.`
41746
+ notes: `${routeOf(animalOperations.log)} (\`animal_log\`). \`do animals log <hens> eggs 2\`, \`do animals log <dog> weight 37.8 --unit kg\`, \`do animals log <tortoise> weight 1409 --note "vet check done"\`, \`do animals log <hen> note "eating again"\`. Eggs are the day's count: logging again that day replaces it.`
41479
41747
  },
41480
41748
  run: async ({ args, flags, requireClient }) => {
41481
41749
  const id = requireArg(args, 0, "animal id");
@@ -41485,8 +41753,11 @@ var commands2 = [
41485
41753
  throw usageError("animals log needs a value (a number, or the note's text)");
41486
41754
  const at = flagString(flags, "at");
41487
41755
  const unit = flagString(flags, "unit");
41756
+ const note = flagString(flags, "note");
41757
+ if (note && kind !== "weight")
41758
+ throw usageError("--note goes with a weight");
41488
41759
  const n = Number(raw);
41489
- const fact = kind === "note" ? { kind, text: raw } : kind === "eggs" ? { kind, count: n } : kind === "weight" ? { kind, value: n, ...unit ? { unit } : {} } : { kind, value: n };
41760
+ const fact = kind === "note" ? { kind, text: raw } : kind === "eggs" ? { kind, count: n } : kind === "weight" ? { kind, value: n, ...unit ? { unit } : {}, ...note ? { note } : {} } : { kind, value: n };
41490
41761
  if (kind !== "note" && Number.isNaN(n))
41491
41762
  throw usageError("the value must be a number");
41492
41763
  const body = parseInput(animalLogInputSchema, { ...fact, ...at ? { occurredAt: at } : {} });
@@ -42414,6 +42685,20 @@ var commands7 = [
42414
42685
  ];
42415
42686
 
42416
42687
  // src/commands/collection.ts
42688
+ var CHAPTER_FLAG = {
42689
+ name: "chapter",
42690
+ type: "string",
42691
+ description: 'Where in the work: "9" (a chapter), "chapter 9", "book 9 and on".'
42692
+ };
42693
+ var LINKED_NOTE_FLAG = {
42694
+ name: "linked-note",
42695
+ type: "string",
42696
+ description: "Note ULID this Entry is tied to."
42697
+ };
42698
+ var whereFlags = (flags) => ({
42699
+ ...flagString(flags, "chapter") ? { chapter: flagString(flags, "chapter") } : {},
42700
+ ...flagString(flags, "linked-note") ? { linkedNoteId: flagString(flags, "linked-note") } : {}
42701
+ });
42417
42702
  var commands8 = [
42418
42703
  {
42419
42704
  spec: {
@@ -42537,6 +42822,53 @@ var commands8 = [
42537
42822
  };
42538
42823
  }
42539
42824
  },
42825
+ {
42826
+ spec: {
42827
+ path: ["collection", "reading"],
42828
+ summary: "Say where you are in a book: its chapter and/or page.",
42829
+ args: [{ name: "id", required: true, description: "Item ULID (a book)." }],
42830
+ flags: [
42831
+ {
42832
+ name: "chapter",
42833
+ type: "string",
42834
+ description: 'Where you are: "9" (a chapter), "chapter 9", "book 9".'
42835
+ },
42836
+ { name: "page", type: "string", description: "The page you're on." },
42837
+ { name: "clear-chapter", type: "boolean", description: "Forget the chapter." }
42838
+ ],
42839
+ scopes: [...collectionOperations.update.scopes],
42840
+ returns: "CollectionItemView",
42841
+ notes: `Reads the book's facet and patches it back with the new place (${routeOf(collectionOperations.update)}). A first page moves a book you want to doing (Progress); a chapter alone moves nothing.`
42842
+ },
42843
+ run: async ({ args, flags, requireClient }) => {
42844
+ const id = requireArg(args, 0, "item id");
42845
+ const chapter = flagString(flags, "chapter");
42846
+ const clear = flagBool(flags, "clear-chapter");
42847
+ const page = flagString(flags, "page");
42848
+ if (page !== undefined && !/^\d+$/.test(page)) {
42849
+ throw usageError("--page must be a whole number, e.g. 142");
42850
+ }
42851
+ if (chapter === undefined && page === undefined && !clear) {
42852
+ throw usageError('collection reading needs --chapter or --page, e.g. --chapter "9"');
42853
+ }
42854
+ const client = requireClient();
42855
+ const item = await client.get(pathOf(collectionOperations.get, { id }));
42856
+ const { chapter: kept, ...rest } = item.facet ?? {};
42857
+ const facet = {
42858
+ ...rest,
42859
+ ...clear || kept === undefined ? {} : { chapter: kept }
42860
+ };
42861
+ if (chapter !== undefined)
42862
+ facet.chapter = chapter;
42863
+ if (page !== undefined)
42864
+ facet.currentPage = Number(page);
42865
+ const body = parseInput(updateCollectionItemInputSchema, { facet });
42866
+ return {
42867
+ data: await client.patch(pathOf(collectionOperations.update, { id }), body),
42868
+ render: renderRecord
42869
+ };
42870
+ }
42871
+ },
42540
42872
  {
42541
42873
  spec: {
42542
42874
  path: ["collection", "add-entry"],
@@ -42556,7 +42888,9 @@ var commands8 = [
42556
42888
  description: "What happened: use (default) \xB7 service \xB7 repair \xB7 fault \xB7 lent \xB7 returned \xB7 valued \xB7 moved."
42557
42889
  },
42558
42890
  { name: "cost", type: "string", description: "What it cost (a sweep, a repair)." },
42559
- { name: "contact", type: "string", description: "Contact ULID of who did it." }
42891
+ { name: "contact", type: "string", description: "Contact ULID of who did it." },
42892
+ CHAPTER_FLAG,
42893
+ LINKED_NOTE_FLAG
42560
42894
  ],
42561
42895
  scopes: [...collectionOperations.addEntry.scopes],
42562
42896
  returns: "CollectionItemDetail"
@@ -42578,7 +42912,8 @@ var commands8 = [
42578
42912
  ...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
42579
42913
  ...flagString(flags, "kind") ? { kind: flagString(flags, "kind") } : {},
42580
42914
  ...cost !== undefined ? { cost: Number(cost) } : {},
42581
- ...flagString(flags, "contact") ? { contactId: flagString(flags, "contact") } : {}
42915
+ ...flagString(flags, "contact") ? { contactId: flagString(flags, "contact") } : {},
42916
+ ...whereFlags(flags)
42582
42917
  });
42583
42918
  return {
42584
42919
  data: await requireClient().post(pathOf(collectionOperations.addEntry, { id }), body),
@@ -42667,7 +43002,8 @@ var commands8 = [
42667
43002
  name: "on",
42668
43003
  type: "string",
42669
43004
  description: "Day it occurred (YYYY-MM-DD); defaults to today."
42670
- }
43005
+ },
43006
+ CHAPTER_FLAG
42671
43007
  ],
42672
43008
  scopes: [...collectionOperations.log.scopes],
42673
43009
  returns: "CollectionLogResult",
@@ -42688,7 +43024,8 @@ var commands8 = [
42688
43024
  ...flagString(flags, "status") ? { status: flagString(flags, "status") } : {},
42689
43025
  ...rating !== undefined ? { rating: Number(rating) } : {},
42690
43026
  ...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
42691
- ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {}
43027
+ ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {},
43028
+ ...flagString(flags, "chapter") ? { chapter: flagString(flags, "chapter") } : {}
42692
43029
  });
42693
43030
  return {
42694
43031
  data: await requireClient().post(pathOf(collectionOperations.log), body),
@@ -42699,7 +43036,7 @@ var commands8 = [
42699
43036
  {
42700
43037
  spec: {
42701
43038
  path: ["collection", "edit-entry"],
42702
- summary: "Fix an Entry's day, rating or note.",
43039
+ summary: "Fix an Entry's day, rating, note, chapter or linked note.",
42703
43040
  args: [
42704
43041
  { name: "id", required: true, description: "Item ULID." },
42705
43042
  { name: "entryId", required: true, description: "Entry ULID." }
@@ -42707,7 +43044,9 @@ var commands8 = [
42707
43044
  flags: [
42708
43045
  { name: "rating", type: "string", description: "New rating 0.5\u20135." },
42709
43046
  { name: "note", type: "string", description: "New note." },
42710
- { name: "on", type: "string", description: "New day (YYYY-MM-DD)." }
43047
+ { name: "on", type: "string", description: "New day (YYYY-MM-DD)." },
43048
+ CHAPTER_FLAG,
43049
+ LINKED_NOTE_FLAG
42711
43050
  ],
42712
43051
  scopes: [...collectionOperations.editEntry.scopes],
42713
43052
  returns: "CollectionItemDetail"
@@ -42722,7 +43061,8 @@ var commands8 = [
42722
43061
  const body = parseInput(updateCollectionEntryInputSchema, {
42723
43062
  ...rating !== undefined ? { rating: Number(rating) } : {},
42724
43063
  ...flagString(flags, "note") ? { note: flagString(flags, "note") } : {},
42725
- ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {}
43064
+ ...flagString(flags, "on") ? { occurredOn: flagString(flags, "on") } : {},
43065
+ ...whereFlags(flags)
42726
43066
  });
42727
43067
  return {
42728
43068
  data: await requireClient().patch(pathOf(collectionOperations.editEntry, { id, entryId }), body),
@@ -46791,7 +47131,8 @@ import { createHash } from "crypto";
46791
47131
  import { readdirSync, readFileSync as readFileSync7 } from "fs";
46792
47132
  import { join as join2 } from "path";
46793
47133
  var DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.md$/;
46794
- var RATING = /(?:[A-Za-z]+\s*\()?(~)?\s*(\d{1,2}(?:\.\d)?)(?:\s*[-\u2013]\s*(\d{1,2}))?\s*\/\s*10\)?/;
47134
+ var SCORE = "(?:10|[1-9])";
47135
+ var RATING = new RegExp(`(?:\\p{L}+\\s*\\()?(?<![\\p{L}\\p{N}_./~-])(~)?\\s*(${SCORE}(?:\\.\\d)?)(?:\\s*[-\u2013]\\s*(${SCORE}))?\\s*/\\s*10(?![\\p{L}\\p{N}_/-]|[.,]\\d)\\)?`, "u");
46795
47136
  var HEALTH = /\b(symptoms?|stomach|nausea|headaches?|migraine|doctor|GP|medication|meds|pills?|blood|fever|pain|clinic|hospital)\b/i;
46796
47137
  var NOISE = /\b(PR monitor|GitHub daily|deploy(?:ment)? watch\w*|cron|heartbeat|sales prospects?)\b/i;
46797
47138
  var MILESTONE = /\b(first|went live|is live|win:|moved in|arrived|wedding|engaged)\b/i;
@@ -46860,8 +47201,8 @@ function parseDailyNote(path, day, text) {
46860
47201
  const ratingLine = all.find((l) => /rating/i.test(l) && RATING.test(l)) ?? all.find((l) => RATING.test(l));
46861
47202
  const match = ratingLine ? RATING.exec(ratingLine) : null;
46862
47203
  if (match) {
46863
- const [whole, approx, low, high] = match;
46864
- if (approx || high)
47204
+ const [whole, approx, low = "", high] = match;
47205
+ if (approx || high || low.includes("."))
46865
47206
  ratingQuestion = whole.trim();
46866
47207
  else
46867
47208
  rating = Number(low);
@@ -46883,7 +47224,7 @@ function parseDailyNote(path, day, text) {
46883
47224
  inCheck = false;
46884
47225
  if (line === ratingLine || /how was (the|your) day/i.test(line))
46885
47226
  continue;
46886
- if (HEALTH.test(line) || NOISE.test(line))
47227
+ if (HEALTH.test(line) || NOISE.test(line) || isPrivateLine(line))
46887
47228
  continue;
46888
47229
  kept.push(line);
46889
47230
  }
@@ -46893,8 +47234,13 @@ function parseDailyNote(path, day, text) {
46893
47234
  `).trim() || null;
46894
47235
  const firstProse = (body ?? "").split(/\n\s*\n/).map((p) => plain(p)).find((p) => p.length > 0);
46895
47236
  title = firstProse ? firstSentence(firstProse.replace(/^The day\s*/i, "")) : null;
47237
+ } else {
47238
+ body = all.filter((l) => !/^#\s/.test(l) && l !== ratingLine && !/how was (the|your) day/i.test(l) && !HEALTH.test(l) && !NOISE.test(l) && !isPrivateLine(l)).join(`
47239
+ `).replace(/\n{3,}/g, `
47240
+
47241
+ `).trim() || null;
46896
47242
  }
46897
- const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && plain(l).length > 0);
47243
+ const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && !isPrivateLine(l) && plain(l).length > 0);
46898
47244
  const highlight = milestoneLine ? firstSentence(plain(milestoneLine), 80) : null;
46899
47245
  const sleep = sleepOf(all);
46900
47246
  const prs = (text.match(PR_REF) ?? []).length;
@@ -47275,7 +47621,7 @@ var commands24 = [
47275
47621
  description: "How the days name their folder (default memory/daily)."
47276
47622
  }
47277
47623
  ],
47278
- notes: ", 400 days a request. The day's '## Day summary' becomes its entry; a clear 'n/10' rating is kept, '~5/10' or '3-4/10' becomes a question; health and automation lines never reach the text. Nothing is written into the Journal until the member approves the month (the app only; a key can't approve). Re-running replaces what's still waiting.",
47624
+ notes: ", 400 days a request. The day's '## Day summary' becomes its entry; a clear 'n/10' rating (n 1 to 10, standing on its own) is kept, '~5/10' or '3-4/10' becomes a question; health and automation lines never reach the text, and lines about drinking, smoking, drugs, relapse or fertility never reach the title, text or highlight (the API strips them too). A day without a summary gets its kept lines as its text. Nothing is written into the Journal until a month is approved (`do journal import approve`, or the member in the app). Re-running replaces what's still waiting.",
47279
47625
  run: async ({ args, flags, requireClient }) => {
47280
47626
  const dir = requireArg(args, 0, "dir");
47281
47627
  const items = scanDailyNotes(dir, flagString(flags, "label") ?? "memory/daily");
@@ -47300,6 +47646,42 @@ var commands24 = [
47300
47646
  };
47301
47647
  }
47302
47648
  }),
47649
+ defineCommand({
47650
+ operation: journalOperations.approveImport,
47651
+ path: ["journal", "import", "approve"],
47652
+ summary: "Approve staged days into the Journal: one month's batch, or --all; --rating answers a fuzzy rating.",
47653
+ args: [{ name: "batch", required: false, description: "The month to approve, YYYY-MM." }],
47654
+ flags: [
47655
+ { name: "all", type: "boolean", description: "Approve every staged batch." },
47656
+ {
47657
+ name: "rating",
47658
+ type: "string",
47659
+ description: "Answer a day's rating question, YYYY-MM-DD=N (1 to 10); repeat per day."
47660
+ }
47661
+ ],
47662
+ notes: ". A day whose rating question has no --rating stays staged and is listed as open; the rest are written as imports. An agent's approve shows in the member's change list with Undo. Lines about drinking, smoking, drugs, relapse or fertility are never written.",
47663
+ run: async ({ args, flags, requireClient }) => {
47664
+ const batch = args[0];
47665
+ const all = flagBool(flags, "all");
47666
+ if (batch === undefined === !all) {
47667
+ throw usageError("approve one month (YYYY-MM) or --all");
47668
+ }
47669
+ const ratings = flagStrings(flags, "rating").map((answer) => {
47670
+ const m = /^(\d{4}-\d{2}-\d{2})=(\d+(?:\.\d+)?)$/.exec(answer.trim());
47671
+ if (!m)
47672
+ throw usageError(`--rating takes YYYY-MM-DD=N, got "${answer}"`);
47673
+ return { day: m[1], rating: Number(m[2]) };
47674
+ });
47675
+ const input = parseInput(journalImportApproveInputSchema, {
47676
+ ...all ? { all: true } : { batch },
47677
+ ratings
47678
+ });
47679
+ return {
47680
+ data: await requireClient().post(pathOf(journalOperations.approveImport), input),
47681
+ render: renderJournalImportApprove
47682
+ };
47683
+ }
47684
+ }),
47303
47685
  defineCommand({
47304
47686
  operation: journalOperations.importOverview,
47305
47687
  path: ["journal", "import", "status"],
@@ -48186,6 +48568,47 @@ var commands30 = [
48186
48568
  // src/commands/money.ts
48187
48569
  import { readFileSync as readFileSync8 } from "fs";
48188
48570
  import { basename as basename5 } from "path";
48571
+
48572
+ // ../../packages/domain/src/money.ts
48573
+ var toNumber = (cents) => Number(cents) / 100;
48574
+ function formatMoney(amount, {
48575
+ currency = "EUR",
48576
+ signed = false,
48577
+ whole = false,
48578
+ cents: always = false,
48579
+ locale
48580
+ } = {}) {
48581
+ const exact = typeof amount === "bigint" ? amount : amountToCents(amount);
48582
+ const cents = whole ? roundToEuro(exact) : exact;
48583
+ const abs = cents < 0n ? -cents : cents;
48584
+ const text = new Intl.NumberFormat(locale ?? "en-IE", {
48585
+ style: "currency",
48586
+ currency,
48587
+ minimumFractionDigits: always || abs % 100n !== 0n ? 2 : 0,
48588
+ maximumFractionDigits: 2
48589
+ }).format(toNumber(abs));
48590
+ if (cents < 0n)
48591
+ return `\u2212${text}`;
48592
+ return signed && cents > 0n ? `+${text}` : text;
48593
+ }
48594
+ function roundToEuro(cents) {
48595
+ const abs = cents < 0n ? -cents : cents;
48596
+ const rounded = (abs + 50n) / 100n * 100n;
48597
+ return cents < 0n ? -rounded : rounded;
48598
+ }
48599
+ function goalSavedSource(goal) {
48600
+ const account = goal.accountTitle ?? "the account";
48601
+ if (goal.savedFrom === "account")
48602
+ return `all of ${account}`;
48603
+ if (goal.savedFrom === "account_share")
48604
+ return `its share of ${account}`;
48605
+ if (goal.accountTitle && goal.accountBalanceKnown === false) {
48606
+ return `counted toward it; the balance of ${goal.accountTitle} is unknown`;
48607
+ }
48608
+ return "counted toward it";
48609
+ }
48610
+
48611
+ // src/commands/money.ts
48189
48612
  function renderDedupe(data) {
48190
48613
  const r = data;
48191
48614
  const lines = r.pairs.map((p) => `${p.bookedOn} ${p.amount.padStart(10)} ${p.description ?? p.counterpartyName ?? ""} keep ${p.keepId} twin ${p.twinId} (${p.twinBookedOn})${p.ambiguous ? " ambiguous: sort by hand" : ""}`);
@@ -48194,6 +48617,38 @@ function renderDedupe(data) {
48194
48617
  return lines.join(`
48195
48618
  `);
48196
48619
  }
48620
+ function renderGoals(data) {
48621
+ const goals = Array.isArray(data) ? data : [];
48622
+ if (goals.length === 0)
48623
+ return "(no goals yet)";
48624
+ return goals.map((g) => {
48625
+ const whole = (a) => formatMoney(a, { whole: true });
48626
+ const parts = [
48627
+ `${whole(g.saved)} of ${whole(g.targetAmount)} (${goalSavedSource(g)})`,
48628
+ Number(g.remaining) > 0 ? `${whole(g.remaining)} to go` : "reached",
48629
+ g.monthlyNeed && g.targetOn ? `${formatMoney(g.monthlyNeed)} a month by ${g.targetOn}` : null
48630
+ ];
48631
+ return `${g.title} ${parts.filter(Boolean).join(" \xB7 ")} ${g.id}`;
48632
+ }).join(`
48633
+ `);
48634
+ }
48635
+ function syncLine(r) {
48636
+ const when = r.startedAt ? `${String(r.startedAt).slice(0, 16).replace("T", " ")} ` : "";
48637
+ const what = r.accountTitle ? `${r.accountTitle} ` : "";
48638
+ const counts = `fetched ${r.fetched ?? 0} \xB7 imported ${r.imported ?? 0} \xB7 matched ${r.matched ?? 0} \xB7 duplicates ${r.duplicates ?? 0}`;
48639
+ return `${when}${what}${r.outcome ?? "?"} ${counts}${r.error ? `
48640
+ ${r.error}` : ""}`;
48641
+ }
48642
+ function renderSyncLog(data) {
48643
+ const rows = Array.isArray(data) ? data : data.data ?? [];
48644
+ return rows.length === 0 ? "(no syncs yet)" : rows.map(syncLine).join(`
48645
+ `);
48646
+ }
48647
+ function renderSyncResults(data) {
48648
+ const { results } = data;
48649
+ return results.length === 0 ? "(no live bank links)" : results.map(syncLine).join(`
48650
+ `);
48651
+ }
48197
48652
  function partyFlag(flags) {
48198
48653
  const userId = flagString(flags, "with-user");
48199
48654
  const contactId = flagString(flags, "with-contact");
@@ -48330,7 +48785,7 @@ var commands31 = [
48330
48785
  });
48331
48786
  return {
48332
48787
  data: await requireClient().post(pathOf(moneyOperations.syncConnections), body),
48333
- render: renderRecord
48788
+ render: renderSyncResults
48334
48789
  };
48335
48790
  }
48336
48791
  }),
@@ -48367,7 +48822,7 @@ var commands31 = [
48367
48822
  args: [{ name: "id", required: true, description: "Bank link (connection) ULID." }],
48368
48823
  flags: [],
48369
48824
  returns: "MoneyConnectionSync[]",
48370
- run: readOperation(moneyOperations.listConnectionSyncs, renderList, { id: "id" })
48825
+ run: readOperation(moneyOperations.listConnectionSyncs, renderSyncLog, { id: "id" })
48371
48826
  }),
48372
48827
  defineCommand({
48373
48828
  operation: moneyOperations.listCategories,
@@ -49115,7 +49570,8 @@ var commands31 = [
49115
49570
  args: [],
49116
49571
  flags: [],
49117
49572
  returns: "MoneyGoalView[]",
49118
- run: readOperation(moneyOperations.listGoals, renderList)
49573
+ notes: ". `saved` reads `savedFrom`: `account` (the only goal in its account: the balance), `account_share` (several share one: each its own, capped by what's left, soonest target first) or `contributions` (savedBefore plus what's counted toward it; also when the account's balance is unknown, `accountBalanceKnown: false`). `remaining` is the target less saved; `monthlyNeed` spreads it over the months to `targetOn`.",
49574
+ run: readOperation(moneyOperations.listGoals, renderGoals)
49119
49575
  }),
49120
49576
  defineCommand({
49121
49577
  operation: moneyOperations.getGoal,
@@ -52147,7 +52603,7 @@ var commands49 = [
52147
52603
  "#tag": "Adds a tag. Repeatable: '#home #urgent'.",
52148
52604
  "!": "Stars the task \u2014 the 1\u20132 things that matter (aliases: !star/!s/!flag/!f). Drives Today's focus and the starred filter.",
52149
52605
  "~area": "Files it into an existing Area by its title, case-insensitive: '~garden' for 'Garden'. One word (letters, digits, - and _), so an Area named with spaces can't be named here \u2014 file it after with `do tasks update <id> --data '{\"areaId\":\"<id>\"}'`. An unknown name files nothing (never creates an Area); the first ~area wins. Valid names: `do areas list --json` (their `title`).",
52150
- when: "The planned day and time: 'today', 'tomorrow 3pm', 'fri', 'next monday', 'in 3 days', 'at 16:00', 'tonight' (18:00); the past for back-filling: 'yesterday', 'last friday'. A time makes it a timed task (allDay false). 'every monday' / 'every 2 weeks' / 'daily' attaches a recurrence.",
52606
+ when: "The planned day and time: 'today', 'tomorrow 3pm', 'fri', 'next monday', 'in 3 days', 'at 16:00', 'tonight' (18:00); the past for back-filling: 'yesterday', 'last friday'. A time makes it a timed task (allDay false). 'every monday' / 'every 2 weeks' / 'daily' / 'every first sunday' / 'every last friday' attaches a recurrence.",
52151
52607
  "by / due": "The Deadline, never the plan: 'by friday', 'due fri', 'due tomorrow 5pm', 'by 17:00' (today). 'by' with no date after it ('pay by card') stays in the title.",
52152
52608
  estimate: "How long it takes (durationMinutes): '30m', '45 min', '1h', '1.5h', '1h30', '2 hours', 'for 20 mins'. Not 'in 2 hours'."
52153
52609
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.333.0",
3
+ "version": "1.334.0",
4
4
  "private": false,
5
5
  "description": "do — the agent-drivable CLI over the public /api/v1 surface",
6
6
  "homepage": "https://github.com/robbeverhelst/do/tree/main/apps/cli",