@slothmoney/agent-cli 0.3.0 → 0.4.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.
- package/CHANGELOG.md +15 -0
- package/README.md +54 -12
- package/dist/args.js +8 -39
- package/dist/cli.js +47 -75
- package/dist/contracts.js +86 -28
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Add the read-only `sloth-agent accounts` command with strict runtime
|
|
6
|
+
validation for opaque references, ownership, native balances, sources, and
|
|
7
|
+
freshness metadata.
|
|
8
|
+
- Prepare 0.4.0 by removing the obsolete `joint-budget-settings` command. Shared
|
|
9
|
+
personal transactions now enter the joint budget through their assignment.
|
|
10
|
+
- Verify each trusted npm release from a fresh temporary directory so the
|
|
11
|
+
registry smoke test cannot resolve a repository-local CLI executable.
|
|
12
|
+
|
|
13
|
+
## 0.3.1 - 2026-07-31
|
|
14
|
+
|
|
15
|
+
- Coordinate transaction reads with the Sloth Budget daily refresh process.
|
|
16
|
+
- Wait up to 45 seconds for fresh persisted data, then return readable cached
|
|
17
|
+
transactions with structured refresh status when work continues or fails.
|
|
18
|
+
- Validate the additive transaction refresh response contract.
|
|
19
|
+
|
|
5
20
|
## 0.3.0 - 2026-07-30
|
|
6
21
|
|
|
7
22
|
- Add command-specific help for every command and auth subcommand, including
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to manage goals and categorise transactions through the
|
|
3
|
+
Use your own agent to inspect known accounts, manage goals, and categorise transactions through the
|
|
4
4
|
[Sloth Money Agent API](https://slothmoney.app/developers/).
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -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.4.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -96,10 +96,10 @@ options, output, and examples:
|
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
98
|
sloth-agent auth login --help
|
|
99
|
+
sloth-agent accounts --help
|
|
99
100
|
sloth-agent categories --help
|
|
100
101
|
sloth-agent transactions --help
|
|
101
102
|
sloth-agent assign --help
|
|
102
|
-
sloth-agent joint-budget-settings --help
|
|
103
103
|
sloth-agent goals create --help
|
|
104
104
|
sloth-agent goals update --help
|
|
105
105
|
sloth-agent ask-partner --help
|
|
@@ -174,6 +174,20 @@ query. Assignments do not create a separate list.
|
|
|
174
174
|
|
|
175
175
|
### Other workflows
|
|
176
176
|
|
|
177
|
+
Read the existing Sloth account inventory:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
sloth-agent accounts
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The command is read-only and cache-only: it does not refresh linked banks or
|
|
184
|
+
change account data. Each result contains an opaque `accountRef`, personal or
|
|
185
|
+
joint ownership, connected or manual source, native balance/currency when
|
|
186
|
+
known, `lastBalanceUpdatedAt`, and `connectionState`. Missing values are JSON
|
|
187
|
+
`null`; currencies are never converted or combined. Partner personal accounts
|
|
188
|
+
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
189
|
+
visibility rules.
|
|
190
|
+
|
|
177
191
|
List your goals:
|
|
178
192
|
|
|
179
193
|
```bash
|
|
@@ -221,19 +235,35 @@ Read uncategorised contributions to the joint budget:
|
|
|
221
235
|
sloth-agent transactions --assignment-scope joint --uncategorized
|
|
222
236
|
```
|
|
223
237
|
|
|
238
|
+
The first transaction read after the UTC day changes may refresh linked bank
|
|
239
|
+
data. The CLI waits up to 45 seconds for that refresh to persist, then returns
|
|
240
|
+
the requested booked transactions. If the refresh is still running, partially
|
|
241
|
+
fails, or fails globally, readable cached transactions are still returned with
|
|
242
|
+
a structured `refresh` object:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"refresh": {
|
|
247
|
+
"status": "in_progress",
|
|
248
|
+
"reason": "wait_timeout",
|
|
249
|
+
"utcDate": "2026-07-31"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Re-run the transaction query later to observe the completed refresh. A partial
|
|
255
|
+
account failure remains eligible for an automatic retry.
|
|
256
|
+
|
|
224
257
|
Set `"assignmentScope": "joint"` on an assignment to categorise the eligible
|
|
225
258
|
shared portion for the joint budget.
|
|
226
259
|
|
|
227
|
-
|
|
228
|
-
|
|
260
|
+
Transaction reads expose `personalBudgetAmountPence` for the caller's explicit
|
|
261
|
+
personal-only portion and `jointBudgetContribution.amountPence` for the full
|
|
262
|
+
shared portion. The 60/40 settlement ratio does not reduce joint-budget spend.
|
|
229
263
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
sloth-agent joint-budget-settings \
|
|
234
|
-
--include-shared-personal-transactions=true \
|
|
235
|
-
--apply
|
|
236
|
-
```
|
|
264
|
+
Shared personal-account transactions with a joint assignment are included in
|
|
265
|
+
the joint budget automatically. The settlement ratio remains independent from
|
|
266
|
+
the amount attributed to the joint budget.
|
|
237
267
|
|
|
238
268
|
Create a partner clarification link:
|
|
239
269
|
|
|
@@ -277,3 +307,15 @@ npm run verify
|
|
|
277
307
|
|
|
278
308
|
`npm run test:package` packs the exact npm artifact, installs it into a clean
|
|
279
309
|
temporary project, and runs the installed binary.
|
|
310
|
+
|
|
311
|
+
## Releasing
|
|
312
|
+
|
|
313
|
+
Releases are published only through the trusted `Publish npm release` GitHub
|
|
314
|
+
workflow from a reviewed `v*` tag whose version matches `package.json`. The
|
|
315
|
+
workflow runs the full verification suite before publishing.
|
|
316
|
+
|
|
317
|
+
After npm accepts the package, the workflow verifies the exact published
|
|
318
|
+
version with `npm run test:registry -- VERSION`. That script runs `npm exec`
|
|
319
|
+
from a fresh temporary directory with an isolated npm cache, so a checkout's
|
|
320
|
+
older local `sloth-agent` executable cannot satisfy the registry smoke test.
|
|
321
|
+
The temporary directory is removed after the check.
|
package/dist/args.js
CHANGED
|
@@ -411,10 +411,10 @@ function helpTopic(argv) {
|
|
|
411
411
|
return 'goals-delete';
|
|
412
412
|
return 'goals';
|
|
413
413
|
}
|
|
414
|
-
if (command === '
|
|
414
|
+
if (command === 'accounts'
|
|
415
|
+
|| command === 'categories'
|
|
415
416
|
|| command === 'transactions'
|
|
416
417
|
|| command === 'assign'
|
|
417
|
-
|| command === 'joint-budget-settings'
|
|
418
418
|
|| command === 'ask-partner') {
|
|
419
419
|
return command;
|
|
420
420
|
}
|
|
@@ -443,6 +443,12 @@ export function parseArgs(argv) {
|
|
|
443
443
|
}
|
|
444
444
|
return withBaseUrl({ command }, baseUrl);
|
|
445
445
|
}
|
|
446
|
+
if (command === 'accounts') {
|
|
447
|
+
if (args.length > 0) {
|
|
448
|
+
throw new UsageError(`Unknown accounts option: ${args[0]}`);
|
|
449
|
+
}
|
|
450
|
+
return withBaseUrl({ command }, baseUrl);
|
|
451
|
+
}
|
|
446
452
|
if (command === 'transactions') {
|
|
447
453
|
return withBaseUrl({ command, filters: parseTransactions(args) }, baseUrl);
|
|
448
454
|
}
|
|
@@ -471,43 +477,6 @@ export function parseArgs(argv) {
|
|
|
471
477
|
throw new UsageError('assign requires --input <file>');
|
|
472
478
|
return withBaseUrl({ command, input, apply }, baseUrl);
|
|
473
479
|
}
|
|
474
|
-
if (command === 'joint-budget-settings') {
|
|
475
|
-
let includeSharedPersonalTransactions;
|
|
476
|
-
let apply = false;
|
|
477
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
478
|
-
const argument = args[index];
|
|
479
|
-
if (argument === '--apply') {
|
|
480
|
-
if (apply)
|
|
481
|
-
throw new UsageError('--apply may only be provided once');
|
|
482
|
-
apply = true;
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
const optionName = '--include-shared-personal-transactions';
|
|
486
|
-
if (argument === optionName || argument.startsWith(`${optionName}=`)) {
|
|
487
|
-
const value = argument === optionName
|
|
488
|
-
? readOptionValue(args, index, optionName)
|
|
489
|
-
: argument.slice(`${optionName}=`.length);
|
|
490
|
-
if (argument === optionName)
|
|
491
|
-
index += 1;
|
|
492
|
-
if (value !== 'true' && value !== 'false') {
|
|
493
|
-
throw new UsageError(`${optionName} must be true or false`);
|
|
494
|
-
}
|
|
495
|
-
includeSharedPersonalTransactions = setOnce(includeSharedPersonalTransactions, value === 'true', optionName);
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
throw new UsageError(`Unknown joint-budget-settings option: ${argument}`);
|
|
499
|
-
}
|
|
500
|
-
if (apply && includeSharedPersonalTransactions === undefined) {
|
|
501
|
-
throw new UsageError('--apply requires --include-shared-personal-transactions=true|false');
|
|
502
|
-
}
|
|
503
|
-
return withBaseUrl({
|
|
504
|
-
command,
|
|
505
|
-
...(includeSharedPersonalTransactions === undefined
|
|
506
|
-
? {}
|
|
507
|
-
: { includeSharedPersonalTransactions }),
|
|
508
|
-
apply,
|
|
509
|
-
}, baseUrl);
|
|
510
|
-
}
|
|
511
480
|
if (command === 'ask-partner') {
|
|
512
481
|
let transactionRef;
|
|
513
482
|
for (let index = 0; index < args.length; index += 1) {
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
|
-
import { parseApiResponse, validateAssignmentPayload,
|
|
3
|
+
import { parseApiResponse, validateAssignmentPayload, } from './contracts.js';
|
|
4
4
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
5
5
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
6
|
-
export const CLI_VERSION = '0.
|
|
7
|
-
const REQUEST_TIMEOUT_MS =
|
|
6
|
+
export const CLI_VERSION = '0.4.0';
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
8
8
|
const API_ORIGIN_HELP_LINES = [
|
|
9
9
|
'',
|
|
10
10
|
'API origin:',
|
|
@@ -24,12 +24,12 @@ export function usageText() {
|
|
|
24
24
|
' sloth-agent auth login [--token-stdin | --from-env] [--base-url URL]',
|
|
25
25
|
' sloth-agent auth status [--base-url URL]',
|
|
26
26
|
' sloth-agent auth logout [--base-url URL]',
|
|
27
|
+
' sloth-agent accounts [--base-url URL]',
|
|
27
28
|
' sloth-agent categories [--base-url URL]',
|
|
28
29
|
' sloth-agent transactions [--uncategorized[=true|false]] [--limit N]',
|
|
29
30
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
30
31
|
' [--account-id ID] [--category-id ID] [--cursor CURSOR] [--base-url URL]',
|
|
31
32
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
32
|
-
' sloth-agent joint-budget-settings [--include-shared-personal-transactions=true|false] [--apply]',
|
|
33
33
|
' sloth-agent goals [list] [--base-url URL]',
|
|
34
34
|
' sloth-agent goals create --name NAME [--target-amount AMOUNT]',
|
|
35
35
|
' [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
@@ -182,6 +182,31 @@ export function categoriesHelpText() {
|
|
|
182
182
|
' jointLineItemsByCategoryId Joint child line items keyed by category ID',
|
|
183
183
|
].join('\n');
|
|
184
184
|
}
|
|
185
|
+
export function accountsHelpText() {
|
|
186
|
+
return [
|
|
187
|
+
'Sloth Agent CLI — accounts',
|
|
188
|
+
'',
|
|
189
|
+
'Read the existing Sloth account inventory known to the authenticated user.',
|
|
190
|
+
'',
|
|
191
|
+
'Usage:',
|
|
192
|
+
' sloth-agent accounts [--base-url URL]',
|
|
193
|
+
'',
|
|
194
|
+
'Options:',
|
|
195
|
+
' --base-url URL Optional. Override the API origin.',
|
|
196
|
+
' -h, --help Show this help.',
|
|
197
|
+
...API_ORIGIN_HELP_LINES,
|
|
198
|
+
'',
|
|
199
|
+
'Access:',
|
|
200
|
+
' This command is read-only and does not refresh connected accounts.',
|
|
201
|
+
'',
|
|
202
|
+
'Output:',
|
|
203
|
+
' asOf Server response time',
|
|
204
|
+
' accounts[].accountRef Opaque stable account reference',
|
|
205
|
+
' accounts[].ownership personal or joint',
|
|
206
|
+
' accounts[].balanceAmount and currency in the native currency when known',
|
|
207
|
+
' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
185
210
|
export function transactionsHelpText() {
|
|
186
211
|
return [
|
|
187
212
|
'Sloth Agent CLI — transactions',
|
|
@@ -208,10 +233,14 @@ export function transactionsHelpText() {
|
|
|
208
233
|
'Constraints:',
|
|
209
234
|
' All filters are omitted by default.',
|
|
210
235
|
' --end-date must not be before --start-date.',
|
|
211
|
-
'
|
|
236
|
+
' The first transaction read each UTC day may refresh linked bank data.',
|
|
237
|
+
' Refresh remotely persists booked transactions and account balances.',
|
|
238
|
+
' The command waits up to 45 seconds, then returns cached data if refresh continues.',
|
|
212
239
|
'',
|
|
213
240
|
'Output:',
|
|
214
|
-
' JSON containing transactions
|
|
241
|
+
' JSON containing transactions, nextCursor, and structured refresh status.',
|
|
242
|
+
' Refresh failures do not hide readable cached transactions.',
|
|
243
|
+
' Use nextCursor with --cursor',
|
|
215
244
|
' to request the next page. A null nextCursor means there are no more pages.',
|
|
216
245
|
'',
|
|
217
246
|
'Examples:',
|
|
@@ -284,37 +313,6 @@ export function assignHelpText() {
|
|
|
284
313
|
' Assignments do not create a separate list.',
|
|
285
314
|
].join('\n');
|
|
286
315
|
}
|
|
287
|
-
export function jointBudgetSettingsHelpText() {
|
|
288
|
-
return [
|
|
289
|
-
'Sloth Agent CLI — joint-budget-settings',
|
|
290
|
-
'',
|
|
291
|
-
'Read or update whether shared personal transactions count in the linked joint budget.',
|
|
292
|
-
'',
|
|
293
|
-
'Usage:',
|
|
294
|
-
' sloth-agent joint-budget-settings [options]',
|
|
295
|
-
'',
|
|
296
|
-
'Options:',
|
|
297
|
-
' --include-shared-personal-transactions=true|false',
|
|
298
|
-
' Optional. Preview the linked setting change.',
|
|
299
|
-
' --apply Optional. Apply the previewed setting change.',
|
|
300
|
-
' --base-url URL Optional. Override the API origin.',
|
|
301
|
-
' -h, --help Show this help.',
|
|
302
|
-
...API_ORIGIN_HELP_LINES,
|
|
303
|
-
'',
|
|
304
|
-
'Safety:',
|
|
305
|
-
' With no setting option, the command is read-only.',
|
|
306
|
-
' Without --apply, a setting option returns a dry-run preview and does not write.',
|
|
307
|
-
' --apply requires an explicit true or false setting value.',
|
|
308
|
-
'',
|
|
309
|
-
'Examples:',
|
|
310
|
-
' sloth-agent joint-budget-settings',
|
|
311
|
-
' sloth-agent joint-budget-settings --include-shared-personal-transactions=true',
|
|
312
|
-
' sloth-agent joint-budget-settings --include-shared-personal-transactions=true --apply',
|
|
313
|
-
'',
|
|
314
|
-
'Output:',
|
|
315
|
-
' JSON containing the linked setting, audit metadata, or a dry-run payload.',
|
|
316
|
-
].join('\n');
|
|
317
|
-
}
|
|
318
316
|
export function goalsHelpText() {
|
|
319
317
|
return [
|
|
320
318
|
'Sloth Agent CLI — goals',
|
|
@@ -485,10 +483,10 @@ export function commandHelpText(topic) {
|
|
|
485
483
|
'auth-login': authLoginHelpText,
|
|
486
484
|
'auth-status': authStatusHelpText,
|
|
487
485
|
'auth-logout': authLogoutHelpText,
|
|
486
|
+
accounts: accountsHelpText,
|
|
488
487
|
categories: categoriesHelpText,
|
|
489
488
|
transactions: transactionsHelpText,
|
|
490
489
|
assign: assignHelpText,
|
|
491
|
-
'joint-budget-settings': jointBudgetSettingsHelpText,
|
|
492
490
|
goals: goalsHelpText,
|
|
493
491
|
'goals-list': goalsListHelpText,
|
|
494
492
|
'goals-create': goalsCreateHelpText,
|
|
@@ -843,36 +841,6 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
843
841
|
writeJson(writeStdout, data);
|
|
844
842
|
return hasFailures(data) ? 1 : 0;
|
|
845
843
|
}
|
|
846
|
-
if (parsed.command === 'joint-budget-settings') {
|
|
847
|
-
const endpoint = `${baseUrl}/api/agent/v1/joint-budget-settings`;
|
|
848
|
-
if (parsed.includeSharedPersonalTransactions !== undefined && !parsed.apply) {
|
|
849
|
-
writeJson(writeStdout, {
|
|
850
|
-
dryRun: true,
|
|
851
|
-
endpoint,
|
|
852
|
-
payload: {
|
|
853
|
-
includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
|
|
854
|
-
},
|
|
855
|
-
});
|
|
856
|
-
return 0;
|
|
857
|
-
}
|
|
858
|
-
const response = await fetchImplementation(endpoint, {
|
|
859
|
-
method: parsed.apply ? 'PUT' : 'GET',
|
|
860
|
-
headers: parsed.apply
|
|
861
|
-
? { ...headers, 'Content-Type': 'application/json' }
|
|
862
|
-
: headers,
|
|
863
|
-
...(parsed.apply
|
|
864
|
-
? {
|
|
865
|
-
body: JSON.stringify({
|
|
866
|
-
includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
|
|
867
|
-
}),
|
|
868
|
-
}
|
|
869
|
-
: {}),
|
|
870
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
871
|
-
});
|
|
872
|
-
const data = validateJointBudgetSettingsResponse(await parseHttpResponse(response, token));
|
|
873
|
-
writeJson(writeStdout, data);
|
|
874
|
-
return 0;
|
|
875
|
-
}
|
|
876
844
|
if (parsed.command === 'ask-partner') {
|
|
877
845
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
|
|
878
846
|
method: 'POST',
|
|
@@ -897,15 +865,19 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
897
865
|
writeJson(writeStdout, data);
|
|
898
866
|
return 0;
|
|
899
867
|
}
|
|
900
|
-
const path = parsed.command === '
|
|
901
|
-
? '/api/agent/v1/
|
|
902
|
-
:
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
868
|
+
const path = parsed.command === 'accounts'
|
|
869
|
+
? '/api/agent/v1/accounts'
|
|
870
|
+
: parsed.command === 'categories'
|
|
871
|
+
? '/api/agent/v1/categories'
|
|
872
|
+
: `/api/agent/v1/transactions${(() => {
|
|
873
|
+
const query = buildTransactionsQuery(parsed.filters);
|
|
874
|
+
return query ? `?${query}` : '';
|
|
875
|
+
})()}`;
|
|
906
876
|
const response = await fetchImplementation(`${baseUrl}${path}`, {
|
|
907
877
|
method: 'GET',
|
|
908
|
-
headers
|
|
878
|
+
headers: parsed.command === 'transactions'
|
|
879
|
+
? { ...headers, Prefer: 'wait=45' }
|
|
880
|
+
: headers,
|
|
909
881
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
910
882
|
});
|
|
911
883
|
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
package/dist/contracts.js
CHANGED
|
@@ -2,6 +2,12 @@ import { ApiError, UsageError, } from './errors.js';
|
|
|
2
2
|
function isObject(value) {
|
|
3
3
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
|
+
function isIsoDate(value) {
|
|
6
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
7
|
+
return false;
|
|
8
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
9
|
+
return !Number.isNaN(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === value;
|
|
10
|
+
}
|
|
5
11
|
function requireObject(value, label) {
|
|
6
12
|
if (!isObject(value))
|
|
7
13
|
throw new UsageError(`${label} must be an object`);
|
|
@@ -139,6 +145,8 @@ function isTransaction(value) {
|
|
|
139
145
|
&& (value.categoryId === null || typeof value.categoryId === 'string')
|
|
140
146
|
&& (value.lineItemId === null || typeof value.lineItemId === 'string')
|
|
141
147
|
&& Array.isArray(value.categorySplits)
|
|
148
|
+
&& Number.isInteger(value.personalBudgetAmountPence)
|
|
149
|
+
&& Number(value.personalBudgetAmountPence) >= 0
|
|
142
150
|
&& (value.jointBudgetContribution === null
|
|
143
151
|
|| (isObject(value.jointBudgetContribution)
|
|
144
152
|
&& typeof value.jointBudgetContribution.eligible === 'boolean'
|
|
@@ -158,10 +166,27 @@ function isTransaction(value) {
|
|
|
158
166
|
|| value.incomeSubtype === 'interest'));
|
|
159
167
|
}
|
|
160
168
|
function isTransactionsResponse(value) {
|
|
169
|
+
const validStatuses = new Set(['skipped', 'completed', 'in_progress', 'partial', 'failed']);
|
|
170
|
+
const validReasons = new Set([
|
|
171
|
+
'all_fetched_today',
|
|
172
|
+
'no_api_connections',
|
|
173
|
+
'no_selected_accounts',
|
|
174
|
+
'refreshed',
|
|
175
|
+
'wait_timeout',
|
|
176
|
+
'account_failures',
|
|
177
|
+
'refresh_error',
|
|
178
|
+
]);
|
|
179
|
+
const refresh = isObject(value) ? value.refresh : undefined;
|
|
161
180
|
return (isObject(value)
|
|
162
181
|
&& Array.isArray(value.transactions)
|
|
163
182
|
&& value.transactions.every(isTransaction)
|
|
164
|
-
&& (value.nextCursor === null || typeof value.nextCursor === 'string')
|
|
183
|
+
&& (value.nextCursor === null || typeof value.nextCursor === 'string')
|
|
184
|
+
&& isObject(refresh)
|
|
185
|
+
&& typeof refresh.status === 'string'
|
|
186
|
+
&& validStatuses.has(refresh.status)
|
|
187
|
+
&& typeof refresh.reason === 'string'
|
|
188
|
+
&& validReasons.has(refresh.reason)
|
|
189
|
+
&& isIsoDate(refresh.utcDate));
|
|
165
190
|
}
|
|
166
191
|
function isAssignmentResponse(value) {
|
|
167
192
|
return (isObject(value)
|
|
@@ -174,20 +199,6 @@ function isAssignmentResponse(value) {
|
|
|
174
199
|
&& typeof item.error === 'string'
|
|
175
200
|
&& (item.transactionRef === undefined || typeof item.transactionRef === 'string'))));
|
|
176
201
|
}
|
|
177
|
-
export function validateJointBudgetSettingsResponse(value) {
|
|
178
|
-
if (!isObject(value)
|
|
179
|
-
|| Object.keys(value).some((key) => !new Set([
|
|
180
|
-
'includeSharedPersonalTransactions',
|
|
181
|
-
'updatedAt',
|
|
182
|
-
'updatedBy',
|
|
183
|
-
]).has(key))
|
|
184
|
-
|| typeof value.includeSharedPersonalTransactions !== 'boolean'
|
|
185
|
-
|| (value.updatedAt !== null && !isIsoDateTime(value.updatedAt))
|
|
186
|
-
|| (value.updatedBy !== null && typeof value.updatedBy !== 'string')) {
|
|
187
|
-
throw new ApiError('Invalid joint budget settings response from the Agent API');
|
|
188
|
-
}
|
|
189
|
-
return value;
|
|
190
|
-
}
|
|
191
202
|
function isHttpUrl(value) {
|
|
192
203
|
if (typeof value !== 'string')
|
|
193
204
|
return false;
|
|
@@ -243,6 +254,51 @@ function isGoal(value) {
|
|
|
243
254
|
function isCurrency(value) {
|
|
244
255
|
return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
|
|
245
256
|
}
|
|
257
|
+
function isNullableNonEmptyString(value) {
|
|
258
|
+
return value === null || (typeof value === 'string'
|
|
259
|
+
&& value.length > 0
|
|
260
|
+
&& value === value.trim());
|
|
261
|
+
}
|
|
262
|
+
function isAccount(value) {
|
|
263
|
+
return (isObject(value)
|
|
264
|
+
&& hasOnlyFields(value, [
|
|
265
|
+
'accountRef',
|
|
266
|
+
'accountName',
|
|
267
|
+
'institutionName',
|
|
268
|
+
'accountType',
|
|
269
|
+
'ownership',
|
|
270
|
+
'balanceAmount',
|
|
271
|
+
'currency',
|
|
272
|
+
'source',
|
|
273
|
+
'lastBalanceUpdatedAt',
|
|
274
|
+
'connectionState',
|
|
275
|
+
])
|
|
276
|
+
&& typeof value.accountRef === 'string'
|
|
277
|
+
&& /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef)
|
|
278
|
+
&& isNullableNonEmptyString(value.accountName)
|
|
279
|
+
&& isNullableNonEmptyString(value.institutionName)
|
|
280
|
+
&& (value.accountType === 'current'
|
|
281
|
+
|| value.accountType === 'savings'
|
|
282
|
+
|| value.accountType === 'investments')
|
|
283
|
+
&& (value.ownership === 'personal' || value.ownership === 'joint')
|
|
284
|
+
&& (value.balanceAmount === null
|
|
285
|
+
|| (typeof value.balanceAmount === 'number' && Number.isFinite(value.balanceAmount)))
|
|
286
|
+
&& (value.currency === null || isCurrency(value.currency))
|
|
287
|
+
&& (value.source === 'connected' || value.source === 'manual')
|
|
288
|
+
&& (value.lastBalanceUpdatedAt === null
|
|
289
|
+
|| isIsoDateTime(value.lastBalanceUpdatedAt))
|
|
290
|
+
&& (value.connectionState === 'active'
|
|
291
|
+
|| value.connectionState === 'expired'
|
|
292
|
+
|| value.connectionState === 'manual'
|
|
293
|
+
|| value.connectionState === 'unknown'));
|
|
294
|
+
}
|
|
295
|
+
function isAccountsResponse(value) {
|
|
296
|
+
return (isObject(value)
|
|
297
|
+
&& hasOnlyFields(value, ['asOf', 'accounts'])
|
|
298
|
+
&& isIsoDateTime(value.asOf)
|
|
299
|
+
&& Array.isArray(value.accounts)
|
|
300
|
+
&& value.accounts.every(isAccount));
|
|
301
|
+
}
|
|
246
302
|
function isGoalsResponse(value) {
|
|
247
303
|
return (isObject(value)
|
|
248
304
|
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
@@ -264,19 +320,21 @@ function isGoalDeleteResponse(value) {
|
|
|
264
320
|
&& value.deletedGoalId.trim().length > 0);
|
|
265
321
|
}
|
|
266
322
|
export function parseApiResponse(command, value) {
|
|
267
|
-
const valid = command === '
|
|
268
|
-
?
|
|
269
|
-
: command === '
|
|
270
|
-
?
|
|
271
|
-
: command === '
|
|
272
|
-
?
|
|
273
|
-
: command === '
|
|
274
|
-
?
|
|
275
|
-
: command === '
|
|
276
|
-
?
|
|
277
|
-
: command === 'goals-
|
|
278
|
-
?
|
|
279
|
-
:
|
|
323
|
+
const valid = command === 'accounts'
|
|
324
|
+
? isAccountsResponse(value)
|
|
325
|
+
: command === 'categories'
|
|
326
|
+
? isCategoryResponse(value)
|
|
327
|
+
: command === 'transactions'
|
|
328
|
+
? isTransactionsResponse(value)
|
|
329
|
+
: command === 'assign'
|
|
330
|
+
? isAssignmentResponse(value)
|
|
331
|
+
: command === 'ask-partner'
|
|
332
|
+
? isPartnerResponse(value)
|
|
333
|
+
: command === 'goals-list'
|
|
334
|
+
? isGoalsResponse(value)
|
|
335
|
+
: command === 'goals-delete'
|
|
336
|
+
? isGoalDeleteResponse(value)
|
|
337
|
+
: isGoalMutationResponse(value);
|
|
280
338
|
if (!valid) {
|
|
281
339
|
const label = command === 'assign' ? 'assignment' : command;
|
|
282
340
|
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.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Command-line access to the Sloth Money Agent API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"prepack": "npm run build",
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"test:package": "node scripts/test-package.mjs",
|
|
19
|
+
"test:registry": "node scripts/test-registry-package.mjs",
|
|
19
20
|
"typecheck": "tsc --noEmit",
|
|
20
21
|
"verify": "npm run lint && npm run typecheck && npm test && npm run build && npm run test:package"
|
|
21
22
|
},
|