@slothmoney/agent-cli 0.8.0 → 0.9.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.1 - 2026-08-15
6
+
7
+ - Clarify how personal and joint category assignments appear in transaction
8
+ results and how assignment scope affects uncategorised filters.
9
+ - Document complete category and line-item assignments, including the
10
+ category-specific `Other` fallback when no more specific line item fits.
11
+ - Extend command-help and clean-installed package checks for the categorisation
12
+ workflow.
13
+
14
+ ## 0.9.0 - 2026-08-12
15
+
16
+ - Manage owned manual balance and manual transaction accounts through partial
17
+ account updates, including metadata, ownership, balance-only settings, and
18
+ goal-savings membership where supported.
19
+ - Preview or apply idempotent manual account archival while retaining the
20
+ underlying account, transaction, import, balance, and categorisation records.
21
+ - Keep account writes local-only by default and require `--apply` before any
22
+ authenticated PATCH or DELETE request is sent.
23
+
5
24
  ## 0.8.0 - 2026-08-12
6
25
 
7
26
  - Read each goal's one-based priority and move one goal to a new position with
package/README.md CHANGED
@@ -15,7 +15,7 @@ sloth-agent --version
15
15
  For a one-off pinned run:
16
16
 
17
17
  ```bash
18
- npm exec --yes --package=@slothmoney/agent-cli@0.8.0 -- sloth-agent --help
18
+ npm exec --yes --package=@slothmoney/agent-cli@0.9.1 -- sloth-agent --help
19
19
  ```
20
20
 
21
21
  ## Authenticate
@@ -27,7 +27,7 @@ where the CLI runs.
27
27
  New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
28
28
  `budget`, `categories`, `transactions`, and `goals` list. Enable **Allow changes** when
29
29
  creating the token only if the CLI must apply assignments, manage categories
30
- or line items, update planned budgets, change goal-savings account membership, ask a partner for an explanation, or manage goals. Token
30
+ or line items, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
31
31
  permissions cannot be changed later - revoke and reissue the token instead.
32
32
 
33
33
  ### Local computer
@@ -115,6 +115,34 @@ sloth-agent goals update --help
115
115
  sloth-agent ask-partner --help
116
116
  ```
117
117
 
118
+ ### How transaction categorisation is represented
119
+
120
+ Personal and joint category assignments are separate. A personal assignment
121
+ uses the transaction's top-level `categoryId`, `lineItemId`, and
122
+ `categorySplits`. A joint-budget assignment uses the corresponding fields under
123
+ `jointBudgetContribution`.
124
+
125
+ A transaction can have no personal category while its joint-budget contribution
126
+ already has a category and line item. To assess a transaction's categorisation,
127
+ inspect both locations. An included joint contribution with a category and line
128
+ item is already categorised for the joint budget. For example:
129
+
130
+ ```json
131
+ {
132
+ "categoryId": null,
133
+ "lineItemId": null,
134
+ "jointBudgetContribution": {
135
+ "included": true,
136
+ "categoryId": "groceries",
137
+ "lineItemId": "joint-groceries"
138
+ }
139
+ }
140
+ ```
141
+
142
+ This transaction is uncategorised personally but categorised as Groceries for
143
+ the joint budget. The `--uncategorized` filter applies to the selected
144
+ assignment scope; personal is used when `--assignment-scope` is omitted.
145
+
118
146
  ### Categorise a transaction end to end
119
147
 
120
148
  1. Read categories and available budget line items:
@@ -127,23 +155,43 @@ A category is the broader parent. A line item is a child within one category.
127
155
  Line-item names such as `Other` may repeat, so preserve the full choice as
128
156
  `(scope, categoryId, lineItemId)`. Use the personal or joint line-item map that
129
157
  matches the transaction scope. For example, `Bills → Other` and `Subscriptions
130
- → Other` are different choices.
158
+ → Other` are different choices. Choose the most specific suitable line item;
159
+ if none fits, use that category's `Other` line item. Historical assignments
160
+ without a line item should not be treated as a recommendation to omit one.
131
161
 
