@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 +19 -0
- package/README.md +85 -18
- package/dist/args.js +75 -4
- package/dist/cli.js +83 -15
- package/dist/contracts.js +31 -21
- package/package.json +1 -1
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
"
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`--uncategorized` and inspect
|
|
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.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
--
|
|
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
|
|
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([
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
317
|
+
' sloth-agent accounts update --account-ref REF [fields] [--apply] [--base-url URL]',
|
|
315
318
|
'',
|
|
316
|
-
'Required
|
|
319
|
+
'Required input:',
|
|
317
320
|
' --account-ref REF Opaque accountRef from sloth-agent accounts.',
|
|
318
|
-
'
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
-
' "
|
|
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
|
|
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:
|
|
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(
|
|
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 === '
|
|
496
|
-
?
|
|
497
|
-
: command === '
|
|
498
|
-
?
|
|
499
|
-
: command === '
|
|
500
|
-
?
|
|
501
|
-
: command === 'categories
|
|
502
|
-
?
|
|
503
|
-
: command === '
|
|
504
|
-
?
|
|
505
|
-
: command === '
|
|
506
|
-
?
|
|
507
|
-
: command === '
|
|
508
|
-
?
|
|
509
|
-
: command === '
|
|
510
|
-
?
|
|
511
|
-
: command === '
|
|
512
|
-
?
|
|
513
|
-
: command === 'goals-
|
|
514
|
-
?
|
|
515
|
-
:
|
|
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`);
|