@robbeverhelst/do 1.332.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 +486 -33
  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.332.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
 
@@ -18355,6 +18355,7 @@ var userSettings = pgTable("user_settings", {
18355
18355
  foodDiaryMode: boolean4("food_diary_mode").notNull().default(false),
18356
18356
  calendarZoom: integer2("calendar_zoom"),
18357
18357
  lifeBlockSharing: jsonb("life_block_sharing").$type(),
18358
+ recentSearches: jsonb("recent_searches").$type().notNull().default([]),
18358
18359
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18359
18360
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
18360
18361
  }, (t) => [unique("user_settings_tenant_user_uniq").on(t.tenantId, t.userId)]);
@@ -19955,9 +19956,29 @@ var animalLostCards = pgTable("animal_lost_cards", {
19955
19956
  phone: text("phone"),
19956
19957
  showChipped: boolean4("show_chipped").notNull().default(false),
19957
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"),
19958
19963
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19959
19964
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
19960
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)]);
19961
19982
  var animalHandoverLinks = pgTable("animal_handover_links", {
19962
19983
  id: text("id").primaryKey(),
19963
19984
  tenantId: text("tenant_id").notNull().references(() => tenants.id),
@@ -20769,22 +20790,72 @@ var animalSitterNoteInputSchema = object({
20769
20790
  body: string2().trim().max(1000).default("")
20770
20791
  }).strict();
20771
20792
  var animalSitterPhoneSchema = object({ phone: string2() });
20793
+ var lostCardPhoneSchema = string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -");
20772
20794
  var animalLostCardInputSchema = object({
20773
20795
  photoFileId: ulidSchema.nullish(),
20774
- phone: string2().trim().regex(/^\+?[0-9][0-9 ()./-]{4,30}$/, "A phone number: digits, spaces, + ( ) . / -").nullish(),
20796
+ phone: lostCardPhoneSchema.nullish(),
20775
20797
  showChipped: boolean2().optional(),
20776
- 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()
20777
20803
  }).strict();
20778
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
+ });
20779
20812
  var animalLostCardPublicSchema = object({
20780
20813
  name: string2(),
20781
20814
  speciesKey: string2().nullable(),
20782
20815
  kind: string2().nullable(),
20783
20816
  missing: boolean2(),
20784
20817
  hasPhoto: boolean2(),
20785
- phone: string2().nullable(),
20818
+ phoneMasked: string2().nullable(),
20786
20819
  chipped: boolean2(),
20787
- 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()
20788
20859
  });