132
162
  2. Read uncategorised transactions:
133
163
 
134
164
  ```bash
135
- sloth-agent transactions --uncategorized --limit 50
165
+ sloth-agent transactions \
166
+ --assignment-scope personal \
167
+ --uncategorized \
168
+ --limit 50
169
+ ```
170
+
171
+ To read uncategorised joint-budget contributions instead:
172
+
173
+ ```bash
174
+ sloth-agent transactions \
175
+ --assignment-scope joint \
176
+ --uncategorized \
177
+ --limit 50
136
178
  ```
137
179
 
138
- 3. Copy the exact `transactionRef` for the transaction and a `categoryId` from
139
- the earlier outputs into `assignments.json`:
180
+ The remaining example continues with a personal assignment. For a joint
181
+ assignment, set `"assignmentScope": "joint"` in the assignment payload and use
182
+ `--assignment-scope joint` when checking the result.
183
+
184
+ 3. Copy the exact `transactionRef`, `categoryId`, and `lineItemId` from the
185
+ earlier outputs into `assignments.json`:
140
186
 
141
187
  ```json
142
188
  {
143
189
  "assignments": [
144
190
  {
145
191
  "transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",
146
- "categoryId": "PASTE_A_CATEGORY_ID_HERE"
192
+ "assignmentScope": "personal",
193
+ "categoryId": "PASTE_A_CATEGORY_ID_HERE",
194
+ "lineItemId": "PASTE_A_LINE_ITEM_ID_HERE"
147
195
  }
148
196
  ]
149
197
  }
@@ -172,17 +220,19 @@ This step requires a token created with **Allow changes**.
172
220
 
173
221
  Inspect every item in the returned `succeeded` and `failed` arrays.
174
222
 
175
- 6. Check the result. Successful assignments update the category and optional
176
- budget line item on the original transaction. See the result in **Sloth
177
- Money Transactions**, or re-run the original transaction query without
178
- `--uncategorized` and inspect its category fields:
223
+ 6. Check the result in the same assignment scope that you changed. Successful
224
+ assignments update the category and optional budget line item on the
225
+ original transaction. See the result in **Sloth Money Transactions**, or
226
+ re-run the original transaction query without `--uncategorized` and inspect
227
+ both the personal and joint category fields:
179
228
 
180
229
  ```bash
181
- sloth-agent transactions --limit 50
230
+ sloth-agent transactions --assignment-scope personal --limit 50
182
231
  ```
183
232
 
184
233
  The transaction should also disappear from the matching `--uncategorized`
185
- query. Assignments do not create a separate list.
234
+ query. Confirm that an existing assignment in the other scope was not changed.
235
+ Assignments do not create a separate list.
186
236
 
187
237
  ### Other workflows
188
238
 
@@ -305,21 +355,38 @@ Missing values are JSON
305
355
  are excluded, while enabled shared joint accounts follow Sloth's existing
306
356
  visibility rules.
307
357
 
308
- Goal-savings changes are previews unless `--apply` is present. Only
309
- caller-owned connected accounts can be changed; partner-owned shared accounts
310
- and fixed manual accounts return an explanatory error.
358
+ Account changes are previews unless `--apply` is present. Connected accounts
359
+ support only goal-savings membership. Manual current accounts support their
360
+ institution, name, currency, and ownership. Manual balance accounts also
361
+ support balance, Savings/Investments type, and goal-savings membership.
362
+ Partner-owned shared accounts return an explanatory error.
311
363
 
312
364
  ```bash
313
365
  sloth-agent accounts update \
314
366
  --account-ref sloth_account_v1_... \
315
- --goal-savings-source true
367
+ --institution-name "Hargreaves Lansdown" \
368
+ --account-name "Stocks & Shares ISA" \
369
+ --currency GBP \
370
+ --ownership individual \
371
+ --balance-amount 12500.75 \
372
+ --account-type investments \
373
+ --goal-savings-source false
316
374
 
317
375
  sloth-agent accounts update \
318
376
  --account-ref sloth_account_v1_... \
