@robbeverhelst/do 1.331.0 → 1.333.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 +698 -73
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -14410,7 +14410,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14410
14410
 
14411
14411
  // src/meta.ts
14412
14412
  var CLI_NAME = "do";
14413
- var VERSION = "1.331.0";
14413
+ var VERSION = "1.333.0";
14414
14414
  var API_PREFIX = "/api/v1";
14415
14415
  var DEFAULT_API_URL = "http://localhost:3001";
14416
14416
 
@@ -17969,7 +17969,8 @@ var nodeGrants = pgTable("node_grants", {
17969
17969
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
17970
17970
  }, (t) => [
17971
17971
  unique("node_grants_node_subject_uniq").on(t.tenantId, t.nodeId, t.subjectType, t.subjectId),
17972
- index("node_grants_tenant_node_idx").on(t.tenantId, t.nodeId)
17972
+ index("node_grants_tenant_node_idx").on(t.tenantId, t.nodeId),
17973
+ index("node_grants_node_idx").on(t.nodeId)
17973
17974
  ]);
17974
17975
  var shareLinks = pgTable("share_links", {
17975
17976
  id: text("id").primaryKey(),
@@ -17982,7 +17983,10 @@ var shareLinks = pgTable("share_links", {
17982
17983
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
17983
17984
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
17984
17985
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
17985
- }, (t) => [index("share_links_tenant_node_idx").on(t.tenantId, t.nodeId)]);
17986
+ }, (t) => [
17987
+ index("share_links_tenant_node_idx").on(t.tenantId, t.nodeId),
17988
+ index("share_links_node_idx").on(t.nodeId)
17989
+ ]);
17986
17990
  var shareLinkGuests = pgTable("share_link_guests", {
17987
17991
  id: text("id").primaryKey(),
17988
17992
  tenantId: text("tenant_id").notNull().references(() => tenants.id),
@@ -18006,7 +18010,8 @@ var comments = pgTable("comments", {
18006
18010
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
18007
18011
  }, (t) => [
18008
18012
  index("comments_tenant_node_created_idx").on(t.tenantId, t.nodeId, t.createdAt),
18009
- index("comments_parent_idx").on(t.parentId)
18013
+ index("comments_parent_idx").on(t.parentId),
18014
+ index("comments_node_idx").on(t.nodeId)
18010
18015
  ]);
18011
18016
  var commentMentions = pgTable("comment_mentions", {
18012
18017
  id: text("id").primaryKey(),
@@ -18212,7 +18217,8 @@ var occurrences = pgTable("occurrences", {
18212
18217
  unique("occurrences_template_date_uniq").on(t.templateId, t.occurrenceDate),
18213
18218
  index("occurrences_status_date_idx").on(t.status, t.occurrenceDate),
18214
18219
  index("occurrences_tenant_date_idx").on(t.tenantId, t.occurrenceDate),
18215
- index("occurrences_rule_idx").on(t.ruleId)
18220
+ index("occurrences_rule_idx").on(t.ruleId),
18221
+ index("occurrences_tenant_done_completed_idx").on(t.tenantId, t.completedAt).where(sql`${t.status} = 'done'`)
18216
18222
  ]);
18217
18223
 
18218
18224
  // ../../packages/schema/src/db/core.ts
@@ -18349,6 +18355,7 @@ var userSettings = pgTable("user_settings", {
18349
18355
  foodDiaryMode: boolean4("food_diary_mode").notNull().default(false),
18350
18356
  calendarZoom: integer2("calendar_zoom"),
18351
18357
  lifeBlockSharing: jsonb("life_block_sharing").$type(),
18358
+ recentSearches: jsonb("recent_searches").$type().notNull().default([]),
18352
18359
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18353
18360
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
18354
18361
  }, (t) => [unique("user_settings_tenant_user_uniq").on(t.tenantId, t.userId)]);
@@ -19806,6 +19813,39 @@ var agentOperations = defineOperations({
19806
19813
  }
19807
19814
  });
19808
19815
 
19816
+ // ../../packages/schema/src/db/files.ts
19817
+ var bytea = customType({
19818
+ dataType() {
19819
+ return "bytea";
19820
+ }
19821
+ });
19822
+ var files = pgTable("files", {
19823
+ id: text("id").primaryKey(),
19824
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
19825
+ ownerNodeId: text("owner_node_id").references(() => nodes.id, { onDelete: "cascade" }),
19826
+ uploadedByUserId: text("uploaded_by_user_id").references(() => users.id, {
19827
+ onDelete: "set null"
19828
+ }),
19829
+ uploadedByAgentId: text("uploaded_by_agent_id").references(() => agents.id),
19830
+ kind: text("kind").notNull(),
19831
+ name: text("name").notNull(),
19832
+ mime: text("mime").notNull(),
19833
+ size: integer2("size").notNull(),
19834
+ sha256: text("sha256").notNull(),
19835
+ data: bytea("data").notNull(),
19836
+ enc: text("enc"),
19837
+ textExtract: text("text_extract"),
19838
+ pages: integer2("pages"),
19839
+ source: text("source").notNull().default("upload"),
19840
+ receivedAt: timestamp("received_at", { withTimezone: true }),
19841
+ durationMs: integer2("duration_ms"),
19842
+ transcript: text("transcript"),
19843
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
19844
+ }, (t) => [
19845
+ index("files_tenant_idx").on(t.tenantId, t.createdAt),
19846
+ index("files_owner_idx").on(t.ownerNodeId, t.createdAt)
19847
+ ]);
19848
+
19809
19849
  // ../../packages/schema/src/db/periods.ts
19810
19850
  var periodKind = pgEnum("period_kind", [
19811
19851
  "hibernation",
@@ -19916,9 +19956,29 @@ var animalLostCards = pgTable("animal_lost_cards", {
19916
19956
  phone: text("phone"),
19917
19957
  showChipped: boolean4("show_chipped").notNull().default(false),
19918
19958
  note: text("note"),
19959
+ description: text("description"),
19960
+ answersTo: text("answers_to"),
19961
+ missingSince: timestamp("missing_since", { withTimezone: true }),
19962
+ missingWhere: text("missing_where"),
19919
19963
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19920
19964
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
19921
19965
  }, (t) => [index("animal_lost_cards_tenant_idx").on(t.tenantId)]);
19966
+ var animalLostReportFrom = pgEnum("animal_lost_report_from", ["owner", "finder"]);
19967
+ var animalLostReports = pgTable("animal_lost_reports", {
19968
+ id: text("id").primaryKey(),
19969
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
19970
+ animalId: text("animal_id").notNull().references(() => animals.id, { onDelete: "cascade" }),
19971
+ from: animalLostReportFrom("from").notNull(),
19972
+ at: timestamp("at", { withTimezone: true }).notNull(),
19973
+ title: text("title"),
19974
+ note: text("note"),
19975
+ place: text("place"),
19976
+ lat: doublePrecision("lat"),
19977
+ lng: doublePrecision("lng"),
19978
+ phone: text("phone"),
19979
+ photoFileId: text("photo_file_id"),
19980
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
19981
+ }, (t) => [index("animal_lost_reports_animal_idx").on(t.tenantId, t.animalId, t.at)]);
19922
19982
  var animalHandoverLinks = pgTable("animal_handover_links", {
19923
19983
  id: text("id").primaryKey(),
19924
19984
  tenantId: text("tenant_id").notNull().references(() => tenants.id),
@@ -19931,6 +19991,25 @@ var animalHandoverLinks = pgTable("animal_handover_links", {
19931
19991
  createdByUserId: text("created_by_user_id").references(() => users.id),
19932
19992
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
19933
19993
  }, (t) => [index("animal_handover_links_period_idx").on(t.tenantId, t.periodId)]);
19994
+ var animalHandoverTicks = pgTable("animal_handover_ticks", {
19995
+ id: text("id").primaryKey(),
19996
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
19997
+ periodId: text("period_id").notNull().references(() => periods.id, { onDelete: "cascade" }),
19998
+ linkId: text("link_id").notNull().references(() => animalHandoverLinks.id, { onDelete: "cascade" }),
19999
+ habitId: text("habit_id").notNull().references(() => habits.id),
20000
+ habitEntryId: text("habit_entry_id").notNull().unique().references(() => habitEntries.id, { onDelete: "cascade" }),
20001
+ day: date5("day", { mode: "string" }).notNull(),
20002
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
20003
+ }, (t) => [unique("animal_handover_ticks_habit_day_uniq").on(t.periodId, t.habitId, t.day)]);
20004
+ var animalHandoverNotes = pgTable("animal_handover_notes", {
20005
+ id: text("id").primaryKey(),
20006
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
20007
+ periodId: text("period_id").notNull().references(() => periods.id, { onDelete: "cascade" }),
20008
+ linkId: text("link_id").notNull().references(() => animalHandoverLinks.id, { onDelete: "cascade" }),
20009
+ body: text("body").notNull(),
20010
+ photoFileId: text("photo_file_id").references(() => files.id, { onDelete: "set null" }),
20011
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
20012
+ }, (t) => [index("animal_handover_notes_period_idx").on(t.tenantId, t.periodId)]);
19934
20013
 
19935
20014
  // ../../packages/schema/src/db/documents.ts
19936
20015
  var documentKind = pgEnum("document_kind", [
@@ -20590,12 +20669,23 @@ var animalHolidayModeSchema = object({
20590
20669
  dailyPhoto: boolean2(),
20591
20670
  feedReorder: boolean2()
20592
20671
  });
20672
+ var animalHandoverWhereSchema = object({
20673
+ label: string2().trim().min(1).max(40),
20674
+ value: string2().trim().min(1).max(200)
20675
+ });
20676
+ var animalHandoverDayNoteSchema = object({
20677
+ day: string2().regex(/^\d{4}-\d{2}-\d{2}$/, "A day, like 2026-12-26"),
20678
+ title: string2().trim().min(1).max(80),
20679
+ lines: array(string2().trim().min(1).max(120)).max(2).default([])
20680
+ });
20593
20681
  var animalHandoverRuleSchema = object({
20594
20682
  sitter: string2().trim().min(1).max(120).optional(),
20595
20683
  houseNote: string2().trim().min(1).max(300).optional(),
20596
20684
  holidayMode: animalHolidayModeSchema.partial().optional(),
20597
20685
  share: object({ tokenHash: string2(), until: string2() }).optional(),
20598
- agentNote: string2().trim().min(1).max(500).optional()
20686
+ agentNote: string2().trim().min(1).max(500).optional(),
20687
+ where: array(animalHandoverWhereSchema).max(12).optional(),
20688
+ dayNotes: array(animalHandoverDayNoteSchema).max(31).optional()
20599
20689
  }).passthrough();
20600
20690
  var animalHandoverQuerySchema = object({ periodId: ulidSchema.optional() });
20601
20691
  var animalHandoverTripSchema = object({
@@ -20650,22 +20740,122 @@ var animalHandoverShareSchema = object({
20650
20740
  path: string2(),
20651
20741
  until: string2()
20652
20742
  });
20743
+ var animalSitterItemSchema = object({
20744
+ habitId: ulidSchema,
20745
+ title: string2(),
20746
+ timeOfDay: string2().nullable(),
20747
+ how: string2().nullable(),
20748
+ done: boolean2(),
20749
+ bySitter: boolean2()
20750
+ });
20751
+ var animalSitterDaySchema = object({
20752
+ day: string2(),
20753
+ items: array(animalSitterItemSchema),
20754
+ notes: array(animalHandoverDayNoteSchema.omit({ day: true }))
20755
+ });
20756
+ var animalSitterContactSchema = object({
20757
+ id: ulidSchema,
20758
+ title: string2(),
20759
+ kind: _enum2(["vet", "member"]),
20760
+ label: string2().nullable(),
20761
+ masked: string2()
20762
+ });
20763
+ var animalSitterNoteSchema = object({
20764
+ id: ulidSchema,
20765
+ body: string2(),
20766
+ photos: number2().int(),
20767
+ sentAt: exports_coerce.date(),
20768
+ seenBy: string2().nullable()
20769
+ });
20770
+ var animalSitterViewSchema = object({
20771
+ today: string2(),
20772
+ trip: object({
20773
+ title: string2(),
20774
+ from: string2(),
20775
+ to: string2(),
20776
+ backAt: string2().nullable(),
20777
+ until: string2()
20778
+ }),
20779
+ owners: array(string2()),
20780
+ sharedBy: string2().nullable(),
20781
+ days: array(animalSitterDaySchema),
20782
+ animals: array(animalHandoverAnimalSchema),
20783
+ where: array(animalHandoverWhereSchema),
20784
+ contacts: array(animalSitterContactSchema),
20785
+ notes: array(animalSitterNoteSchema)
20786
+ });
20787
+ var animalSitterTickInputSchema = object({ habitId: ulidSchema, done: boolean2() }).strict();
20788
+ var animalSitterNoteInputSchema = object({
20789
+ id: ulidSchema,
20790
+ body: string2().trim().max(1000).default("")
20791
+ }).strict();
20792
+ var animalSitterPhoneSchema = object({ phone: string2() });
20793
+ var lostCardPhoneSchema = string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -");
20653
20794
  var animalLostCardInputSchema = object({
20654
20795
  photoFileId: ulidSchema.nullish(),
20655
- phone: string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -").nullish(),
20796
+ phone: lostCardPhoneSchema.nullish(),
20656
20797
  showChipped: boolean2().optional(),
20657
- note: string2().trim().min(1).max(200).nullish()
20798
+ note: string2().trim().min(1).max(200).nullish(),
20799
+ description: string2().trim().min(1).max(400).nullish(),
20800
+ answersTo: string2().trim().min(1).max(120).nullish(),
20801
+ missingSince: exports_coerce.date().nullish(),
20802
+ missingWhere: string2().trim().min(1).max(200).nullish()
20658
20803
  }).strict();
20659
20804
  var animalLostCardPhotoSchema = object({ id: ulidSchema, name: string2() });
20805
+ var animalLostUpdateSchema = object({
20806
+ id: ulidSchema,
20807
+ at: exports_coerce.date(),
20808
+ title: string2(),
20809
+ note: string2().nullable(),
20810
+ place: string2().nullable()
20811
+ });
20660
20812
  var animalLostCardPublicSchema = object({
20661
20813
  name: string2(),
20662
20814
  speciesKey: string2().nullable(),
20663
20815
  kind: string2().nullable(),
20664
20816
  missing: boolean2(),
20665
20817
  hasPhoto: boolean2(),
20666
- phone: string2().nullable(),
20818
+ phoneMasked: string2().nullable(),
20667
20819
  chipped: boolean2(),
20668
- note: string2().nullable()
20820
+ note: string2().nullable(),
20821
+ description: string2().nullable(),
20822
+ answersTo: string2().nullable(),
20823
+ missingSince: exports_coerce.date().nullable(),
20824
+ missingWhere: string2().nullable(),
20825
+ pronoun: _enum2(["he", "she", "they"]),
20826
+ sharedBy: string2().nullable(),
20827
+ owners: array(string2()),
20828
+ vet: string2().nullable(),
20829
+ updates: array(animalLostUpdateSchema)
20830
+ });
20831
+ var animalLostCardCallSchema = object({ phone: string2() });
20832
+ var blankless = (v) => typeof v === "string" && v.trim() === "" ? undefined : v;
20833
+ var animalLostFoundInputSchema = object({
20834
+ where: string2().trim().min(1).max(300),
20835
+ lat: preprocess(blankless, exports_coerce.number().min(-90).max(90).optional()),
20836
+ lng: preprocess(blankless, exports_coerce.number().min(-180).max(180).optional()),
20837
+ phone: preprocess(blankless, lostCardPhoneSchema.optional())
20838
+ }).strict().refine((v) => v.lat === undefined === (v.lng === undefined), {
20839
+ message: "Send both lat and lng, or neither."
20840
+ });
20841
+ var animalLostFoundSentSchema = object({ to: array(string2()) });
20842
+ var animalLostUpdateInputSchema = object({
20843
+ at: exports_coerce.date(),
20844
+ title: string2().trim().min(1).max(120),
20845
+ note: string2().trim().min(1).max(200).nullish(),
20846
+ place: string2().trim().min(1).max(200).nullish()
20847
+ }).strict();
20848
+ var animalLostReportSchema = object({
20849
+ id: ulidSchema,
20850
+ from: _enum2(["owner", "finder"]),
20851
+ at: exports_coerce.date(),
20852
+ title: string2().nullable(),
20853
+ note: string2().nullable(),
20854
+ place: string2().nullable(),
20855
+ lat: number2().nullable(),
20856
+ lng: number2().nullable(),
20857
+ phone: string2().nullable(),
20858
+ photoFileId: ulidSchema.nullable()
20669
20859
  });
20670
20860
  var animalLostCardStateSchema = object({
20671
20861
  card: object({
@@ -20674,7 +20864,12 @@ var animalLostCardStateSchema = object({
20674
20864
  phone: string2().nullable(),
20675
20865
  showChipped: boolean2(),
20676
20866
  note: string2().nullable(),
20867
+ description: string2().nullable(),
20868
+ answersTo: string2().nullable(),
20869
+ missingSince: exports_coerce.date().nullable(),
20870
+ missingWhere: string2().nullable(),
20677
20871
  since: exports_coerce.date(),
20872
+ reports: array(animalLostReportSchema),
20678
20873
  preview: animalLostCardPublicSchema
20679
20874
  }).nullable(),
20680
20875
  photos: array(animalLostCardPhotoSchema),
@@ -20925,7 +21120,7 @@ var animalOperations = defineOperations({
20925
21120
  scopes: READ,
20926
21121
  tag: "Animals",
20927
21122
  summary: "An animal's lost-pet card",
20928
- description: "`card` is null while it's off; else its `path` (`/lost/<token>`, the page its QR code opens, no account), what it shows (`photoFileId`, `phone`, `showChipped`, `note`) and `preview`, the finder's page. `photos` are the pictures on the animal it may show; `chipOnFile` whether it can say chipped.",
21123
+ description: "`card` is null while it's off; else its `path` (`/lost/<token>`, the page its QR code opens, no account), what it shows (`photoFileId`, `phone`, `showChipped`, `note`, `description`, `answersTo`, `missingSince`, `missingWhere`), `reports` (the household's updates and finders' reports, with a finder's number) and `preview`, the finder's page (its number masked). `photos` are the pictures on the animal it may show; `chipOnFile` whether it can say chipped.",
20929
21124
  status: 200,
20930
21125
  response: { description: "The card's state." },
20931
21126
  errors: { 404: true }
@@ -21617,6 +21812,8 @@ var taskOwnerSchema = actorRefSchema.extend({ desk: string2().nullable() });
21617
21812
  var taskFilingSuggestionSchema = object({
21618
21813
  areaId: ulidSchema.nullable(),
21619
21814
  area: string2().nullable(),
21815
+ projectId: ulidSchema.nullable().default(null),
21816
+ project: string2().nullable().default(null),
21620
21817
  owner: taskOwnerSchema.nullable(),
21621
21818
  scheduledAt: exports_coerce.date().nullable(),
21622
21819
  by: actorRefSchema
@@ -23027,6 +23224,8 @@ var moneyBankPendingSchema = object({
23027
23224
  pendingId: ulidSchema,
23028
23225
  bank: string2(),
23029
23226
  consentValidUntil: exports_coerce.date(),
23227
+ createdAt: exports_coerce.date(),
23228
+ expiresAt: exports_coerce.date(),
23030
23229
  accounts: array(object({
23031
23230
  index: number2().int().min(0),
23032
23231
  name: string2().nullable(),
@@ -23062,6 +23261,33 @@ var moneyBankSyncResultSchema = object({
23062
23261
  error: true
23063
23262
  }).extend({ accountTitle: string2() }))
23064
23263
  });
23264
+ var moneyDedupeInputSchema = object({ accountId: ulidSchema, commit: boolean2().optional() }).strict();
23265
+ var moneyDedupePairSchema = object({
23266
+ keepId: string2(),
23267
+ twinId: string2(),
23268
+ bookedOn: string2(),
23269
+ twinBookedOn: string2(),
23270
+ amount: string2(),
23271
+ description: string2().nullable(),
23272
+ counterpartyName: string2().nullable(),
23273
+ ambiguous: boolean2()
23274
+ });
23275
+ var moneyDedupeChangesSchema = object({
23276
+ merged: number2().int(),
23277
+ links: number2().int(),
23278
+ splits: number2().int(),
23279
+ settlements: number2().int(),
23280
+ transfers: number2().int(),
23281
+ rules: number2().int(),
23282
+ notes: number2().int()
23283
+ });
23284
+ var moneyDedupeResultSchema = object({
23285
+ accountId: string2(),
23286
+ committed: boolean2(),
23287
+ pairs: array(moneyDedupePairSchema),
23288
+ counts: object({ pairs: number2().int(), ambiguous: number2().int() }),
23289
+ changed: moneyDedupeChangesSchema
23290
+ });
23065
23291
 
23066
23292
  // ../../packages/schema/src/operations/bank-link.ts
23067
23293
  var CONNECT = ["connections:write"];
@@ -23144,6 +23370,18 @@ var bankLinkOperations = defineOperations({
23144
23370
  response: { description: "The bank accounts to map." },
23145
23371
  errors: { 400: true, 403: true, 409: true }
23146
23372
  },
23373
+ pendings: {
23374
+ method: "get",
23375
+ path: "/money/bank-link/pending",
23376
+ scopes: CONNECT,
23377
+ afterScopes: AFTER,
23378
+ tag: "Money",
23379
+ summary: "List the bank links waiting to be mapped",
23380
+ description: "Every authorisation the key's member brought back from a bank and hasn't mapped yet, newest first, while it lasts (a day): each as the pending answers it, with `createdAt` and `expiresAt`. For when the page that finished it is gone; map one with link.",
23381
+ status: 200,
23382
+ response: { description: "The bank links waiting to be mapped." },
23383
+ errors: { 403: true }
23384
+ },
23147
23385
  pending: {
23148
23386
  method: "get",
23149
23387
  path: "/money/bank-link/pending/{id}",
@@ -23580,7 +23818,8 @@ var calendarEventLinks = pgTable("calendar_event_links", {
23580
23818
  }, (t) => [
23581
23819
  unique("calendar_event_links_connection_node_uniq").on(t.connectionId, t.nodeId, t.purpose),
23582
23820
  unique("calendar_event_links_connection_event_uniq").on(t.connectionId, t.externalEventId),
23583
- index("calendar_event_links_tenant_connection_idx").on(t.tenantId, t.connectionId)
23821
+ index("calendar_event_links_tenant_connection_idx").on(t.tenantId, t.connectionId),
23822
+ index("calendar_event_links_node_idx").on(t.nodeId)
23584
23823
  ]);
23585
23824
  var eventResponseStatus = pgEnum("event_response_status", [
23586
23825
  "none",
@@ -24856,39 +25095,6 @@ var doccleSyncResultSchema = object({
24856
25095
  failure: doccleFailureSchema.nullable()
24857
25096
  });
24858
25097
 
24859
- // ../../packages/schema/src/db/files.ts
24860
- var bytea = customType({
24861
- dataType() {
24862
- return "bytea";
24863
- }
24864
- });
24865
- var files = pgTable("files", {
24866
- id: text("id").primaryKey(),
24867
- tenantId: text("tenant_id").notNull().references(() => tenants.id),
24868
- ownerNodeId: text("owner_node_id").references(() => nodes.id, { onDelete: "cascade" }),
24869
- uploadedByUserId: text("uploaded_by_user_id").references(() => users.id, {
24870
- onDelete: "set null"
24871
- }),
24872
- uploadedByAgentId: text("uploaded_by_agent_id").references(() => agents.id),
24873
- kind: text("kind").notNull(),
24874
- name: text("name").notNull(),
24875
- mime: text("mime").notNull(),
24876
- size: integer2("size").notNull(),
24877
- sha256: text("sha256").notNull(),
24878
- data: bytea("data").notNull(),
24879
- enc: text("enc"),
24880
- textExtract: text("text_extract"),
24881
- pages: integer2("pages"),
24882
- source: text("source").notNull().default("upload"),
24883
- receivedAt: timestamp("received_at", { withTimezone: true }),
24884
- durationMs: integer2("duration_ms"),
24885
- transcript: text("transcript"),
24886
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
24887
- }, (t) => [
24888
- index("files_tenant_idx").on(t.tenantId, t.createdAt),
24889
- index("files_owner_idx").on(t.ownerNodeId, t.createdAt)
24890
- ]);
24891
-
24892
25098
  // ../../packages/schema/src/contracts/files.ts
24893
25099
  var FILE_TEXT_MAX = 1e6;
24894
25100
  var FILE_SOURCES = [
@@ -24998,7 +25204,8 @@ var moneyAccountSchema = createSelectSchema(moneyAccounts, {
24998
25204
  });
24999
25205
  var moneyAccountViewSchema = moneyAccountSchema.omit({ ibanHash: true }).extend({
25000
25206
  title: string2(),
25001
- visibility: _enum2(NODE_VISIBILITY_VALUES)
25207
+ visibility: _enum2(NODE_VISIBILITY_VALUES),
25208
+ balanceKnown: boolean2()
25002
25209
  });
25003
25210
  var moneyCategorySchema = createSelectSchema(moneyCategories, {
25004
25211
  archivedAt: exports_coerce.date().nullable(),
@@ -25058,7 +25265,14 @@ var createMoneyAccountInputSchema = object({
25058
25265
  includeInNetWorth: boolean2().optional(),
25059
25266
  visibility: _enum2(NODE_VISIBILITY_VALUES).optional()
25060
25267
  }).strict();
25061
- var updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().strict();
25268
+ var moneyBalanceAtSchema = union([keyDateDaySchema, exports_iso.datetime({ offset: true })]);
25269
+ var updateMoneyAccountInputSchema = createMoneyAccountInputSchema.omit({ id: true, currency: true }).partial().extend({
25270
+ balance: moneyAmountSchema.optional(),
25271
+ balanceAt: moneyBalanceAtSchema.optional()
25272
+ }).strict().refine((u) => u.balance === undefined === (u.balanceAt === undefined), {
25273
+ message: "a balance and the day it's from go together",
25274
+ path: ["balanceAt"]
25275
+ });
25062
25276
  var createMoneyCategoryInputSchema = object({
25063
25277
  id: ulidSchema,
25064
25278
  name: string2().trim().min(1).max(60),
@@ -25091,6 +25305,7 @@ var sortMoneyTransactionInputSchema = object({
25091
25305
  taxTag: moneyTaxTagSchema.nullish(),
25092
25306
  orderItem: string2().trim().min(1).max(200).nullish()
25093
25307
  }).strict();
25308
+ var moneyTransactionLinkInputSchema = object({ nodeId: ulidSchema, kind: moneyLinkKindSchema.default("about") }).strict();
25094
25309
  var createMoneyRuleInputSchema = object({
25095
25310
  id: ulidSchema,
25096
25311
  name: string2().trim().min(1).max(120),
@@ -25152,8 +25367,13 @@ var moneyTransactionPageSchema = object({
25152
25367
  var MONEY_IMPORT_MAX_BYTES = 5 * 1024 * 1024;
25153
25368
  var moneyImportFieldsSchema = object({
25154
25369
  accountId: ulidSchema,
25155
- since: keyDateDaySchema.optional()
25156
- }).strict();
25370
+ since: keyDateDaySchema.optional(),
25371
+ balance: moneyAmountSchema.optional(),
25372
+ balanceAt: moneyBalanceAtSchema.optional()
25373
+ }).strict().refine((f) => f.balance === undefined === (f.balanceAt === undefined), {
25374
+ message: "a balance and the day it's from go together",
25375
+ path: ["balanceAt"]
25376
+ });
25157
25377
  var moneyImportOutcomeSchema = _enum2([
25158
25378
  "new",
25159
25379
  "duplicate",
@@ -26857,6 +27077,8 @@ var healthShares = pgTable("health_shares", {
26857
27077
  episodeId: text("episode_id"),
26858
27078
  sections: text("sections").array().notNull().default(sql`'{}'::text[]`),
26859
27079
  snapshot: jsonb("snapshot").notNull(),
27080
+ note: text("note"),
27081
+ attachments: jsonb("attachments").notNull().default(sql`'[]'::jsonb`),
26860
27082
  tokenHash: text("token_hash").notNull().unique(),
26861
27083
  expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
26862
27084
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
@@ -31969,6 +32191,98 @@ var keyOperations = defineOperations({
31969
32191
  }
31970
32192
  });
31971
32193
 
32194
+ // ../../packages/schema/src/contracts/money-kraken.ts
32195
+ var KRAKEN_SECRET = /^[A-Za-z0-9+/]+={0,2}$/;
32196
+ var moneyKrakenKeyInputSchema = object({
32197
+ apiKey: string2().trim().min(16).max(200).regex(/^\S+$/, "The API key has no spaces."),
32198
+ apiSecret: string2().trim().min(40).max(200).regex(KRAKEN_SECRET, { message: "Paste the private key as Kraken shows it (base64)." }),
32199
+ accountId: ulidSchema.optional()
32200
+ }).strict();
32201
+ var moneyHoldingSchema = object({
32202
+ asset: string2(),
32203
+ amount: string2(),
32204
+ priceEur: string2().nullable(),
32205
+ valueEur: string2().nullable(),
32206
+ share: number2().nullable()
32207
+ });
32208
+ var moneyHoldingsSchema = object({
32209
+ accountId: ulidSchema,
32210
+ at: exports_coerce.date(),
32211
+ totalEur: string2(),
32212
+ dustEur: string2(),
32213
+ holdings: array(moneyHoldingSchema)
32214
+ });
32215
+ var moneyAccountHoldingsSchema = object({ holdings: moneyHoldingsSchema.nullable() });
32216
+ var moneyKrakenLinkStatusSchema = object({
32217
+ configured: boolean2(),
32218
+ link: object({
32219
+ id: ulidSchema,
32220
+ accountId: ulidSchema,
32221
+ accountTitle: string2(),
32222
+ status: _enum2(moneyConnectionStatus.enumValues),
32223
+ lastSyncAt: exports_coerce.date().nullable(),
32224
+ lastError: string2().nullable()
32225
+ }).nullable(),
32226
+ holdings: moneyHoldingsSchema.nullable()
32227
+ });
32228
+
32229
+ // ../../packages/schema/src/operations/kraken-link.ts
32230
+ var CONNECT2 = ["connections:write"];
32231
+ var AFTER2 = "`*` and `private:*` never grant `connections:write`: a key holds it by name, and a guest's key never does.";
32232
+ var krakenLinkOperations = defineOperations({
32233
+ status: {
32234
+ method: "get",
32235
+ path: "/money/kraken-link",
32236
+ scopes: CONNECT2,
32237
+ afterScopes: AFTER2,
32238
+ tag: "Money",
32239
+ summary: "Get the Kraken link",
32240
+ description: "Whether the key's member has a Kraken key set (`configured`), the investment account it feeds with how its last sync went (`link`: `status` `active` or `failing`, `lastSyncAt`, `lastError`), and the `holdings` that sync saw: per asset its `amount`, EUR `priceEur` and `valueEur` and `share`, with `totalEur` and `at`; holdings worth under \u20AC0.50 are left out of the list (`dustEur` sums them), never out of the total. The key is never returned.",
32241
+ status: 200,
32242
+ response: { description: "The Kraken link." },
32243
+ errors: { 403: true }
32244
+ },
32245
+ set: {
32246
+ method: "put",
32247
+ path: "/money/kraken-link",
32248
+ scopes: CONNECT2,
32249
+ afterScopes: AFTER2,
32250
+ tag: "Money",
32251
+ summary: "Set the Kraken key",
32252
+ description: "Keep the key's member's Kraken `apiKey` and `apiSecret` (the private key), sealed, then sync. Make the key at Kraken with only Query Funds ticked. The key is checked, never echoed: it must read the balance and must not see open orders (a key that can may do more than read, and is refused); the answer is the link, without it. `accountId` picks the investment account it feeds; left out, the link keeps its account, or a first set makes one named Kraken. 400 for a key Kraken refuses, a key that isn't read-only, or an account that isn't an EUR investment account.",
32253
+ body: moneyKrakenKeyInputSchema,
32254
+ status: 200,
32255
+ response: { description: "The Kraken link, without the key." },
32256
+ errors: { 400: true, 403: true, 404: true, 503: true },
32257
+ throttle: "strict",
32258
+ mcp: false
32259
+ },
32260
+ sync: {
32261
+ method: "post",
32262
+ path: "/money/kraken-link/sync",
32263
+ scopes: CONNECT2,
32264
+ afterScopes: AFTER2,
32265
+ tag: "Money",
32266
+ summary: "Sync Kraken now",
32267
+ description: "Read the holdings from Kraken now, value them in EUR and set the account's balance, as the daily sync does. A failure keeps the last holdings and marks the link `failing` with its reason. Answers the link. 400 before a key is set.",
32268
+ status: 200,
32269
+ response: { description: "The Kraken link after the sync." },
32270
+ errors: { 400: true, 403: true }
32271
+ },
32272
+ remove: {
32273
+ method: "delete",
32274
+ path: "/money/kraken-link",
32275
+ scopes: CONNECT2,
32276
+ afterScopes: AFTER2,
32277
+ tag: "Money",
32278
+ summary: "Remove the Kraken link",
32279
+ description: "Forget the key's member's Kraken key and stop syncing. The account stays, with its last balance and holdings.",
32280
+ status: 200,
32281
+ response: { description: "The Kraken link, not configured." },
32282
+ errors: { 403: true }
32283
+ }
32284
+ });
32285
+
31972
32286
  // ../../packages/schema/src/db/mail.ts
31973
32287
  var mailCampaignStatus = pgEnum("mail_campaign_status", [
31974
32288
  "draft",
@@ -32514,6 +32828,13 @@ var moneyNetWorthAccountSchema = object({
32514
32828
  source: moneySnapshotSourceSchema,
32515
32829
  month: moneyMonthSchema
32516
32830
  });
32831
+ var moneyNetWorthUnknownSchema = moneyNetWorthAccountSchema.pick({
32832
+ accountId: true,
32833
+ title: true,
32834
+ kind: true,
32835
+ visibility: true,
32836
+ currency: true
32837
+ });
32517
32838
  var moneyNetWorthSchema = object({
32518
32839
  scope: moneyBudgetScopeSchema,
32519
32840
  month: moneyMonthSchema,
@@ -32521,7 +32842,9 @@ var moneyNetWorthSchema = object({
32521
32842
  have: amount5,
32522
32843
  owe: amount5,
32523
32844
  points: array(moneyNetWorthPointSchema),
32524
- accounts: array(moneyNetWorthAccountSchema)
32845
+ accounts: array(moneyNetWorthAccountSchema),
32846
+ complete: boolean2(),
32847
+ unknown: array(moneyNetWorthUnknownSchema)
32525
32848
  });
32526
32849
  var setMoneyNetWorthValueInputSchema = object({
32527
32850
  accountId: ulidSchema,
@@ -32703,7 +33026,7 @@ var moneyOperations = defineOperations({
32703
33026
  scopes: ["finance:write"],
32704
33027
  tag: "Money",
32705
33028
  summary: "Import a statement",
32706
- description: "Book a statement's new lines as unreviewed transactions (the same upload as the preview). Re-importing a file, or an overlapping one, books nothing twice. An account without an IBAN takes the file's; the newest line's balance becomes the account's.",
33029
+ description: "Book a statement's new lines as unreviewed transactions (the same upload as the preview). Re-importing a file, or an overlapping one, books nothing twice. An account without an IBAN takes the file's; the newest line's balance becomes the account's. For a bank whose export has no running balance, `balance` + `balanceAt` (a day, read as its end, or a moment; together, never a day ahead) state what the account holds and anchor it there, as an account update does; a preview ignores them.",
32707
33030
  body: moneyImportFieldsSchema,
32708
33031
  multipart: STATEMENT,
32709
33032
  status: 201,
@@ -32750,7 +33073,7 @@ var moneyOperations = defineOperations({
32750
33073
  scopes: ["finance:write"],
32751
33074
  tag: "Money",
32752
33075
  summary: "Sync bank connections now",
32753
- description: "Sync every live bank link the key may see, or with `accountId` the one feeding that account, the way the daily run does: booked transactions go through the CSV import's pipeline (the bank's entry id, else the CSV row of the same day, amount and counterparty or reference, is matched instead of booked twice), the rules and transfer matching run, and the balance is updated. Per link: outcome (ok \xB7 failed), fetched, imported, matched, duplicates and the error in plain words. An account is the household's (`visibility: space`) or \"Only me\" (`owner`, the default for a new one); another member never sees an Only me account or anything on it.",
33076
+ description: "Sync every live bank link the key may see, or with `accountId` the one feeding that account, the way the daily run does: booked transactions go through the CSV import's pipeline (the bank's entry id, else the CSV row of the same amount booked within a day \u2014 the most alike when several fit \u2014 is matched: it adopts the bank's entry id instead of the row being booked twice), the rules and transfer matching run, and the balance is updated. Per link: outcome (ok \xB7 failed), fetched, imported, matched, duplicates and the error in plain words. An account is the household's (`visibility: space`) or \"Only me\" (`owner`, the default for a new one); another member never sees an Only me account or anything on it.",
32754
33077
  body: moneyBankSyncInputSchema,
32755
33078
  status: 200,
32756
33079
  response: { description: "What each sync did." },
@@ -32772,6 +33095,18 @@ var moneyOperations = defineOperations({
32772
33095
  response: { description: "The sync log." },
32773
33096
  errors: { 404: `No such account or transaction, or it is another member's "Only me" one.` }
32774
33097
  },
33098
+ dedupe: {
33099
+ method: "post",
33100
+ path: "/money/dedupe",
33101
+ scopes: ["finance:write"],
33102
+ tag: "Money",
33103
+ summary: "Fold a bank link's twins into the statement's rows",
33104
+ description: "Find, on one `accountId`, the movements booked twice: a statement import's row (no bank entry id) and a bank link's row (with one), paired by the bank sync's own rule \u2014 the same amount, booked a day apart at most, the most alike when several fit (counterparty IBAN, structured reference, name, then the words), never one row twice. A preview by default: the pairs (`keepId` the statement's row, `twinId` the bank's, their days, amount, description) and their counts, writing nothing. With `commit: true` each pair is folded: the statement's row keeps its category, review status, notes, links, splits and transfer, takes the bank's entry id and the details it lacked; whatever pointed at the twin (links, splits, settlements, its transfer peer, a rule made from it, its note) moves to the kept row, and the twin is deleted. `changed` counts what moved. A pair marked `ambiguous` (another row fit just as well) is listed but never committed. Running it again changes nothing.",
33105
+ body: moneyDedupeInputSchema,
33106
+ status: 200,
33107
+ response: { description: "The pairs, their counts and what changed." },
33108
+ errors: { 400: true, 404: NO_ACCOUNT }
33109
+ },
32775
33110
  getAccount: {
32776
33111
  method: "get",
32777
33112
  path: "/money/accounts/{id}",
@@ -32791,7 +33126,7 @@ var moneyOperations = defineOperations({
32791
33126
  scopes: ["finance:write"],
32792
33127
  tag: "Money",
32793
33128
  summary: "Update a money account",
32794
- description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it.",
33129
+ description: "Rename it, change its kind, IBAN, opening balance, what it counts towards, or who sees it. `balance` with `balanceAt` (a day, read as its end, or a moment) states what it held then: the anchor net worth counts from, back and forward through the ledger, for a bank whose export has no running balance. The two go together; a day ahead, or an account a bank link keeps, is a 400.",
32795
33130
  pathParams: { id: "The account's ULID." },
32796
33131
  pathParamSchemas: { id: ulidSchema },
32797
33132
  body: updateMoneyAccountInputSchema,
@@ -32907,6 +33242,36 @@ var moneyOperations = defineOperations({
32907
33242
  404: `No such account or transaction, or it is another member's "Only me" one.`
32908
33243
  }
32909
33244
  },
33245
+ linkTransaction: {
33246
+ method: "post",
33247
+ path: "/money/transactions/{id}/links",
33248
+ scopes: ["finance:write"],
33249
+ tag: "Money",
33250
+ summary: "Link a money transaction to something across do",
33251
+ description: "`nodeId` (a paper, a goal, an asset, an animal, a Project) and `kind`: `evidence` for a paper that proves it, `for_goal` to count it toward a goal, `paid_as` for what it paid, else `about` (the default). Again is a no-op. Answers the transaction with its `links`.",
33252
+ pathParams: { id: "The transaction's ULID." },
33253
+ pathParamSchemas: { id: ulidSchema },
33254
+ body: moneyTransactionLinkInputSchema,
33255
+ status: 200,
33256
+ response: { description: "The transaction." },
33257
+ errors: {
33258
+ 400: true,
33259
+ 404: `No such transaction or node, or it is another member's "Only me" one.`
33260
+ }
33261
+ },
33262
+ unlinkTransaction: {
33263
+ method: "delete",
33264
+ path: "/money/transactions/{id}/links/{nodeId}",
33265
+ scopes: ["finance:write"],
33266
+ tag: "Money",
33267
+ summary: "Unlink a money transaction from something across do",
33268
+ description: "Takes every link from the transaction to `nodeId` off; both stay. Undo is the link again (`POST \u2026/links` with the same `nodeId` and `kind`). Answers the transaction with its `links`.",
33269
+ pathParams: { id: "The transaction's ULID.", nodeId: "The linked node's ULID." },
33270
+ pathParamSchemas: { id: ulidSchema, nodeId: ulidSchema },
33271
+ status: 200,
33272
+ response: { description: "The transaction." },
33273
+ errors: { 404: "The transaction isn't linked to that node, or isn't yours to see." }
33274
+ },
32910
33275
  listSplits: {
32911
33276
  method: "get",
32912
33277
  path: "/money/transactions/{id}/splits",
@@ -33281,7 +33646,7 @@ var moneyOperations = defineOperations({
33281
33646
  scopes: ["finance:read"],
33282
33647
  tag: "Money",
33283
33648
  summary: "Get net worth and its monthly line",
33284
- description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `points` (one per month, oldest first, the current month last; `total` null before any account had a value) and `accounts` (biggest first, each with its `value`, `source` \u2014 balance, ledger or manual \u2014 and the `month` that value is from). A loan or a credit card always counts as owed. This month follows each account's balance; the months before an account was first read are worked back from its ledger; a value set by hand carries on until the next one. `scope`: `all` (default), `space` or `owner`; `months` 2\u201360 (12 by default).",
33649
+ description: "Net worth for the accounts that count in it (`includeInNetWorth`): `total`, `have` and `owe` this month, `points` (one per month, oldest first, the current month last; `total` null before any account had a value) and `accounts` (biggest first, each with its `value`, `source` \u2014 balance, ledger or manual \u2014 and the `month` that value is from). A loan or a credit card always counts as owed. This month follows each account's balance, carried forward by what was booked after `balanceAt`; the months before an account was first read are worked back from its ledger; a value set by hand carries on until the next one. An account whose ledger came from a bank but has no balance yet (an export with no running balance) is never counted as 0: it is listed under `unknown` and left out of the total, and `complete` is false. `scope`: `all` (default), `space` or `owner`; `months` 2\u201360 (12 by default).",
33285
33650
  query: [
33286
33651
  { name: "scope", description: "all, space or owner." },
33287
33652
  { name: "months", description: "How many months the line covers (2\u201360, default 12)." }
@@ -38776,7 +39141,12 @@ var gameFacetSchema = object({
38776
39141
  coverUrl,
38777
39142
  coverSources: coverSourcesSchema.nullish(),
38778
39143
  completion: number2().int().min(0).max(100).nullish(),
38779
- steamAppId: number2().int().positive().max(99999999).nullish()
39144
+ steamAppId: number2().int().positive().max(99999999).nullish(),
39145
+ format: _enum2(["physical", "digital"]).nullish(),
39146
+ store: string2().min(1).max(60).nullish(),
39147
+ studio: string2().min(1).max(120).nullish(),
39148
+ year: number2().int().min(1950).max(2100).nullish(),
39149
+ playedWith: string2().min(1).max(120).nullish()
38780
39150
  }).strict();
38781
39151
  var musicFacetSchema = object({
38782
39152
  artist: string2().min(1).max(200).nullish(),
@@ -39092,7 +39462,7 @@ var STATUS_MARK = {
39092
39462
  };
39093
39463
  function suggestionText(s) {
39094
39464
  const parts = [
39095
- s.area,
39465
+ [s.area, s.project].filter(Boolean).join(" \u203A ") || null,
39096
39466
  s.owner ? `owner ${ownerText(s.owner)}` : null,
39097
39467
  s.scheduledAt ? shortDate(s.scheduledAt) : null
39098
39468
  ].filter(Boolean);
@@ -42559,12 +42929,18 @@ var CONNECTION_COMMAND_SPECS = [
42559
42929
  },
42560
42930
  {
42561
42931
  path: ["money", "bank-link", "pending"],
42562
- summary: "A bank authorisation waiting to be mapped: its bank accounts and the suggestions.",
42563
- args: [{ name: "id", required: true, description: "The pendingId from `complete`." }],
42932
+ summary: "The bank authorisations waiting to be mapped, or one by its id: bank accounts and suggestions.",
42933
+ args: [
42934
+ {
42935
+ name: "id",
42936
+ required: false,
42937
+ description: "The pendingId from `complete`; without it, every one still waiting."
42938
+ }
42939
+ ],
42564
42940
  flags: [],
42565
42941
  scopes: scopesOf(bankLinkOperations.pending),
42566
- returns: "MoneyBankPending",
42567
- notes: `${routeOf(bankLinkOperations.pending)}.`
42942
+ returns: "MoneyBankPending (one) or MoneyBankPending[] (all)",
42943
+ notes: `${routeOf(bankLinkOperations.pending)}; without an id ${routeOf(bankLinkOperations.pendings)}, newest first, each with createdAt and expiresAt (a day after). Finds an authorisation whose browser tab is gone, before it runs out.`
42568
42944
  },
42569
42945
  {
42570
42946
  path: ["money", "bank-link", "link"],
@@ -42582,6 +42958,53 @@ var CONNECTION_COMMAND_SPECS = [
42582
42958
  returns: "MoneyConnectionView[]",
42583
42959
  notes: `${routeOf(bankLinkOperations.link)}. Each do account is fed by one bank account.`
42584
42960
  },
42961
+ {
42962
+ path: ["money", "kraken-link", "status"],
42963
+ summary: "Your Kraken link: the account it feeds, how its last sync went, and the holdings it saw.",
42964
+ args: [],
42965
+ flags: [],
42966
+ scopes: scopesOf(krakenLinkOperations.status),
42967
+ returns: "MoneyKrakenLinkStatus",
42968
+ notes: `${routeOf(krakenLinkOperations.status)}. The key is never returned.`
42969
+ },
42970
+ {
42971
+ path: ["money", "kraken-link", "set"],
42972
+ summary: "Give do your read-only Kraken API key, read from a file, and sync.",
42973
+ args: [],
42974
+ flags: [
42975
+ {
42976
+ name: "key-file",
42977
+ type: "string",
42978
+ description: "Path to a file holding the API key and its private key (two lines, or JSON with apiKey and apiSecret), or - to read it from stdin."
42979
+ },
42980
+ {
42981
+ name: "account",
42982
+ type: "string",
42983
+ description: "The EUR investment account it feeds. Left out: the link's account, or a new one named Kraken."
42984
+ }
42985
+ ],
42986
+ scopes: scopesOf(krakenLinkOperations.set),
42987
+ returns: "MoneyKrakenLinkStatus",
42988
+ notes: `${routeOf(krakenLinkOperations.set)}. Make the key at Kraken with only Query Funds ticked: a key that can see open orders is refused. The key and secret are read from the file and sealed; they are never a flag, never printed and never read back. A new Kraken account is "Only me", so making it needs finance:write on the key too (or pass --account). Replaces a key set before.`
42989
+ },
42990
+ {
42991
+ path: ["money", "kraken-link", "sync"],
42992
+ summary: "Read your Kraken holdings now and update the account's balance.",
42993
+ args: [],
42994
+ flags: [],
42995
+ scopes: scopesOf(krakenLinkOperations.sync),
42996
+ returns: "MoneyKrakenLinkStatus",
42997
+ notes: `${routeOf(krakenLinkOperations.sync)}. A failure keeps the last holdings and shows on the link as failing, with its reason.`
42998
+ },
42999
+ {
43000
+ path: ["money", "kraken-link", "remove"],
43001
+ summary: "Forget your Kraken key. The account keeps its last balance and holdings.",
43002
+ args: [],
43003
+ flags: [DESTRUCTIVE],
43004
+ scopes: scopesOf(krakenLinkOperations.remove),
43005
+ returns: "MoneyKrakenLinkStatus",
43006
+ notes: `${routeOf(krakenLinkOperations.remove)}. Gated behind --allow-destructive.`
43007
+ },
42585
43008
  {
42586
43009
  path: ["doccle", "connect"],
42587
43010
  summary: "Give do your Doccle login so the nightly import can run. do checks it with Doccle first and keeps it sealed.",
@@ -42688,8 +43111,14 @@ var CONNECTION_HANDLERS = {
42688
43111
  },
42689
43112
  "money bank-link pending": async ({ args, requireClient }) => {
42690
43113
  const id = args[0];
42691
- if (!id)
42692
- throw usageError("money bank-link pending needs the pendingId");
43114
+ if (!id) {
43115
+ return {
43116
+ data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pendings))),
43117
+ render: (data, mode) => Array.isArray(data) ? data.length === 0 ? "No bank links waiting to be mapped." : data.map((p) => renderPending(p, mode)).join(`
43118
+
43119
+ `) : renderRecord(data, mode)
43120
+ };
43121
+ }
42693
43122
  return {
42694
43123
  data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pending, { id }))),
42695
43124
  render: renderPending
@@ -42704,6 +43133,32 @@ var CONNECTION_HANDLERS = {
42704
43133
  `) || "Linked."
42705
43134
  };
42706
43135
  },
43136
+ "money kraken-link status": async ({ requireClient }) => ({
43137
+ data: await guarded([], () => requireClient().get(pathOf(krakenLinkOperations.status))),
43138
+ render: renderKraken
43139
+ }),
43140
+ "money kraken-link set": async ({ io, flags, requireClient }) => {
43141
+ refuseSecretFlags(flags, ["key", "api-key", "secret", "api-secret", "private-key"], "--key-file <path>");
43142
+ const text = await readSecret(io, flags, "key-file", "the Kraken key", "all");
43143
+ const { apiKey, apiSecret } = parseKrakenKey(text);
43144
+ const account = flagString(flags, "account")?.trim();
43145
+ const body = { apiKey, apiSecret, ...account ? { accountId: account } : {} };
43146
+ return {
43147
+ data: await guarded([apiKey, apiSecret], () => requireClient().put(pathOf(krakenLinkOperations.set), body)),
43148
+ render: renderKraken
43149
+ };
43150
+ },
43151
+ "money kraken-link sync": async ({ requireClient }) => ({
43152
+ data: await guarded([], () => requireClient().post(pathOf(krakenLinkOperations.sync), {})),
43153
+ render: renderKraken
43154
+ }),
43155
+ "money kraken-link remove": async ({ flags, requireClient }) => {
43156
+ assertDestructive(flags, "forget the Kraken key");
43157
+ return {
43158
+ data: await guarded([], () => requireClient().delete(pathOf(krakenLinkOperations.remove))),
43159
+ render: renderKraken
43160
+ };
43161
+ },
42707
43162
  "doccle connect": (ctx) => connectDoccle(ctx),
42708
43163
  "steam link": async ({ io, flags, requireClient }) => {
42709
43164
  refuseSecretFlags(flags, ["key", "steam-key", "web-api-key"], "--key-file <path>");
@@ -42880,7 +43335,8 @@ function renderPending(data, mode) {
42880
43335
  const p = data;
42881
43336
  if (!Array.isArray(p?.accounts))
42882
43337
  return renderRecord(data, mode);
42883
- const lines = [`${p.bank ?? "Bank"} \xB7 pending ${p.pendingId}`];
43338
+ const until = p.expiresAt ? ` \xB7 map before ${p.expiresAt}` : "";
43339
+ const lines = [`${p.bank ?? "Bank"} \xB7 pending ${p.pendingId}${until}`];
42884
43340
  for (const a of p.accounts) {
42885
43341
  const label = [a.name, a.ibanMasked].filter(Boolean).join(" ") || "account";
42886
43342
  const hint = a.suggestedAccountId ? ` \u2192 ${a.suggestedAccountId}` : "";
@@ -42889,6 +43345,48 @@ function renderPending(data, mode) {
42889
43345
  return lines.join(`
42890
43346
  `);
42891
43347
  }
43348
+ function parseKrakenKey(text) {
43349
+ const hint = "the key file should hold the API key and its private key: two lines, or JSON with apiKey and apiSecret";
43350
+ if (text.startsWith("{")) {
43351
+ let parsed;
43352
+ try {
43353
+ parsed = JSON.parse(text);
43354
+ } catch {
43355
+ throw usageError(hint);
43356
+ }
43357
+ const o = parsed ?? {};
43358
+ const apiKey = o.apiKey ?? o.key;
43359
+ const apiSecret = o.apiSecret ?? o.secret ?? o.privateKey;
43360
+ if (typeof apiKey !== "string" || typeof apiSecret !== "string")
43361
+ throw usageError(hint);
43362
+ return { apiKey: apiKey.trim(), apiSecret: apiSecret.trim() };
43363
+ }
43364
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
43365
+ if (lines.length !== 2)
43366
+ throw usageError(hint);
43367
+ return { apiKey: lines[0], apiSecret: lines[1] };
43368
+ }
43369
+ function renderKraken(data, mode) {
43370
+ const k = data;
43371
+ if (typeof k?.configured !== "boolean")
43372
+ return renderRecord(data, mode);
43373
+ if (!k.link)
43374
+ return k.configured ? "Kraken key set, not linked yet." : "No Kraken key set.";
43375
+ const synced = k.link.lastSyncAt ? ` \xB7 synced ${k.link.lastSyncAt}` : " \xB7 not synced yet";
43376
+ const lines = [`Kraken \u2192 ${k.link.accountTitle} \xB7 ${k.link.status}${synced}`];
43377
+ if (k.link.lastError)
43378
+ lines.push(` ${k.link.lastError}`);
43379
+ if (k.holdings) {
43380
+ lines.push(`Holdings at ${k.holdings.at} \xB7 \u20AC ${k.holdings.totalEur}`);
43381
+ for (const h of k.holdings.holdings) {
43382
+ const value = h.valueEur === null ? "no EUR price" : `\u20AC ${h.valueEur}`;
43383
+ const share = h.share === null ? "" : ` \xB7 ${(h.share * 100).toFixed(1)}%`;
43384
+ lines.push(` ${h.asset.padEnd(6)} ${h.amount} ${value}${share}`);
43385
+ }
43386
+ }
43387
+ return lines.join(`
43388
+ `);
43389
+ }
42892
43390
  var commands10 = pairCommands(CONNECTION_COMMAND_SPECS, CONNECTION_HANDLERS);
42893
43391
 
42894
43392
  // src/commands/contacts.ts
@@ -47688,6 +48186,14 @@ var commands30 = [
47688
48186
  // src/commands/money.ts
47689
48187
  import { readFileSync as readFileSync8 } from "fs";
47690
48188
  import { basename as basename5 } from "path";
48189
+ function renderDedupe(data) {
48190
+ const r = data;
48191
+ 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" : ""}`);
48192
+ const c = r.changed;
48193
+ lines.push(`${r.counts.pairs} pairs, ${r.counts.ambiguous} ambiguous`, r.committed ? `merged ${c.merged} \xB7 links ${c.links} \xB7 splits ${c.splits} \xB7 settlements ${c.settlements} \xB7 transfers ${c.transfers} \xB7 rules ${c.rules} \xB7 notes ${c.notes}` : "preview: nothing changed (--commit folds the unambiguous pairs)");
48194
+ return lines.join(`
48195
+ `);
48196
+ }
47691
48197
  function partyFlag(flags) {
47692
48198
  const userId = flagString(flags, "with-user");
47693
48199
  const contactId = flagString(flags, "with-contact");
@@ -47748,13 +48254,34 @@ var commands31 = [
47748
48254
  defineCommand({
47749
48255
  operation: moneyOperations.updateAccount,
47750
48256
  path: ["money", "account-update"],
47751
- summary: "Edit a money account.",
48257
+ summary: "Edit a money account, or state its balance (for an export with no balance column).",
47752
48258
  args: [{ name: "id", required: true, description: "Account ULID." }],
47753
- flags: [{ name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." }],
48259
+ flags: [
48260
+ { name: "data", type: "string", description: "UpdateMoneyAccountInput JSON." },
48261
+ {
48262
+ name: "balance",
48263
+ type: "string",
48264
+ description: "What the account holds, e.g. 1234.56 (sets balance)."
48265
+ },
48266
+ {
48267
+ name: "balance-at",
48268
+ type: "string",
48269
+ description: "The day (YYYY-MM-DD, its end) or moment that balance is from (default today)."
48270
+ }
48271
+ ],
47754
48272
  returns: "MoneyAccountView",
47755
- run: async ({ args, flags, requireClient }) => {
48273
+ notes: ". `--balance 1234.56 --balance-at 2026-10-27` anchors an account whose bank export has no running balance (ING's): net worth counts from it, back and forward through the ledger. A negative one rides as `--balance=-20`. A day ahead is refused, and so is an account a bank link keeps. `--data` takes the rest (title, kind, iban, includeInNetWorth, \u2026).",
48274
+ run: async ({ args, flags, io, requireClient }) => {
47756
48275
  const id = requireArg(args, 0, "account id");
47757
- const body = parseInput(updateMoneyAccountInputSchema, readDataFlag(flags));
48276
+ const balance = flagString(flags, "balance");
48277
+ const balanceAt = flagString(flags, "balance-at");
48278
+ if (balanceAt !== undefined && balance === undefined) {
48279
+ throw usageError("--balance-at goes with --balance");
48280
+ }
48281
+ const body = parseInput(updateMoneyAccountInputSchema, {
48282
+ ...readDataFlag(flags),
48283
+ ...balance !== undefined ? { balance, balanceAt: balanceAt ?? isoDay(io.now()) } : {}
48284
+ });
47758
48285
  return {
47759
48286
  data: await requireClient().patch(pathOf(moneyOperations.updateAccount, { id }), body),
47760
48287
  render: renderRecord
@@ -47807,6 +48334,32 @@ var commands31 = [
47807
48334
  };
47808
48335
  }
47809
48336
  }),
48337
+ defineCommand({
48338
+ operation: moneyOperations.dedupe,
48339
+ path: ["money", "dedupe"],
48340
+ summary: "Find movements booked twice on an account (a statement's row and a bank link's twin); --commit folds them.",
48341
+ args: [],
48342
+ flags: [
48343
+ { name: "account", type: "string", description: "The account ULID (required)." },
48344
+ {
48345
+ name: "commit",
48346
+ type: "boolean",
48347
+ description: "Fold the pairs; without it, a preview that writes nothing."
48348
+ }
48349
+ ],
48350
+ returns: "MoneyDedupeResult",
48351
+ notes: ". Pairs by the bank sync's rule: same amount, booked a day apart at most, the most alike when several fit. A commit keeps the statement's row (category, review status, notes, links, splits, transfer), gives it the bank's entry id, moves what pointed at the twin onto it and deletes the twin. An `ambiguous` pair is never committed. Running it twice changes nothing the second time.",
48352
+ run: async ({ flags, requireClient }) => {
48353
+ const body = parseInput(moneyDedupeInputSchema, {
48354
+ accountId: requireFlag(flags, "account", "say which account: --account <id>"),
48355
+ ...flagBool(flags, "commit") ? { commit: true } : {}
48356
+ });
48357
+ return {
48358
+ data: await requireClient().post(pathOf(moneyOperations.dedupe), body),
48359
+ render: renderDedupe
48360
+ };
48361
+ }
48362
+ }),
47810
48363
  defineCommand({
47811
48364
  operation: moneyOperations.listConnectionSyncs,
47812
48365
  path: ["money", "connection-syncs"],
@@ -47867,9 +48420,9 @@ var commands31 = [
47867
48420
  { name: "cursor", type: "string", description: "The previous page's nextCursor." }
47868
48421
  ],
47869
48422
  returns: "{ data: MoneyTransactionView[], nextCursor }",
47870
- notes: ". Amounts are signed decimal strings. `--business` keeps rows with a business share; `--business-status` and `--tax-tag` narrow further.",
48423
+ notes: ". Amounts are signed decimal strings. `--business` keeps rows with a business share; `--business-status` and `--tax-tag` narrow further. A page is 100 rows at most: while `nextCursor` isn't null, pass it back as `--cursor` for the next.",
47871
48424
  run: async ({ flags, requireClient }) => ({
47872
- data: await requireClient().get(pathOf(moneyOperations.listTransactions), {
48425
+ data: await requireClient().getPage(pathOf(moneyOperations.listTransactions), {
47873
48426
  query: {
47874
48427
  accountId: flagString(flags, "account"),
47875
48428
  status: flagString(flags, "status"),
@@ -48002,6 +48555,55 @@ var commands31 = [
48002
48555
  };
48003
48556
  }
48004
48557
  }),
48558
+ defineCommand({
48559
+ operation: moneyOperations.linkTransaction,
48560
+ path: ["money", "link"],
48561
+ summary: "Link a transaction to something across do (a paper, a goal, an asset).",
48562
+ args: [
48563
+ { name: "id", required: true, description: "Transaction ULID." },
48564
+ { name: "node", required: true, description: "The node's ULID." }
48565
+ ],
48566
+ flags: [
48567
+ {
48568
+ name: "kind",
48569
+ type: "string",
48570
+ description: "evidence | for_goal | about | paid_as (default about)."
48571
+ }
48572
+ ],
48573
+ returns: "MoneyTransactionView",
48574
+ notes: " Again is a no-op; `do money unlink` takes it off.",
48575
+ run: async ({ args, flags, requireClient }) => {
48576
+ const id = requireArg(args, 0, "transaction id");
48577
+ const body = parseInput(moneyTransactionLinkInputSchema, {
48578
+ nodeId: requireArg(args, 1, "node id"),
48579
+ kind: flagString(flags, "kind")
48580
+ });
48581
+ return {
48582
+ data: await requireClient().post(pathOf(moneyOperations.linkTransaction, { id }), body),
48583
+ render: renderRecord
48584
+ };
48585
+ }
48586
+ }),
48587
+ defineCommand({
48588
+ operation: moneyOperations.unlinkTransaction,
48589
+ path: ["money", "unlink"],
48590
+ summary: "Unlink a transaction from a node across do (both stay).",
48591
+ args: [
48592
+ { name: "id", required: true, description: "Transaction ULID." },
48593
+ { name: "node", required: true, description: "The linked node's ULID." }
48594
+ ],
48595
+ flags: [],
48596
+ returns: "MoneyTransactionView",
48597
+ notes: " Undo is `do money link` with the same node and kind.",
48598
+ run: async ({ args, requireClient }) => {
48599
+ const id = requireArg(args, 0, "transaction id");
48600
+ const nodeId = requireArg(args, 1, "node id");
48601
+ return {
48602
+ data: await requireClient().delete(pathOf(moneyOperations.unlinkTransaction, { id, nodeId })),
48603
+ render: renderRecord
48604
+ };
48605
+ }
48606
+ }),
48005
48607
  defineCommand({
48006
48608
  operation: moneyOperations.listSplits,
48007
48609
  path: ["money", "splits"],
@@ -48284,12 +48886,22 @@ var commands31 = [
48284
48886
  flags: [
48285
48887
  { name: "account", type: "string", description: "The account's ULID." },
48286
48888
  { name: "since", type: "string", description: "First day to take (default 2025-01-01)." },
48287
- { name: "commit", type: "boolean", description: "Book the new lines (else a preview)." }
48889
+ { name: "commit", type: "boolean", description: "Book the new lines (else a preview)." },
48890
+ {
48891
+ name: "balance",
48892
+ type: "string",
48893
+ description: "What the account holds, for an export with no balance column (anchors it with --commit)."
48894
+ },
48895
+ {
48896
+ name: "balance-at",
48897
+ type: "string",
48898
+ description: "The day (YYYY-MM-DD, its end) or moment --balance is from (default today)."
48899
+ }
48288
48900
  ],
48289
48901
  scopes: [...moneyOperations.previewImport.scopes, ...moneyOperations.commitImport.scopes],
48290
48902
  returns: "MoneyImportPreview | MoneyImportResult",
48291
- notes: `${routeOf(moneyOperations.previewImport)}, or ${routeOf(moneyOperations.commitImport)} with --commit (\`money_import_file\`). A KBC Touch export reads as is (\`;\`, comma decimals, UTF-8 or Windows-1252), and so does a CODA file. Each line is new, duplicate, before_window, other_account or other_currency; a line at exactly 0,00 (nothing moved, e.g. ING's empty SEPA lines) is left out and listed under \`skipped\`, counted as \`counts.nothing_moved\`, never a problem; re-importing books nothing twice. On a credit_card account the file is the card's statement: its CSV export (KBC's credit card CSV reads as is; a transaction cost is its own row) or the statement PDF's text (\`cardStatement\` says which, the total and the debit day); paying it off is a transfer, never spending twice.`,
48292
- run: async ({ args, flags, requireClient }) => {
48903
+ notes: `${routeOf(moneyOperations.previewImport)}, or ${routeOf(moneyOperations.commitImport)} with --commit (\`money_import_file\`). A KBC Touch export reads as is (\`;\`, comma decimals, UTF-8 or Windows-1252), and so does a CODA file. Each line is new, duplicate, before_window, other_account or other_currency; a line at exactly 0,00 (nothing moved, e.g. ING's empty SEPA lines) is left out and listed under \`skipped\`, counted as \`counts.nothing_moved\`, never a problem; re-importing books nothing twice. On a credit_card account the file is the card's statement: its CSV export (KBC's credit card CSV reads as is; a transaction cost is its own row) or the statement PDF's text (\`cardStatement\` says which, the total and the debit day); paying it off is a transfer, never spending twice. A bank whose export has no running balance (ING's) leaves the account's balance unknown: \`--balance 1234.56 [--balance-at YYYY-MM-DD]\` with --commit states it, as \`do money account-update --balance\` does (a preview ignores it).`,
48904
+ run: async ({ args, flags, io, requireClient }) => {
48293
48905
  const path = requireArg(args, 0, "path to the statement CSV");
48294
48906
  const account = requireFlag(flags, "account", "money import needs --account <account id>");
48295
48907
  let bytes;
@@ -48303,6 +48915,19 @@ var commands31 = [
48303
48915
  const since = flagString(flags, "since");
48304
48916
  if (since !== undefined)
48305
48917
  form.append("since", since);
48918
+ const balance = flagString(flags, "balance");
48919
+ const balanceAt = flagString(flags, "balance-at");
48920
+ if (balanceAt !== undefined && balance === undefined) {
48921
+ throw usageError("--balance-at goes with --balance");
48922
+ }
48923
+ if (balance !== undefined) {
48924
+ const anchor = parseInput(updateMoneyAccountInputSchema, {
48925
+ balance,
48926
+ balanceAt: balanceAt ?? isoDay(io.now())
48927
+ });
48928
+ form.append("balance", anchor.balance);
48929
+ form.append("balanceAt", anchor.balanceAt);
48930
+ }
48306
48931
  const name = basename5(path);
48307
48932
  form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/csv" }), name);
48308
48933
  const op = flagBool(flags, "commit") ? moneyOperations.commitImport : moneyOperations.previewImport;
@@ -48451,7 +49076,7 @@ var commands31 = [
48451
49076
  }
48452
49077
  ],
48453
49078
  returns: "MoneyNetWorth",
48454
- notes: ". total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed.",
49079
+ notes: ". total, have and owe this month; points per month (oldest first, total null before any value); accounts biggest first with value, source (balance \xB7 ledger \xB7 manual) and the month it's from. A loan or a card always counts as owed. `complete: false` when an account counts but its balance isn't known (a bank export with no running balance): it's listed under `unknown`, left out of the total, never counted as 0; `do money account-update <id> --balance \u2026 --balance-at \u2026` states it.",
48455
49080
  run: async ({ flags, requireClient }) => ({
48456
49081
  data: await requireClient().get(pathOf(moneyOperations.netWorthRead), {
48457
49082
  query: { scope: flagString(flags, "scope"), months: flagString(flags, "months") }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.331.0",
3
+ "version": "1.333.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",