20789
20860
  var animalLostCardStateSchema = object({
20790
20861
  card: object({
@@ -20793,7 +20864,12 @@ var animalLostCardStateSchema = object({
20793
20864
  phone: string2().nullable(),
20794
20865
  showChipped: boolean2(),
20795
20866
  note: string2().nullable(),
20867
+ description: string2().nullable(),
20868
+ answersTo: string2().nullable(),
20869
+ missingSince: exports_coerce.date().nullable(),
20870
+ missingWhere: string2().nullable(),
20796
20871
  since: exports_coerce.date(),
20872
+ reports: array(animalLostReportSchema),
20797
20873
  preview: animalLostCardPublicSchema
20798
20874
  }).nullable(),
20799
20875
  photos: array(animalLostCardPhotoSchema),
@@ -21044,7 +21120,7 @@ var animalOperations = defineOperations({
21044
21120
  scopes: READ,
21045
21121
  tag: "Animals",
21046
21122
  summary: "An animal's lost-pet card",
21047
- 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.",
21048
21124
  status: 200,
21049
21125
  response: { description: "The card's state." },
21050
21126
  errors: { 404: true }
@@ -21736,6 +21812,8 @@ var taskOwnerSchema = actorRefSchema.extend({ desk: string2().nullable() });
21736
21812
  var taskFilingSuggestionSchema = object({
21737
21813
  areaId: ulidSchema.nullable(),
21738
21814
  area: string2().nullable(),
21815
+ projectId: ulidSchema.nullable().default(null),
21816
+ project: string2().nullable().default(null),
21739
21817
  owner: taskOwnerSchema.nullable(),
21740
21818
  scheduledAt: exports_coerce.date().nullable(),
21741
21819
  by: actorRefSchema
@@ -23146,6 +23224,8 @@ var moneyBankPendingSchema = object({
23146
23224
  pendingId: ulidSchema,
23147
23225
  bank: string2(),
23148
23226
  consentValidUntil: exports_coerce.date(),
23227
+ createdAt: exports_coerce.date(),
23228
+ expiresAt: exports_coerce.date(),
23149
23229
  accounts: array(object({
23150
23230
  index: number2().int().min(0),
23151
23231
  name: string2().nullable(),
@@ -23181,6 +23261,33 @@ var moneyBankSyncResultSchema = object({
23181
23261
  error: true
23182
23262
  }).extend({ accountTitle: string2() }))
23183
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
+ });
23184
23291
 
23185
23292
  // ../../packages/schema/src/operations/bank-link.ts
23186
23293
  var CONNECT = ["connections:write"];
@@ -23263,6 +23370,18 @@ var bankLinkOperations = defineOperations({
23263
23370
  response: { description: "The bank accounts to map." },
23264
23371
  errors: { 400: true, 403: true, 409: true }
23265
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
+ },
23266
23385
  pending: {
23267
23386
  method: "get",
23268
23387
  path: "/money/bank-link/pending/{id}",
@@ -25085,7 +25204,8 @@ var moneyAccountSchema = createSelectSchema(moneyAccounts, {
25085
25204
  });
25086
25205
  var moneyAccountViewSchema = moneyAccountSchema.omit({ ibanHash: true }).extend({
25087
25206
  title: string2(),
25088
- visibility: _enum2(NODE_VISIBILITY_VALUES)
25207
+ visibility: _enum2(NODE_VISIBILITY_VALUES),
25208
+ balanceKnown: boolean2()
25089
25209
  });
25090
25210
  var moneyCategorySchema = createSelectSchema(moneyCategories, {
25091
25211
  archivedAt: exports_coerce.date().nullable(),
@@ -25145,7 +25265,14 @@ var createMoneyAccountInputSchema = object({
25145
25265
  includeInNetWorth: boolean2().optional(),
25146
25266
  visibility: _enum2(NODE_VISIBILITY_VALUES).optional()
25147
25267
  }).strict();
25148
- 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
+ });
25149
25276
  var createMoneyCategoryInputSchema = object({
25150
25277
  id: ulidSchema,
25151
25278
  name: string2().trim().min(1).max(60),
@@ -25240,8 +25367,13 @@ var moneyTransactionPageSchema = object({
25240
25367
  var MONEY_IMPORT_MAX_BYTES = 5 * 1024 * 1024;
25241
25368
  var moneyImportFieldsSchema = object({
25242
25369
  accountId: ulidSchema,
25243
- since: keyDateDaySchema.optional()
25244
- }).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
+ });
25245
25377
  var moneyImportOutcomeSchema = _enum2([
25246
25378
  "new",
25247
25379
  "duplicate",
@@ -26945,6 +27077,8 @@ var healthShares = pgTable("health_shares", {
26945
27077
  episodeId: text("episode_id"),
26946
27078
  sections: text("sections").array().notNull().default(sql`'{}'::text[]`),
26947
27079
  snapshot: jsonb("snapshot").notNull(),
27080
+ note: text("note"),
27081
+ attachments: jsonb("attachments").notNull().default(sql`'[]'::jsonb`),
26948
27082
  tokenHash: text("token_hash").notNull().unique(),
26949
27083
  expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
26950
27084
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
@@ -32057,6 +32191,98 @@ var keyOperations = defineOperations({
32057
32191
  }
32058
32192
  });
32059
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
+
32060
32286
  // ../../packages/schema/src/db/mail.ts
32061
32287
  var mailCampaignStatus = pgEnum("mail_campaign_status", [
32062
32288
  "draft",
@@ -32602,6 +32828,13 @@ var moneyNetWorthAccountSchema = object({
32602
32828
  source: moneySnapshotSourceSchema,
32603
32829
  month: moneyMonthSchema
32604
32830
  });
32831
+ var moneyNetWorthUnknownSchema = moneyNetWorthAccountSchema.pick({
32832
+ accountId: true,
32833
+ title: true,
32834
+ kind: true,
32835
+ visibility: true,
32836
+ currency: true
32837
+ });
32605
32838
  var moneyNetWorthSchema = object({
32606
32839
  scope: moneyBudgetScopeSchema,
32607
32840
  month: moneyMonthSchema,
@@ -32609,7 +32842,9 @@ var moneyNetWorthSchema = object({
32609
32842
  have: amount5,
32610
32843
  owe: amount5,
32611
32844
  points: array(moneyNetWorthPointSchema),
32612
- accounts: array(moneyNetWorthAccountSchema)
32845
+ accounts: array(moneyNetWorthAccountSchema),
32846
+ complete: boolean2(),
32847
+ unknown: array(moneyNetWorthUnknownSchema)
32613
32848
  });
32614
32849
  var setMoneyNetWorthValueInputSchema = object({
32615
32850
  accountId: ulidSchema,
@@ -32791,7 +33026,7 @@ var moneyOperations = defineOperations({
32791
33026
  scopes: ["finance:write"],
32792
33027
  tag: "Money",
32793
33028
  summary: "Import a statement",
32794
- 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.",
32795
33030
  body: moneyImportFieldsSchema,
32796
33031
  multipart: STATEMENT,
32797
33032
  status: 201,
@@ -32838,7 +33073,7 @@ var moneyOperations = defineOperations({
32838
33073
  scopes: ["finance:write"],
32839
33074
  tag: "Money",
32840
33075
  summary: "Sync bank connections now",
32841
- 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.",
32842
33077
  body: moneyBankSyncInputSchema,
32843
33078
  status: 200,
32844
33079
  response: { description: "What each sync did." },
@@ -32860,6 +33095,18 @@ var moneyOperations = defineOperations({
32860
33095
  response: { description: "The sync log." },
32861
33096
  errors: { 404: `No such account or transaction, or it is another member's "Only me" one.` }
32862
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
+ },
32863
33110
  getAccount: {
32864
33111
  method: "get",
32865
33112
  path: "/money/accounts/{id}",
@@ -32879,7 +33126,7 @@ var moneyOperations = defineOperations({
32879
33126
  scopes: ["finance:write"],
32880
33127
  tag: "Money",
32881
33128
  summary: "Update a money account",
32882
- 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.",
32883
33130
  pathParams: { id: "The account's ULID." },
32884
33131
  pathParamSchemas: { id: ulidSchema },
32885
33132
  body: updateMoneyAccountInputSchema,
@@ -33399,7 +33646,7 @@ var moneyOperations = defineOperations({
33399
33646
  scopes: ["finance:read"],
33400
33647
  tag: "Money",
33401
33648
  summary: "Get net worth and its monthly line",
33402
- 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).",
33403
33650
  query: [
33404
33651
  { name: "scope", description: "all, space or owner." },
33405
33652
  { name: "months", description: "How many months the line covers (2\u201360, default 12)." }
@@ -39215,7 +39462,7 @@ var STATUS_MARK = {
39215
39462
  };
39216
39463
  function suggestionText(s) {
39217
39464
  const parts = [
39218
- s.area,
39465
+ [s.area, s.project].filter(Boolean).join(" \u203A ") || null,
39219
39466
  s.owner ? `owner ${ownerText(s.owner)}` : null,
39220
39467
  s.scheduledAt ? shortDate(s.scheduledAt) : null
39221
39468
  ].filter(Boolean);
@@ -42682,12 +42929,18 @@ var CONNECTION_COMMAND_SPECS = [
42682
42929
  },
42683
42930
  {
42684
42931
  path: ["money", "bank-link", "pending"],
42685
- summary: "A bank authorisation waiting to be mapped: its bank accounts and the suggestions.",
42686
- 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
+ ],
42687
42940
  flags: [],
42688
42941
  scopes: scopesOf(bankLinkOperations.pending),
42689
- returns: "MoneyBankPending",
42690
- 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.`
42691
42944
  },
42692
42945
  {
42693
42946
  path: ["money", "bank-link", "link"],
@@ -42705,6 +42958,53 @@ var CONNECTION_COMMAND_SPECS = [
42705
42958
  returns: "MoneyConnectionView[]",
42706
42959
  notes: `${routeOf(bankLinkOperations.link)}. Each do account is fed by one bank account.`
42707
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
+ },
42708
43008
  {
42709
43009
  path: ["doccle", "connect"],
42710
43010
  summary: "Give do your Doccle login so the nightly import can run. do checks it with Doccle first and keeps it sealed.",
@@ -42811,8 +43111,14 @@ var CONNECTION_HANDLERS = {
42811
43111
  },
42812
43112
  "money bank-link pending": async ({ args, requireClient }) => {
42813
43113
  const id = args[0];
42814
- if (!id)
42815
- 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
+ }
42816
43122
  return {
42817
43123
  data: await guarded([], () => requireClient().get(pathOf(bankLinkOperations.pending, { id }))),
42818
43124
  render: renderPending
@@ -42827,6 +43133,32 @@ var CONNECTION_HANDLERS = {
42827
43133
  `) || "Linked."
42828
43134
  };
42829
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
+ },
42830
43162
  "doccle connect": (ctx) => connectDoccle(ctx),
42831
43163
  "steam link": async ({ io, flags, requireClient }) => {
42832
43164
  refuseSecretFlags(flags, ["key", "steam-key", "web-api-key"], "--key-file <path>");
@@ -43003,7 +43335,8 @@ function renderPending(data, mode) {
43003
43335
  const p = data;
43004
43336
  if (!Array.isArray(p?.accounts))
43005
43337
  return renderRecord(data, mode);
43006
- 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}`];
43007
43340
  for (const a of p.accounts) {
43008
43341
  const label = [a.name, a.ibanMasked].filter(Boolean).join(" ") || "account";
43009
43342
  const hint = a.suggestedAccountId ? ` \u2192 ${a.suggestedAccountId}` : "";
@@ -43012,6 +43345,48 @@ function renderPending(data, mode) {
43012
43345
  return lines.join(`
43013
43346
  `);
43014
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
+ }
43015
43390
  var commands10 = pairCommands(CONNECTION_COMMAND_SPECS, CONNECTION_HANDLERS);
43016
43391
 
43017
43392
  // src/commands/contacts.ts
@@ -47811,6 +48186,14 @@ var commands30 = [
47811
48186
  // src/commands/money.ts
47812
48187
  import { readFileSync as readFileSync8 } from "fs";
47813
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
+ }
47814
48197
  function partyFlag(flags) {
47815
48198
  const userId = flagString(flags, "with-user");
47816
48199
  const contactId = flagString(flags, "with-contact");
@@ -47871,13 +48254,34 @@ var commands31 = [
47871
48254
  defineCommand({
47872
48255
  operation: moneyOperations.updateAccount,
47873
48256
  path: ["money", "account-update"],
47874
- summary: "Edit a money account.",
48257
+ summary: "Edit a money account, or state its balance (for an export with no balance column).",
47875
48258
  args: [{ name: "id", required: true, description: "Account ULID." }],
47876
- 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
+ ],
47877
48272
  returns: "MoneyAccountView",
47878
- 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 }) => {
47879
48275
  const id = requireArg(args, 0, "account id");
47880
- 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
+ });
47881
48285
  return {
47882
48286
  data: await requireClient().patch(pathOf(moneyOperations.updateAccount, { id }), body),
47883
48287
  render: renderRecord
@@ -47930,6 +48334,32 @@ var commands31 = [
47930
48334
  };
47931
48335
  }
47932
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
+ }),
47933
48363
  defineCommand({
47934
48364
  operation: moneyOperations.listConnectionSyncs,
47935
48365
  path: ["money", "connection-syncs"],
@@ -47990,9 +48420,9 @@ var commands31 = [
47990
48420
  { name: "cursor", type: "string", description: "The previous page's nextCursor." }
47991
48421
  ],
47992
48422
  returns: "{ data: MoneyTransactionView[], nextCursor }",
47993
- 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.",
47994
48424
  run: async ({ flags, requireClient }) => ({
47995
- data: await requireClient().get(pathOf(moneyOperations.listTransactions), {
48425
+ data: await requireClient().getPage(pathOf(moneyOperations.listTransactions), {
47996
48426
  query: {
47997
48427
  accountId: flagString(flags, "account"),
47998
48428
  status: flagString(flags, "status"),
@@ -48456,12 +48886,22 @@ var commands31 = [
48456
48886
  flags: [
48457
48887
  { name: "account", type: "string", description: "The account's ULID." },
48458
48888
  { name: "since", type: "string", description: "First day to take (default 2025-01-01)." },
48459
- { 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
+ }
48460
48900
  ],
48461
48901
  scopes: [...moneyOperations.previewImport.scopes, ...moneyOperations.commitImport.scopes],
48462
48902
  returns: "MoneyImportPreview | MoneyImportResult",
48463
- 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.`,
48464
- 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 }) => {
48465
48905
  const path = requireArg(args, 0, "path to the statement CSV");
48466
48906
  const account = requireFlag(flags, "account", "money import needs --account <account id>");
48467
48907
  let bytes;
@@ -48475,6 +48915,19 @@ var commands31 = [
48475
48915
  const since = flagString(flags, "since");
48476
48916
  if (since !== undefined)
48477
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
+ }
48478
48931
  const name = basename5(path);
48479
48932
  form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/csv" }), name);
48480
48933
  const op = flagBool(flags, "commit") ? moneyOperations.commitImport : moneyOperations.previewImport;
@@ -48623,7 +49076,7 @@ var commands31 = [
48623
49076
  }
48624
49077
  ],
48625
49078
  returns: "MoneyNetWorth",
48626
- 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.",
48627
49080
  run: async ({ flags, requireClient }) => ({
48628
49081
  data: await requireClient().get(pathOf(moneyOperations.netWorthRead), {
48629
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.332.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",