319
- --goal-savings-source true \
377
+ --goal-savings-source false \
320
378
  --apply
321
379
  ```
322
380
 
381
+ Archive an owned manual account. The account disappears from active Sloth
382
+ surfaces, but its underlying records are retained. Repeating an applied removal
383
+ is safe and returns `changed: false`.
384
+
385
+ ```bash
386
+ sloth-agent accounts remove --account-ref sloth_account_v1_...
387
+ sloth-agent accounts remove --account-ref sloth_account_v1_... --apply
388
+ ```
389
+
323
390
  Read linked investment accounts and their cached holdings:
324
391
 
325
392
  ```bash
package/dist/args.js CHANGED
@@ -242,6 +242,19 @@ function parseAccountRef(value) {
242
242
  }
243
243
  return value;
244
244
  }
245
+ function parseAccountName(value, option) {
246
+ const name = value.trim();
247
+ if (name.length > 300)
248
+ throw new UsageError(`${option} must be at most 300 characters`);
249
+ return name;
250
+ }
251
+ function parseAccountBalance(value) {
252
+ const amount = Number(value);
253
+ if (!/^\d+(?:\.\d+)?$/.test(value) || !Number.isFinite(amount) || amount < 0) {
254
+ throw new UsageError('--balance-amount must be a nonnegative amount');
255
+ }
256
+ return amount;
257
+ }
245
258
  function parseAccounts(args, baseUrl) {
246
259
  const subcommand = args.shift();
247
260
  if (subcommand === undefined || subcommand === 'list') {
@@ -250,15 +263,71 @@ function parseAccounts(args, baseUrl) {
250
263
  return withBaseUrl({ command: 'accounts' }, baseUrl);
251
264
  }
252
265
  if (subcommand === 'update') {
253
- const { values, apply } = parseNamedOptions(args, 'accounts update', new Set(['--account-ref', '--goal-savings-source']));
254
- const source = requiredOption(values, '--goal-savings-source', 'accounts update');
255
- if (source !== 'true' && source !== 'false') {
266
+ const { values, apply } = parseNamedOptions(args, 'accounts update', new Set([
267
+ '--account-ref',
268
+ '--institution-name',
269
+ '--account-name',
270
+ '--currency',
271
+ '--ownership',
272
+ '--balance-amount',
273
+ '--account-type',
274
+ '--goal-savings-source',
275
+ ]));
276
+ const institutionName = values.get('--institution-name');
277
+ const accountName = values.get('--account-name');
278
+ const currencyValue = values.get('--currency');
279
+ const ownershipValue = values.get('--ownership');
280
+ const balanceValue = values.get('--balance-amount');
281
+ const accountTypeValue = values.get('--account-type');
282
+ const sourceValue = values.get('--goal-savings-source');
283
+ if (currencyValue !== undefined && !/^[A-Za-z]{3}$/.test(currencyValue)) {
284
+ throw new UsageError('--currency must be a three-letter currency code');
285
+ }
286
+ if (ownershipValue !== undefined
287
+ && ownershipValue !== 'individual'
288
+ && ownershipValue !== 'joint') {
289
+ throw new UsageError('--ownership must be individual or joint');
290
+ }
291
+ if (accountTypeValue !== undefined
292
+ && accountTypeValue !== 'savings'
293
+ && accountTypeValue !== 'investments') {
294
+ throw new UsageError('--account-type must be savings or investments');
295
+ }
296
+ if (sourceValue !== undefined && sourceValue !== 'true' && sourceValue !== 'false') {
256
297
  throw new UsageError('--goal-savings-source must be true or false');
257
298
  }
299
+ const update = {
300
+ ...(institutionName === undefined
301
+ ? {}
302
+ : { institutionName: parseAccountName(institutionName, '--institution-name') }),
303
+ ...(accountName === undefined
304
+ ? {}
305
+ : { accountName: parseAccountName(accountName, '--account-name') }),
306
+ ...(currencyValue === undefined ? {} : { currency: currencyValue.toUpperCase() }),
307
+ ...(ownershipValue === undefined
308
+ ? {}
309
+ : { ownership: ownershipValue === 'individual' ? 'personal' : 'joint' }),
310
+ ...(balanceValue === undefined ? {} : { balanceAmount: parseAccountBalance(balanceValue) }),
311
+ ...(accountTypeValue === undefined
312
+ ? {}
313
+ : { accountType: accountTypeValue }),
314
+ ...(sourceValue === undefined ? {} : { isGoalSavingsSource: sourceValue === 'true' }),
315
+ };
316
+ if (Object.keys(update).length === 0) {
317
+ throw new UsageError('accounts update requires at least one field to update');
318
+ }
258
319
  return withBaseUrl({
259
320
  command: 'accounts-update',
260
321
  accountRef: parseAccountRef(requiredOption(values, '--account-ref', 'accounts update')),
261
- isGoalSavingsSource: source === 'true',
322
+ update,
323
+ apply,
324
+ }, baseUrl);
325
+ }
326
+ if (subcommand === 'remove') {
327
+ const { values, apply } = parseNamedOptions(args, 'accounts remove', new Set(['--account-ref']));
328
+ return withBaseUrl({
329
+ command: 'accounts-remove',
330
+ accountRef: parseAccountRef(requiredOption(values, '--account-ref', 'accounts remove')),
262
331
  apply,
263
332
  }, baseUrl);
264
333
  }
@@ -657,6 +726,8 @@ function helpTopic(argv) {
657
726
  || command === 'ask-partner') {
658
727
  if (command === 'accounts' && subcommand === 'update')
659
728
  return 'accounts-update';
729
+ if (command === 'accounts' && subcommand === 'remove')
730
+ return 'accounts-remove';
660
731
  return command;
661
732
  }
662
733
  if (command === 'investments')
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { ICON_KEYS } from './category-metadata.js';
4
4
  import { parseApiResponse, validateAssignmentPayload, validateBudgetUpdatePayload, } from './contracts.js';
5
5
  import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
6
6
  import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
7
- export const CLI_VERSION = '0.8.0';
7
+ export const CLI_VERSION = '0.9.1';
8
8
  const REQUEST_TIMEOUT_MS = 60_000;
9
9
  const API_ORIGIN_HELP_LINES = [
10
10
  '',
@@ -26,7 +26,8 @@ export function usageText() {
26
26
  ' sloth-agent auth status [--base-url URL]',
27
27
  ' sloth-agent auth logout [--base-url URL]',
28
28
  ' sloth-agent accounts [list] [--base-url URL]',
29
- ' sloth-agent accounts update --account-ref REF --goal-savings-source true|false [--apply]',
29
+ ' sloth-agent accounts update --account-ref REF [fields] [--apply]',
30
+ ' sloth-agent accounts remove --account-ref REF [--apply]',
30
31
  ' sloth-agent investments [--account-ref REF] [--base-url URL]',
31
32
  ' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
32
33
  ' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
@@ -184,6 +185,8 @@ export function categoriesHelpText() {
184
185
  ' Line-item names such as "Other" may repeat. Preserve the full choice as',
185
186
  ' (scope, categoryId, lineItemId), using the personal or joint line-item',
186
187
  ' map matching the transaction scope.',
188
+ ' Choose the most specific suitable line item. If none fits, use that',
189
+ ' category\'s "Other" line item.',
187
190
  '',
188
191
  'Examples:',
189
192
  ' sloth-agent categories',
@@ -308,19 +311,29 @@ export function accountsUpdateHelpText() {
308
311
  return [
309
312
  'Sloth Agent CLI — accounts update',
310
313
  '',
311
- 'Preview or update whether an owned connected account is used for goal savings.',
314
+ 'Preview or update an owned account. Manual accounts support their editable fields.',
312
315
  '',
313
316
  'Usage:',
314
- ' sloth-agent accounts update --account-ref REF --goal-savings-source true|false [--apply] [--base-url URL]',
317
+ ' sloth-agent accounts update --account-ref REF [fields] [--apply] [--base-url URL]',
315
318
  '',
316
- 'Required inputs:',
319
+ 'Required input:',
317
320
  ' --account-ref REF Opaque accountRef from sloth-agent accounts.',
318
- ' --goal-savings-source true|false Enable or disable goal-savings membership.',
321
+ '',
322
+ 'Update fields (at least one):',
323
+ ' --institution-name NAME Manual account institution.',
324
+ ' --account-name NAME Manual account name.',
325
+ ' --currency CODE Three-letter currency code.',
326
+ ' --ownership individual|joint Manual account ownership.',
327
+ ' --balance-amount AMOUNT Balance-only account balance.',
328
+ ' --account-type savings|investments Balance-only account type.',
329
+ ' --goal-savings-source true|false Goal-savings membership.',
319
330
  '',
320
331
  'Write behavior:',
321
332
  ' Without --apply, returns a JSON preview without credentials or a network request.',
322
333
  ' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
323
- ' Partner-owned shared accounts and manual accounts cannot be changed.',
334
+ ' Connected accounts support only --goal-savings-source.',
335
+ ' Manual current accounts cannot change type, balance, or goal-savings membership.',
336
+ ' Partner-owned shared accounts cannot be changed.',
324
337
  ' Unknown, disconnected, or inaccessible references return Account not found.',
325
338
  ...API_ORIGIN_HELP_LINES,
326
339
  '',
@@ -329,6 +342,30 @@ export function accountsUpdateHelpText() {
329
342
  ' Apply mode returns changed and the complete persisted account.',
330
343
  ].join('\n');
331
344
  }
345
+ export function accountsRemoveHelpText() {
346
+ return [
347
+ 'Sloth Agent CLI — accounts remove',
348
+ '',
349
+ 'Preview or archive an owned manual account while retaining its underlying records.',
350
+ '',
351
+ 'Usage:',
352
+ ' sloth-agent accounts remove --account-ref REF [--apply] [--base-url URL]',
353
+ '',
354
+ 'Required input:',
355
+ ' --account-ref REF Opaque accountRef from sloth-agent accounts.',
356
+ '',
357
+ 'Write behavior:',
358
+ ' Without --apply, returns a JSON preview without credentials or a network request.',
359
+ ' With --apply, requires agent:write and archives the manual account.',
360
+ ' Connected and partner-owned accounts cannot be removed.',
361
+ ' Repeating an applied removal succeeds with changed false.',
362
+ ...API_ORIGIN_HELP_LINES,
363
+ '',
364
+ 'Output:',
365
+ ' Preview mode returns dryRun, method, and endpoint.',
366
+ ' Apply mode returns removed, changed, and accountRef.',
367
+ ].join('\n');
368
+ }
332
369
  export function investmentsHelpText() {
333
370
  return [
334
371
  'Sloth Agent CLI — investments',
@@ -428,7 +465,8 @@ export function transactionsHelpText() {
428
465
  ' sloth-agent transactions [options]',
429
466
  '',
430
467
  'Options:',
431
- ' --uncategorized[=true|false] Optional. Filter by state; with no value, use true.',
468
+ ' --uncategorized[=true|false] Optional. Filter the selected assignment scope by state;',
469
+ ' with no value, use true.',
432
470
  ' --limit N Optional. Integer from 1 to 200; omit for API default.',
433
471
  ' --start-date YYYY-MM-DD Optional. Include transactions on or after this date.',
434
472
  ' --end-date YYYY-MM-DD Optional. Include transactions on or before this date.',
@@ -437,6 +475,7 @@ export function transactionsHelpText() {
437
475
  ' --category-id ID Optional. Filter by category ID.',
438
476
  ' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
439
477
  ' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
478
+ ' Personal is used when omitted.',
440
479
  ' --cursor CURSOR Optional. Continue from a previous nextCursor.',
441
480
  ' --base-url URL Optional. Override the API origin.',
442
481
  ' -h, --help Show this help.',
@@ -452,6 +491,10 @@ export function transactionsHelpText() {
452
491
  'Output:',
453
492
  ' JSON containing transactions, nextCursor, and structured refresh status.',
454
493
  ' Refresh failures do not hide readable cached transactions.',
494
+ ' Personal assignments use the top-level categoryId, lineItemId, and categorySplits.',
495
+ ' Joint assignments appear under jointBudgetContribution.',
496
+ ' A transaction can be uncategorised personally while its joint-budget contribution',
497
+ ' is already categorised. To assess its categorisation, inspect both locations.',
455
498
  ' Use nextCursor with --cursor',
456
499
  ' to request the next page. A null nextCursor means there are no more pages.',
457
500
  '',
@@ -494,29 +537,34 @@ export function assignHelpText() {
494
537
  ' categories output. The example values below are placeholders.',
495
538
  ' Set categoryId to null to clear an assignment.',
496
539
  ' lineItemId is optional and accepts a non-empty string or null.',
540
+ ' When the category has line items, choose the most specific suitable line item.',
541
+ ' If none fits, use that category\'s Other line item from the matching scope.',
497
542
  ' categorySplits is optional and accepts a non-empty array or null.',
498
543
  ' Each split requires categoryId and a positive integer amountPence;',
499
544
  ' a split lineItemId is optional.',
500
545
  ' incomeSubtype is optional and accepts "pay", "interest", or null.',
501
546
  ' assignmentScope is optional and accepts "personal" or "joint".',
547
+ ' Personal is used when assignmentScope is omitted.',
502
548
  '',
503
549
  'Workflow:',
504
550
  ' sloth-agent categories',
505
- ' sloth-agent transactions --uncategorized --limit 50',
551
+ ' sloth-agent transactions --assignment-scope personal --uncategorized --limit 50',
506
552
  ' sloth-agent assign --input assignments.json Preview only',
507
553
  ' sloth-agent assign --input assignments.json --apply Write assignments',
508
- ' sloth-agent transactions --limit 50 Read back the result',
554
+ ' sloth-agent transactions --assignment-scope personal --limit 50 Read back',
509
555
  '',
510
556
  'Example:',
511
557
  ' {',
512
558
  ' "assignments": [',
513
559
  ' {',
514
560
  ' "transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",',
515
- ' "categoryId": "PASTE_A_CATEGORY_ID_HERE"',
561
+ ' "assignmentScope": "personal",',
562
+ ' "categoryId": "PASTE_A_CATEGORY_ID_HERE",',
563
+ ' "lineItemId": "PASTE_A_LINE_ITEM_ID_HERE"',
516
564
  ' }',
517
565
  ' ]',
518
566
  ' }',
519
- ' These are placeholders. Replace both values with exact IDs from CLI output.',
567
+ ' These are placeholders. Replace all three values with exact IDs from CLI output.',
520
568
  '',
521
569
  'Output:',
522
570
  ' Preview mode returns dryRun, endpoint, and the validated payload.',
@@ -711,6 +759,7 @@ export function commandHelpText(topic) {
711
759
  'auth-logout': authLogoutHelpText,
712
760
  accounts: accountsHelpText,
713
761
  'accounts-update': accountsUpdateHelpText,
762
+ 'accounts-remove': accountsRemoveHelpText,
714
763
  investments: investmentsHelpText,
715
764
  budget: budgetHelpText,
716
765
  'budget-update': budgetUpdateHelpText,
@@ -998,7 +1047,16 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
998
1047
  dryRun: true,
999
1048
  endpoint,
1000
1049
  method: 'PATCH',
1001
- payload: { isGoalSavingsSource: parsed.isGoalSavingsSource },
1050
+ payload: parsed.update,
1051
+ });
1052
+ return 0;
1053
+ }
1054
+ if (parsed.command === 'accounts-remove' && !parsed.apply) {
1055
+ const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
1056
+ writeJson(writeStdout, {
1057
+ dryRun: true,
1058
+ endpoint,
1059
+ method: 'DELETE',
1002
1060
  });
1003
1061
  return 0;
1004
1062
  }
@@ -1023,11 +1081,21 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1023
1081
  const headers = requestHeaders(token);
1024
1082
  if (parsed.command === 'accounts-update') {
1025
1083
  const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
1026
- const payload = { isGoalSavingsSource: parsed.isGoalSavingsSource };
1027
1084
  const response = await fetchImplementation(endpoint, {
1028
1085
  method: 'PATCH',
1029
1086
  headers: { ...headers, 'Content-Type': 'application/json' },
1030
- body: JSON.stringify(payload),
1087
+ body: JSON.stringify(parsed.update),
1088
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1089
+ });
1090
+ const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
1091
+ writeJson(writeStdout, data);
1092
+ return 0;
1093
+ }
1094
+ if (parsed.command === 'accounts-remove') {
1095
+ const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
1096
+ const response = await fetchImplementation(endpoint, {
1097
+ method: 'DELETE',
1098
+ headers,
1031
1099
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1032
1100
  });
1033
1101
  const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
package/dist/contracts.js CHANGED
@@ -423,6 +423,14 @@ function isAccountMutationResponse(value) {
423
423
  && typeof value.changed === 'boolean'
424
424
  && isAccount(value.account));
425
425
  }
426
+ function isAccountRemovalResponse(value) {
427
+ return (isObject(value)
428
+ && hasOnlyFields(value, ['removed', 'changed', 'accountRef'])
429
+ && value.removed === true
430
+ && typeof value.changed === 'boolean'
431
+ && typeof value.accountRef === 'string'
432
+ && /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef));
433
+ }
426
434
  function isInvestmentHolding(value) {
427
435
  return (isObject(value)
428
436
  && hasOnlyFields(value, [
@@ -492,27 +500,29 @@ export function parseApiResponse(command, value) {
492
500
  ? isAccountsResponse(value)
493
501
  : command === 'accounts-update'
494
502
  ? isAccountMutationResponse(value)
495
- : command === 'investments'
496
- ? isInvestmentsResponse(value)
497
- : command === 'budget' || command === 'budget-update'
498
- ? isBudgetResponse(value)
499
- : command === 'categories'
500
- ? isCategoryResponse(value)
501
- : command === 'categories-create' || command === 'categories-rename'
502
- ? isCategoryMutationResponse(value)
503
- : command === 'line-items-create' || command === 'line-items-rename'
504
- ? isLineItemMutationResponse(value)
505
- : command === 'transactions'
506
- ? isTransactionsResponse(value)
507
- : command === 'assign'
508
- ? isAssignmentResponse(value)
509
- : command === 'ask-partner'
510
- ? isPartnerResponse(value)
511
- : command === 'goals-list'
512
- ? isGoalsResponse(value)
513
- : command === 'goals-delete'
514
- ? isGoalDeleteResponse(value)
515
- : isGoalMutationResponse(value);
503
+ : command === 'accounts-remove'
504
+ ? isAccountRemovalResponse(value)
505
+ : command === 'investments'
506
+ ? isInvestmentsResponse(value)
507
+ : command === 'budget' || command === 'budget-update'
508
+ ? isBudgetResponse(value)
509
+ : command === 'categories'
510
+ ? isCategoryResponse(value)
511
+ : command === 'categories-create' || command === 'categories-rename'
512
+ ? isCategoryMutationResponse(value)
513
+ : command === 'line-items-create' || command === 'line-items-rename'
514
+ ? isLineItemMutationResponse(value)
515
+ : command === 'transactions'
516
+ ? isTransactionsResponse(value)
517
+ : command === 'assign'
518
+ ? isAssignmentResponse(value)
519
+ : command === 'ask-partner'
520
+ ? isPartnerResponse(value)
521
+ : command === 'goals-list'
522
+ ? isGoalsResponse(value)
523
+ : command === 'goals-delete'
524
+ ? isGoalDeleteResponse(value)
525
+ : isGoalMutationResponse(value);
516
526
  if (!valid) {
517
527
  const label = command === 'assign' ? 'assignment' : command;
518
528
  throw new ApiError(`Invalid ${label} response from the Agent API`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slothmoney/agent-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Command-line access to the Sloth Money Agent API.",
5
5
  "type": "module",
6
6
  "bin": {