@slothmoney/agent-cli 0.3.1 → 0.5.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 +23 -0
- package/README.md +102 -12
- package/dist/args.js +148 -40
- package/dist/category-metadata.js +11 -0
- package/dist/cli.js +177 -74
- package/dist/contracts.js +97 -27
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.0 - 2026-08-07
|
|
6
|
+
|
|
7
|
+
- Create and rename custom categories, with existing icon and category type
|
|
8
|
+
validation and preview-only writes unless `--apply` is supplied.
|
|
9
|
+
- Create and rename personal or joint budget line items while preserving
|
|
10
|
+
historical snapshots and future plan amounts.
|
|
11
|
+
- Filter transactions directly by `--line-item-id`, including paired
|
|
12
|
+
`--category-id` matching for split assignments.
|
|
13
|
+
- Strictly validate category and line-item mutation responses and extend
|
|
14
|
+
command-specific and packed-binary help coverage.
|
|
15
|
+
|
|
16
|
+
## 0.4.0 - 2026-08-01
|
|
17
|
+
|
|
18
|
+
- Document that new Sloth Money personal access tokens are view-only by
|
|
19
|
+
default, and identify the CLI operations that require explicit write access.
|
|
20
|
+
- Add the read-only `sloth-agent accounts` command with strict runtime
|
|
21
|
+
validation for opaque references, ownership, native balances, sources, and
|
|
22
|
+
freshness metadata.
|
|
23
|
+
- Prepare 0.4.0 by removing the obsolete `joint-budget-settings` command. Shared
|
|
24
|
+
personal transactions now enter the joint budget through their assignment.
|
|
25
|
+
- Verify each trusted npm release from a fresh temporary directory so the
|
|
26
|
+
registry smoke test cannot resolve a repository-local CLI executable.
|
|
27
|
+
|
|
5
28
|
## 0.3.1 - 2026-07-31
|
|
6
29
|
|
|
7
30
|
- Coordinate transaction reads with the Sloth Budget daily refresh process.
|
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.5.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -24,6 +24,12 @@ Create a personal access token in Sloth Money under
|
|
|
24
24
|
**Settings > Developer access**, then choose the authentication method for
|
|
25
25
|
where the CLI runs.
|
|
26
26
|
|
|
27
|
+
New tokens are view-only. That is enough for `auth status`, `accounts`,
|
|
28
|
+
`categories`, `transactions`, and `goals` list. Enable **Allow changes** when
|
|
29
|
+
creating the token only if the CLI must apply assignments, manage categories
|
|
30
|
+
or line items, ask a partner for an explanation, or manage goals. Token
|
|
31
|
+
permissions cannot be changed later - revoke and reissue the token instead.
|
|
32
|
+
|
|
27
33
|
### Local computer
|
|
28
34
|
|
|
29
35
|
On an interactive desktop, save the token in your operating system's secure
|
|
@@ -96,10 +102,12 @@ options, output, and examples:
|
|
|
96
102
|
|
|
97
103
|
```bash
|
|
98
104
|
sloth-agent auth login --help
|
|
105
|
+
sloth-agent accounts --help
|
|
99
106
|
sloth-agent categories --help
|
|
107
|
+
sloth-agent categories create --help
|
|
108
|
+
sloth-agent line-items create --help
|
|
100
109
|
sloth-agent transactions --help
|
|
101
110
|
sloth-agent assign --help
|
|
102
|
-
sloth-agent joint-budget-settings --help
|
|
103
111
|
sloth-agent goals create --help
|
|
104
112
|
sloth-agent goals update --help
|
|
105
113
|
sloth-agent ask-partner --help
|
|
@@ -158,6 +166,8 @@ does not guarantee that applying it will succeed.
|
|
|
158
166
|
sloth-agent assign --input assignments.json --apply
|
|
159
167
|
```
|
|
160
168
|
|
|
169
|
+
This step requires a token created with **Allow changes**.
|
|
170
|
+
|
|
161
171
|
Inspect every item in the returned `succeeded` and `failed` arrays.
|
|
162
172
|
|
|
163
173
|
6. Check the result. Successful assignments update the category and optional
|
|
@@ -174,6 +184,77 @@ query. Assignments do not create a separate list.
|
|
|
174
184
|
|
|
175
185
|
### Other workflows
|
|
176
186
|
|
|
187
|
+
Create or rename a custom category. Writes are previews until `--apply` is
|
|
188
|
+
present:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
sloth-agent categories create \
|
|
192
|
+
--name "Holidays" \
|
|
193
|
+
--icon-key plane \
|
|
194
|
+
--type Wants
|
|
195
|
+
|
|
196
|
+
sloth-agent categories create \
|
|
197
|
+
--name "Holidays" \
|
|
198
|
+
--icon-key plane \
|
|
199
|
+
--type Wants \
|
|
200
|
+
--apply
|
|
201
|
+
|
|
202
|
+
sloth-agent categories rename \
|
|
203
|
+
--category-id category-id \
|
|
204
|
+
--name "Travel fund" \
|
|
205
|
+
--apply
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Built-in categories cannot be renamed. A created category is available in the
|
|
209
|
+
next `sloth-agent categories` result without needing a budget allocation.
|
|
210
|
+
|
|
211
|
+
Create or rename a line item within a personal or joint budget:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
sloth-agent line-items create \
|
|
215
|
+
--scope personal \
|
|
216
|
+
--category-id groceries \
|
|
217
|
+
--name "Weekly shop" \
|
|
218
|
+
--apply
|
|
219
|
+
|
|
220
|
+
sloth-agent line-items rename \
|
|
221
|
+
--scope personal \
|
|
222
|
+
--category-id groceries \
|
|
223
|
+
--line-item-id line-item-id \
|
|
224
|
+
--name "Essentials" \
|
|
225
|
+
--apply
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Line-item writes update the current period and explicit future plans.
|
|
229
|
+
Historical snapshots remain unchanged. New items start at zero and do not
|
|
230
|
+
change total allocation.
|
|
231
|
+
|
|
232
|
+
Filter transactions by a line-item ID. Pair it with `--category-id` when the
|
|
233
|
+
same ID may appear under different categories:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
sloth-agent transactions \
|
|
237
|
+
--assignment-scope personal \
|
|
238
|
+
--category-id groceries \
|
|
239
|
+
--line-item-id line-item-id
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The category and line-item IDs must match the same primary assignment or split.
|
|
243
|
+
|
|
244
|
+
Read the existing Sloth account inventory:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
sloth-agent accounts
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The command is read-only and cache-only: it does not refresh linked banks or
|
|
251
|
+
change account data. Each result contains an opaque `accountRef`, personal or
|
|
252
|
+
joint ownership, connected or manual source, native balance/currency when
|
|
253
|
+
known, `lastBalanceUpdatedAt`, and `connectionState`. Missing values are JSON
|
|
254
|
+
`null`; currencies are never converted or combined. Partner personal accounts
|
|
255
|
+
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
256
|
+
visibility rules.
|
|
257
|
+
|
|
177
258
|
List your goals:
|
|
178
259
|
|
|
179
260
|
```bash
|
|
@@ -243,16 +324,13 @@ account failure remains eligible for an automatic retry.
|
|
|
243
324
|
Set `"assignmentScope": "joint"` on an assignment to categorise the eligible
|
|
244
325
|
shared portion for the joint budget.
|
|
245
326
|
|
|
246
|
-
|
|
247
|
-
|
|
327
|
+
Transaction reads expose `personalBudgetAmountPence` for the caller's explicit
|
|
328
|
+
personal-only portion and `jointBudgetContribution.amountPence` for the full
|
|
329
|
+
shared portion. The 60/40 settlement ratio does not reduce joint-budget spend.
|
|
248
330
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
sloth-agent joint-budget-settings \
|
|
253
|
-
--include-shared-personal-transactions=true \
|
|
254
|
-
--apply
|
|
255
|
-
```
|
|
331
|
+
Shared personal-account transactions with a joint assignment are included in
|
|
332
|
+
the joint budget automatically. The settlement ratio remains independent from
|
|
333
|
+
the amount attributed to the joint budget.
|
|
256
334
|
|
|
257
335
|
Create a partner clarification link:
|
|
258
336
|
|
|
@@ -296,3 +374,15 @@ npm run verify
|
|
|
296
374
|
|
|
297
375
|
`npm run test:package` packs the exact npm artifact, installs it into a clean
|
|
298
376
|
temporary project, and runs the installed binary.
|
|
377
|
+
|
|
378
|
+
## Releasing
|
|
379
|
+
|
|
380
|
+
Releases are published only through the trusted `Publish npm release` GitHub
|
|
381
|
+
workflow from a reviewed `v*` tag whose version matches `package.json`. The
|
|
382
|
+
workflow runs the full verification suite before publishing.
|
|
383
|
+
|
|
384
|
+
After npm accepts the package, the workflow verifies the exact published
|
|
385
|
+
version with `npm run test:registry -- VERSION`. That script runs `npm exec`
|
|
386
|
+
from a fresh temporary directory with an isolated npm cache, so a checkout's
|
|
387
|
+
older local `sloth-agent` executable cannot satisfy the registry smoke test.
|
|
388
|
+
The temporary directory is removed after the check.
|
package/dist/args.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UsageError } from './errors.js';
|
|
2
|
+
import { CATEGORY_TYPES, ICON_KEYS, } from './category-metadata.js';
|
|
2
3
|
const PRODUCTION_BASE_URL = 'https://budget.slothmoney.app';
|
|
3
4
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
4
5
|
function readOptionValue(args, index, name) {
|
|
@@ -88,6 +89,28 @@ function parseGoalId(value) {
|
|
|
88
89
|
}
|
|
89
90
|
return goalId;
|
|
90
91
|
}
|
|
92
|
+
function parseResourceId(value, option) {
|
|
93
|
+
const id = value.trim();
|
|
94
|
+
if (!id
|
|
95
|
+
|| id.length > 500
|
|
96
|
+
|| id.includes('/')
|
|
97
|
+
|| Array.from(id).some((character) => {
|
|
98
|
+
const codePoint = character.codePointAt(0);
|
|
99
|
+
return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
|
|
100
|
+
})) {
|
|
101
|
+
const resource = option === '--category-id' ? 'category' : 'line-item';
|
|
102
|
+
throw new UsageError(`${option} must be a valid ${resource} document ID`);
|
|
103
|
+
}
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
function parseResourceName(value) {
|
|
107
|
+
const name = value.trim();
|
|
108
|
+
if (!name)
|
|
109
|
+
throw new UsageError('--name requires a value');
|
|
110
|
+
if (name.length > 200)
|
|
111
|
+
throw new UsageError('--name must be at most 200 characters');
|
|
112
|
+
return name;
|
|
113
|
+
}
|
|
91
114
|
function parseTransactions(args) {
|
|
92
115
|
const filters = {};
|
|
93
116
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -114,6 +137,7 @@ function parseTransactions(args) {
|
|
|
114
137
|
'--q',
|
|
115
138
|
'--account-id',
|
|
116
139
|
'--category-id',
|
|
140
|
+
'--line-item-id',
|
|
117
141
|
'--assignment-scope',
|
|
118
142
|
'--cursor',
|
|
119
143
|
]);
|
|
@@ -150,6 +174,9 @@ function parseTransactions(args) {
|
|
|
150
174
|
else if (name === '--category-id') {
|
|
151
175
|
filters.categoryId = setOnce(filters.categoryId, value, name);
|
|
152
176
|
}
|
|
177
|
+
else if (name === '--line-item-id') {
|
|
178
|
+
filters.lineItemId = setOnce(filters.lineItemId, value, name);
|
|
179
|
+
}
|
|
153
180
|
else if (name === '--assignment-scope') {
|
|
154
181
|
if (value !== 'personal' && value !== 'joint') {
|
|
155
182
|
throw new UsageError('--assignment-scope must be personal or joint');
|
|
@@ -167,6 +194,105 @@ function parseTransactions(args) {
|
|
|
167
194
|
}
|
|
168
195
|
return filters;
|
|
169
196
|
}
|
|
197
|
+
function parseNamedOptions(args, commandLabel, allowed) {
|
|
198
|
+
const values = new Map();
|
|
199
|
+
let apply = false;
|
|
200
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
201
|
+
const argument = args[index];
|
|
202
|
+
if (argument === '--apply') {
|
|
203
|
+
if (apply)
|
|
204
|
+
throw new UsageError('--apply may only be provided once');
|
|
205
|
+
apply = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const [option, inlineValue] = argument.includes('=')
|
|
209
|
+
? argument.split(/=(.*)/s, 2)
|
|
210
|
+
: [argument, undefined];
|
|
211
|
+
if (!option || !allowed.has(option)) {
|
|
212
|
+
throw new UsageError(`Unknown ${commandLabel} option: ${argument}`);
|
|
213
|
+
}
|
|
214
|
+
if (values.has(option))
|
|
215
|
+
throw new UsageError(`${option} may only be provided once`);
|
|
216
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
217
|
+
if (inlineValue === undefined)
|
|
218
|
+
index += 1;
|
|
219
|
+
values.set(option, value);
|
|
220
|
+
}
|
|
221
|
+
return { values, apply };
|
|
222
|
+
}
|
|
223
|
+
function requiredOption(values, option, commandLabel) {
|
|
224
|
+
const value = values.get(option);
|
|
225
|
+
if (!value)
|
|
226
|
+
throw new UsageError(`${commandLabel} requires ${option} <value>`);
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
function parseCategories(args, baseUrl) {
|
|
230
|
+
const subcommand = args.shift();
|
|
231
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
232
|
+
if (args.length > 0)
|
|
233
|
+
throw new UsageError(`Unknown categories list option: ${args[0]}`);
|
|
234
|
+
return withBaseUrl({ command: 'categories' }, baseUrl);
|
|
235
|
+
}
|
|
236
|
+
if (subcommand === 'create') {
|
|
237
|
+
const { values, apply } = parseNamedOptions(args, 'categories create', new Set(['--name', '--icon-key', '--type']));
|
|
238
|
+
const iconKey = requiredOption(values, '--icon-key', 'categories create');
|
|
239
|
+
const categoryType = requiredOption(values, '--type', 'categories create');
|
|
240
|
+
if (!ICON_KEYS.includes(iconKey)) {
|
|
241
|
+
throw new UsageError(`--icon-key must be one of: ${ICON_KEYS.join(', ')}`);
|
|
242
|
+
}
|
|
243
|
+
if (!CATEGORY_TYPES.includes(categoryType)) {
|
|
244
|
+
throw new UsageError(`--type must be one of: ${CATEGORY_TYPES.join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
return withBaseUrl({
|
|
247
|
+
command: 'categories-create',
|
|
248
|
+
name: parseResourceName(requiredOption(values, '--name', 'categories create')),
|
|
249
|
+
iconKey: iconKey,
|
|
250
|
+
categoryType: categoryType,
|
|
251
|
+
apply,
|
|
252
|
+
}, baseUrl);
|
|
253
|
+
}
|
|
254
|
+
if (subcommand === 'rename') {
|
|
255
|
+
const { values, apply } = parseNamedOptions(args, 'categories rename', new Set(['--category-id', '--name']));
|
|
256
|
+
return withBaseUrl({
|
|
257
|
+
command: 'categories-rename',
|
|
258
|
+
categoryId: parseResourceId(requiredOption(values, '--category-id', 'categories rename'), '--category-id'),
|
|
259
|
+
name: parseResourceName(requiredOption(values, '--name', 'categories rename')),
|
|
260
|
+
apply,
|
|
261
|
+
}, baseUrl);
|
|
262
|
+
}
|
|
263
|
+
if (subcommand.startsWith('-')) {
|
|
264
|
+
throw new UsageError(`Unknown categories option: ${subcommand}`);
|
|
265
|
+
}
|
|
266
|
+
throw new UsageError(`Unknown categories command: ${subcommand}`);
|
|
267
|
+
}
|
|
268
|
+
function parseLineItems(args, baseUrl) {
|
|
269
|
+
const subcommand = args.shift();
|
|
270
|
+
if (subcommand !== 'create' && subcommand !== 'rename') {
|
|
271
|
+
throw new UsageError('line-items requires create or rename');
|
|
272
|
+
}
|
|
273
|
+
const allowed = new Set(['--scope', '--category-id', '--name']);
|
|
274
|
+
if (subcommand === 'rename')
|
|
275
|
+
allowed.add('--line-item-id');
|
|
276
|
+
const { values, apply } = parseNamedOptions(args, `line-items ${subcommand}`, allowed);
|
|
277
|
+
const scope = requiredOption(values, '--scope', `line-items ${subcommand}`);
|
|
278
|
+
if (scope !== 'personal' && scope !== 'joint') {
|
|
279
|
+
throw new UsageError('--scope must be personal or joint');
|
|
280
|
+
}
|
|
281
|
+
const common = {
|
|
282
|
+
scope: scope,
|
|
283
|
+
categoryId: parseResourceId(requiredOption(values, '--category-id', `line-items ${subcommand}`), '--category-id'),
|
|
284
|
+
name: parseResourceName(requiredOption(values, '--name', `line-items ${subcommand}`)),
|
|
285
|
+
apply,
|
|
286
|
+
};
|
|
287
|
+
if (subcommand === 'create') {
|
|
288
|
+
return withBaseUrl({ command: 'line-items-create', ...common }, baseUrl);
|
|
289
|
+
}
|
|
290
|
+
return withBaseUrl({
|
|
291
|
+
command: 'line-items-rename',
|
|
292
|
+
...common,
|
|
293
|
+
lineItemId: parseResourceId(requiredOption(values, '--line-item-id', 'line-items rename'), '--line-item-id'),
|
|
294
|
+
}, baseUrl);
|
|
295
|
+
}
|
|
170
296
|
function withBaseUrl(value, baseUrl) {
|
|
171
297
|
return baseUrl === undefined ? value : { ...value, baseUrl };
|
|
172
298
|
}
|
|
@@ -411,10 +537,23 @@ function helpTopic(argv) {
|
|
|
411
537
|
return 'goals-delete';
|
|
412
538
|
return 'goals';
|
|
413
539
|
}
|
|
414
|
-
if (command === 'categories'
|
|
540
|
+
if (command === 'categories') {
|
|
541
|
+
if (subcommand === 'create')
|
|
542
|
+
return 'categories-create';
|
|
543
|
+
if (subcommand === 'rename')
|
|
544
|
+
return 'categories-rename';
|
|
545
|
+
return 'categories';
|
|
546
|
+
}
|
|
547
|
+
if (command === 'line-items') {
|
|
548
|
+
if (subcommand === 'create')
|
|
549
|
+
return 'line-items-create';
|
|
550
|
+
if (subcommand === 'rename')
|
|
551
|
+
return 'line-items-rename';
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
if (command === 'accounts'
|
|
415
555
|
|| command === 'transactions'
|
|
416
556
|
|| command === 'assign'
|
|
417
|
-
|| command === 'joint-budget-settings'
|
|
418
557
|
|| command === 'ask-partner') {
|
|
419
558
|
return command;
|
|
420
559
|
}
|
|
@@ -438,8 +577,14 @@ export function parseArgs(argv) {
|
|
|
438
577
|
return parseGoals(args, baseUrl);
|
|
439
578
|
}
|
|
440
579
|
if (command === 'categories') {
|
|
580
|
+
return parseCategories(args, baseUrl);
|
|
581
|
+
}
|
|
582
|
+
if (command === 'line-items') {
|
|
583
|
+
return parseLineItems(args, baseUrl);
|
|
584
|
+
}
|
|
585
|
+
if (command === 'accounts') {
|
|
441
586
|
if (args.length > 0) {
|
|
442
|
-
throw new UsageError(`Unknown
|
|
587
|
+
throw new UsageError(`Unknown accounts option: ${args[0]}`);
|
|
443
588
|
}
|
|
444
589
|
return withBaseUrl({ command }, baseUrl);
|
|
445
590
|
}
|
|
@@ -471,43 +616,6 @@ export function parseArgs(argv) {
|
|
|
471
616
|
throw new UsageError('assign requires --input <file>');
|
|
472
617
|
return withBaseUrl({ command, input, apply }, baseUrl);
|
|
473
618
|
}
|
|
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
619
|
if (command === 'ask-partner') {
|
|
512
620
|
let transactionRef;
|
|
513
621
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const ICON_KEYS = [
|
|
2
|
+
'home', 'shopping-cart', 'plane', 'car', 'bill', 'dots', 'chart', 'receipt',
|
|
3
|
+
'credit-card', 'user', 'utensils', 'truck', 'calendar', 'heart', 'shopping-bag',
|
|
4
|
+
'gift', 'ban', 'question', 'arrows-sync', 'dollar-sign', 'plus',
|
|
5
|
+
];
|
|
6
|
+
export const CATEGORY_TYPES = [
|
|
7
|
+
'Needs',
|
|
8
|
+
'Debts',
|
|
9
|
+
'Savings & Investments',
|
|
10
|
+
'Wants',
|
|
11
|
+
];
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
|
-
import {
|
|
3
|
+
import { ICON_KEYS } from './category-metadata.js';
|
|
4
|
+
import { parseApiResponse, validateAssignmentPayload, } from './contracts.js';
|
|
4
5
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
5
6
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
6
|
-
export const CLI_VERSION = '0.
|
|
7
|
+
export const CLI_VERSION = '0.5.0';
|
|
7
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
8
9
|
const API_ORIGIN_HELP_LINES = [
|
|
9
10
|
'',
|
|
@@ -24,12 +25,17 @@ export function usageText() {
|
|
|
24
25
|
' sloth-agent auth login [--token-stdin | --from-env] [--base-url URL]',
|
|
25
26
|
' sloth-agent auth status [--base-url URL]',
|
|
26
27
|
' sloth-agent auth logout [--base-url URL]',
|
|
27
|
-
' sloth-agent
|
|
28
|
+
' sloth-agent accounts [--base-url URL]',
|
|
29
|
+
' sloth-agent categories [list] [--base-url URL]',
|
|
30
|
+
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply]',
|
|
31
|
+
' sloth-agent categories rename --category-id ID --name NAME [--apply]',
|
|
32
|
+
' sloth-agent line-items create --scope personal|joint --category-id ID --name NAME [--apply]',
|
|
33
|
+
' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply]',
|
|
28
34
|
' sloth-agent transactions [--uncategorized[=true|false]] [--limit N]',
|
|
29
35
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
30
|
-
' [--account-id ID] [--category-id ID] [--
|
|
36
|
+
' [--account-id ID] [--category-id ID] [--line-item-id ID]',
|
|
37
|
+
' [--cursor CURSOR] [--base-url URL]',
|
|
31
38
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
32
|
-
' sloth-agent joint-budget-settings [--include-shared-personal-transactions=true|false] [--apply]',
|
|
33
39
|
' sloth-agent goals [list] [--base-url URL]',
|
|
34
40
|
' sloth-agent goals create --name NAME [--target-amount AMOUNT]',
|
|
35
41
|
' [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
@@ -51,6 +57,10 @@ export function usageText() {
|
|
|
51
57
|
'Environment:',
|
|
52
58
|
' SLOTH_AGENT_TOKEN Personal access token from Settings > Developer access',
|
|
53
59
|
' SLOTH_AGENT_API_BASE_URL Optional API origin; defaults to https://budget.slothmoney.app',
|
|
60
|
+
'',
|
|
61
|
+
'Access:',
|
|
62
|
+
' New tokens are view-only. Enable Allow changes in Sloth Money when the',
|
|
63
|
+
' CLI needs to call write endpoints.',
|
|
54
64
|
].join('\n');
|
|
55
65
|
}
|
|
56
66
|
export function authHelpText() {
|
|
@@ -89,6 +99,8 @@ export function authLoginHelpText() {
|
|
|
89
99
|
' Tokens must start with sloth_pat_v1_ and contain no whitespace.',
|
|
90
100
|
' Never pass a token as a command argument.',
|
|
91
101
|
' An existing stored credential is replaced only after remote validation succeeds.',
|
|
102
|
+
' Remote validation requires agent:read. Commands that write also require',
|
|
103
|
+
' agent:write, selected with Allow changes when the token is created.',
|
|
92
104
|
...API_ORIGIN_HELP_LINES,
|
|
93
105
|
'',
|
|
94
106
|
'Examples:',
|
|
@@ -154,7 +166,7 @@ export function categoriesHelpText() {
|
|
|
154
166
|
'Read categories and the personal and joint line items within them.',
|
|
155
167
|
'',
|
|
156
168
|
'Usage:',
|
|
157
|
-
' sloth-agent categories [--base-url URL]',
|
|
169
|
+
' sloth-agent categories [list] [--base-url URL]',
|
|
158
170
|
'',
|
|
159
171
|
'Options:',
|
|
160
172
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -182,6 +194,110 @@ export function categoriesHelpText() {
|
|
|
182
194
|
' jointLineItemsByCategoryId Joint child line items keyed by category ID',
|
|
183
195
|
].join('\n');
|
|
184
196
|
}
|
|
197
|
+
export function categoriesCreateHelpText() {
|
|
198
|
+
return [
|
|
199
|
+
'Sloth Agent CLI — categories create',
|
|
200
|
+
'',
|
|
201
|
+
'Create a custom category.',
|
|
202
|
+
'',
|
|
203
|
+
'Usage:',
|
|
204
|
+
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply] [--base-url URL]',
|
|
205
|
+
'',
|
|
206
|
+
'Required inputs:',
|
|
207
|
+
' --name NAME Category name, up to 200 characters.',
|
|
208
|
+
` --icon-key KEY One of: ${ICON_KEYS.join(', ')}.`,
|
|
209
|
+
' --type TYPE Needs, Debts, Savings & Investments, or Wants.',
|
|
210
|
+
'',
|
|
211
|
+
'Write behavior:',
|
|
212
|
+
' Without --apply, returns a JSON preview and makes no mutation request.',
|
|
213
|
+
' With --apply, creates the category globally and requires a write-enabled token.',
|
|
214
|
+
...API_ORIGIN_HELP_LINES,
|
|
215
|
+
'',
|
|
216
|
+
'Output:',
|
|
217
|
+
' JSON containing category.id, name, iconKey, categoryType, and source.',
|
|
218
|
+
].join('\n');
|
|
219
|
+
}
|
|
220
|
+
export function categoriesRenameHelpText() {
|
|
221
|
+
return [
|
|
222
|
+
'Sloth Agent CLI — categories rename',
|
|
223
|
+
'',
|
|
224
|
+
'Rename a user-created category. Built-in categories are immutable.',
|
|
225
|
+
'',
|
|
226
|
+
'Usage:',
|
|
227
|
+
' sloth-agent categories rename --category-id ID --name NAME [--apply] [--base-url URL]',
|
|
228
|
+
'',
|
|
229
|
+
'Required inputs:',
|
|
230
|
+
' --category-id ID Custom category document ID.',
|
|
231
|
+
' --name NAME New category name, up to 200 characters.',
|
|
232
|
+
'',
|
|
233
|
+
'Write behavior:',
|
|
234
|
+
' Without --apply, returns a JSON preview and makes no mutation request.',
|
|
235
|
+
' With --apply, renames the canonical category and requires a write-enabled token.',
|
|
236
|
+
...API_ORIGIN_HELP_LINES,
|
|
237
|
+
'',
|
|
238
|
+
'Output:',
|
|
239
|
+
' JSON containing the renamed category.',
|
|
240
|
+
].join('\n');
|
|
241
|
+
}
|
|
242
|
+
function lineItemsMutationHelpText(operation) {
|
|
243
|
+
const rename = operation === 'rename';
|
|
244
|
+
return [
|
|
245
|
+
`Sloth Agent CLI — line-items ${operation}`,
|
|
246
|
+
'',
|
|
247
|
+
rename ? 'Rename a scoped budget line item.' : 'Create a scoped budget line item at zero.',
|
|
248
|
+
'',
|
|
249
|
+
'Usage:',
|
|
250
|
+
rename
|
|
251
|
+
? ' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply] [--base-url URL]'
|
|
252
|
+
: ' sloth-agent line-items create --scope personal|joint --category-id ID --name NAME [--apply] [--base-url URL]',
|
|
253
|
+
'',
|
|
254
|
+
'Required inputs:',
|
|
255
|
+
' --scope SCOPE personal or joint.',
|
|
256
|
+
' --category-id ID Parent category ID.',
|
|
257
|
+
...(rename ? [' --line-item-id ID Existing child line-item ID.'] : []),
|
|
258
|
+
' --name NAME Line-item name, up to 200 characters.',
|
|
259
|
+
'',
|
|
260
|
+
'Write behavior:',
|
|
261
|
+
' Without --apply, returns a JSON preview and makes no mutation request.',
|
|
262
|
+
' With --apply, updates the current period and explicit future plans.',
|
|
263
|
+
' Historical snapshots remain unchanged. A write-enabled token is required.',
|
|
264
|
+
...API_ORIGIN_HELP_LINES,
|
|
265
|
+
'',
|
|
266
|
+
'Output:',
|
|
267
|
+
' JSON containing scope, categoryId, and lineItem.id and name.',
|
|
268
|
+
].join('\n');
|
|
269
|
+
}
|
|
270
|
+
export function lineItemsCreateHelpText() {
|
|
271
|
+
return lineItemsMutationHelpText('create');
|
|
272
|
+
}
|
|
273
|
+
export function lineItemsRenameHelpText() {
|
|
274
|
+
return lineItemsMutationHelpText('rename');
|
|
275
|
+
}
|
|
276
|
+
export function accountsHelpText() {
|
|
277
|
+
return [
|
|
278
|
+
'Sloth Agent CLI — accounts',
|
|
279
|
+
'',
|
|
280
|
+
'Read the existing Sloth account inventory known to the authenticated user.',
|
|
281
|
+
'',
|
|
282
|
+
'Usage:',
|
|
283
|
+
' sloth-agent accounts [--base-url URL]',
|
|
284
|
+
'',
|
|
285
|
+
'Options:',
|
|
286
|
+
' --base-url URL Optional. Override the API origin.',
|
|
287
|
+
' -h, --help Show this help.',
|
|
288
|
+
...API_ORIGIN_HELP_LINES,
|
|
289
|
+
'',
|
|
290
|
+
'Access:',
|
|
291
|
+
' This command is read-only and does not refresh connected accounts.',
|
|
292
|
+
'',
|
|
293
|
+
'Output:',
|
|
294
|
+
' asOf Server response time',
|
|
295
|
+
' accounts[].accountRef Opaque stable account reference',
|
|
296
|
+
' accounts[].ownership personal or joint',
|
|
297
|
+
' accounts[].balanceAmount and currency in the native currency when known',
|
|
298
|
+
' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
|
|
299
|
+
].join('\n');
|
|
300
|
+
}
|
|
185
301
|
export function transactionsHelpText() {
|
|
186
302
|
return [
|
|
187
303
|
'Sloth Agent CLI — transactions',
|
|
@@ -199,6 +315,7 @@ export function transactionsHelpText() {
|
|
|
199
315
|
' --q TEXT Optional. Search transactions by text.',
|
|
200
316
|
' --account-id ID Optional. Filter by account ID.',
|
|
201
317
|
' --category-id ID Optional. Filter by category ID.',
|
|
318
|
+
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
202
319
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
203
320
|
' --cursor CURSOR Optional. Continue from a previous nextCursor.',
|
|
204
321
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -248,6 +365,7 @@ export function assignHelpText() {
|
|
|
248
365
|
' A successful preview does not guarantee that applying it will succeed.',
|
|
249
366
|
' With --apply, assignments are best-effort; any failed item makes the command',
|
|
250
367
|
' exit with code 1 while the complete result remains available on stdout.',
|
|
368
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
251
369
|
'',
|
|
252
370
|
'Input:',
|
|
253
371
|
' The top-level object must contain an assignments array.',
|
|
@@ -288,37 +406,6 @@ export function assignHelpText() {
|
|
|
288
406
|
' Assignments do not create a separate list.',
|
|
289
407
|
].join('\n');
|
|
290
408
|
}
|
|
291
|
-
export function jointBudgetSettingsHelpText() {
|
|
292
|
-
return [
|
|
293
|
-
'Sloth Agent CLI — joint-budget-settings',
|
|
294
|
-
'',
|
|
295
|
-
'Read or update whether shared personal transactions count in the linked joint budget.',
|
|
296
|
-
'',
|
|
297
|
-
'Usage:',
|
|
298
|
-
' sloth-agent joint-budget-settings [options]',
|
|
299
|
-
'',
|
|
300
|
-
'Options:',
|
|
301
|
-
' --include-shared-personal-transactions=true|false',
|
|
302
|
-
' Optional. Preview the linked setting change.',
|
|
303
|
-
' --apply Optional. Apply the previewed setting change.',
|
|
304
|
-
' --base-url URL Optional. Override the API origin.',
|
|
305
|
-
' -h, --help Show this help.',
|
|
306
|
-
...API_ORIGIN_HELP_LINES,
|
|
307
|
-
'',
|
|
308
|
-
'Safety:',
|
|
309
|
-
' With no setting option, the command is read-only.',
|
|
310
|
-
' Without --apply, a setting option returns a dry-run preview and does not write.',
|
|
311
|
-
' --apply requires an explicit true or false setting value.',
|
|
312
|
-
'',
|
|
313
|
-
'Examples:',
|
|
314
|
-
' sloth-agent joint-budget-settings',
|
|
315
|
-
' sloth-agent joint-budget-settings --include-shared-personal-transactions=true',
|
|
316
|
-
' sloth-agent joint-budget-settings --include-shared-personal-transactions=true --apply',
|
|
317
|
-
'',
|
|
318
|
-
'Output:',
|
|
319
|
-
' JSON containing the linked setting, audit metadata, or a dry-run payload.',
|
|
320
|
-
].join('\n');
|
|
321
|
-
}
|
|
322
409
|
export function goalsHelpText() {
|
|
323
410
|
return [
|
|
324
411
|
'Sloth Agent CLI — goals',
|
|
@@ -379,6 +466,7 @@ export function goalsCreateHelpText() {
|
|
|
379
466
|
'',
|
|
380
467
|
'Safety:',
|
|
381
468
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
469
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
382
470
|
' New goals are private to the owner and appended to the existing goal order.',
|
|
383
471
|
'',
|
|
384
472
|
'Example:',
|
|
@@ -424,6 +512,7 @@ export function goalsUpdateHelpText() {
|
|
|
424
512
|
'',
|
|
425
513
|
'Safety:',
|
|
426
514
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
515
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
427
516
|
'',
|
|
428
517
|
'Output:',
|
|
429
518
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
@@ -448,6 +537,7 @@ export function goalsDeleteHelpText() {
|
|
|
448
537
|
'',
|
|
449
538
|
'Safety:',
|
|
450
539
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
540
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
451
541
|
' Applying deletion also removes the goal from forecast assignments and',
|
|
452
542
|
' removes its goal drift history. This operation cannot be undone.',
|
|
453
543
|
'',
|
|
@@ -473,6 +563,7 @@ export function askPartnerHelpText() {
|
|
|
473
563
|
'',
|
|
474
564
|
'Write behavior:',
|
|
475
565
|
' Running this command creates the request immediately. There is no preview mode.',
|
|
566
|
+
' Creating the request requires a write-enabled token created with Allow changes.',
|
|
476
567
|
'',
|
|
477
568
|
'Example:',
|
|
478
569
|
' sloth-agent ask-partner --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE',
|
|
@@ -489,10 +580,14 @@ export function commandHelpText(topic) {
|
|
|
489
580
|
'auth-login': authLoginHelpText,
|
|
490
581
|
'auth-status': authStatusHelpText,
|
|
491
582
|
'auth-logout': authLogoutHelpText,
|
|
583
|
+
accounts: accountsHelpText,
|
|
492
584
|
categories: categoriesHelpText,
|
|
585
|
+
'categories-create': categoriesCreateHelpText,
|
|
586
|
+
'categories-rename': categoriesRenameHelpText,
|
|
587
|
+
'line-items-create': lineItemsCreateHelpText,
|
|
588
|
+
'line-items-rename': lineItemsRenameHelpText,
|
|
493
589
|
transactions: transactionsHelpText,
|
|
494
590
|
assign: assignHelpText,
|
|
495
|
-
'joint-budget-settings': jointBudgetSettingsHelpText,
|
|
496
591
|
goals: goalsHelpText,
|
|
497
592
|
'goals-list': goalsListHelpText,
|
|
498
593
|
'goals-create': goalsCreateHelpText,
|
|
@@ -588,6 +683,8 @@ function buildTransactionsQuery(filters) {
|
|
|
588
683
|
params.set('accountId', filters.accountId);
|
|
589
684
|
if (filters.categoryId !== undefined)
|
|
590
685
|
params.set('categoryId', filters.categoryId);
|
|
686
|
+
if (filters.lineItemId !== undefined)
|
|
687
|
+
params.set('lineItemId', filters.lineItemId);
|
|
591
688
|
if (filters.assignmentScope !== undefined) {
|
|
592
689
|
params.set('assignmentScope', filters.assignmentScope);
|
|
593
690
|
}
|
|
@@ -739,6 +836,40 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
739
836
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
740
837
|
token = credential.token;
|
|
741
838
|
const headers = requestHeaders(token);
|
|
839
|
+
if (parsed.command === 'categories-create'
|
|
840
|
+
|| parsed.command === 'categories-rename'
|
|
841
|
+
|| parsed.command === 'line-items-create'
|
|
842
|
+
|| parsed.command === 'line-items-rename') {
|
|
843
|
+
const isCategory = parsed.command.startsWith('categories-');
|
|
844
|
+
const isCreate = parsed.command.endsWith('-create');
|
|
845
|
+
const resourceId = parsed.command === 'categories-rename'
|
|
846
|
+
? parsed.categoryId
|
|
847
|
+
: parsed.command === 'line-items-rename'
|
|
848
|
+
? parsed.lineItemId
|
|
849
|
+
: null;
|
|
850
|
+
const endpoint = isCreate
|
|
851
|
+
? `${baseUrl}/api/agent/v1/${isCategory ? 'categories' : 'line-items'}`
|
|
852
|
+
: `${baseUrl}/api/agent/v1/${isCategory ? 'categories' : 'line-items'}/${encodeURIComponent(resourceId)}`;
|
|
853
|
+
const payload = parsed.command === 'categories-create'
|
|
854
|
+
? { name: parsed.name, iconKey: parsed.iconKey, categoryType: parsed.categoryType }
|
|
855
|
+
: parsed.command === 'categories-rename'
|
|
856
|
+
? { name: parsed.name }
|
|
857
|
+
: { scope: parsed.scope, categoryId: parsed.categoryId, name: parsed.name };
|
|
858
|
+
const method = isCreate ? 'POST' : 'PATCH';
|
|
859
|
+
if (!parsed.apply) {
|
|
860
|
+
writeJson(writeStdout, { dryRun: true, endpoint, method, payload });
|
|
861
|
+
return 0;
|
|
862
|
+
}
|
|
863
|
+
const response = await fetchImplementation(endpoint, {
|
|
864
|
+
method,
|
|
865
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
866
|
+
body: JSON.stringify(payload),
|
|
867
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
868
|
+
});
|
|
869
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
870
|
+
writeJson(writeStdout, data);
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
742
873
|
if (parsed.command === 'goals-create') {
|
|
743
874
|
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
744
875
|
const payload = {
|
|
@@ -847,36 +978,6 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
847
978
|
writeJson(writeStdout, data);
|
|
848
979
|
return hasFailures(data) ? 1 : 0;
|
|
849
980
|
}
|
|
850
|
-
if (parsed.command === 'joint-budget-settings') {
|
|
851
|
-
const endpoint = `${baseUrl}/api/agent/v1/joint-budget-settings`;
|
|
852
|
-
if (parsed.includeSharedPersonalTransactions !== undefined && !parsed.apply) {
|
|
853
|
-
writeJson(writeStdout, {
|
|
854
|
-
dryRun: true,
|
|
855
|
-
endpoint,
|
|
856
|
-
payload: {
|
|
857
|
-
includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
|
|
858
|
-
},
|
|
859
|
-
});
|
|
860
|
-
return 0;
|
|
861
|
-
}
|
|
862
|
-
const response = await fetchImplementation(endpoint, {
|
|
863
|
-
method: parsed.apply ? 'PUT' : 'GET',
|
|
864
|
-
headers: parsed.apply
|
|
865
|
-
? { ...headers, 'Content-Type': 'application/json' }
|
|
866
|
-
: headers,
|
|
867
|
-
...(parsed.apply
|
|
868
|
-
? {
|
|
869
|
-
body: JSON.stringify({
|
|
870
|
-
includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
|
|
871
|
-
}),
|
|
872
|
-
}
|
|
873
|
-
: {}),
|
|
874
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
875
|
-
});
|
|
876
|
-
const data = validateJointBudgetSettingsResponse(await parseHttpResponse(response, token));
|
|
877
|
-
writeJson(writeStdout, data);
|
|
878
|
-
return 0;
|
|
879
|
-
}
|
|
880
981
|
if (parsed.command === 'ask-partner') {
|
|
881
982
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
|
|
882
983
|
method: 'POST',
|
|
@@ -901,12 +1002,14 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
901
1002
|
writeJson(writeStdout, data);
|
|
902
1003
|
return 0;
|
|
903
1004
|
}
|
|
904
|
-
const path = parsed.command === '
|
|
905
|
-
? '/api/agent/v1/
|
|
906
|
-
:
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1005
|
+
const path = parsed.command === 'accounts'
|
|
1006
|
+
? '/api/agent/v1/accounts'
|
|
1007
|
+
: parsed.command === 'categories'
|
|
1008
|
+
? '/api/agent/v1/categories'
|
|
1009
|
+
: `/api/agent/v1/transactions${(() => {
|
|
1010
|
+
const query = buildTransactionsQuery(parsed.filters);
|
|
1011
|
+
return query ? `?${query}` : '';
|
|
1012
|
+
})()}`;
|
|
910
1013
|
const response = await fetchImplementation(`${baseUrl}${path}`, {
|
|
911
1014
|
method: 'GET',
|
|
912
1015
|
headers: parsed.command === 'transactions'
|
package/dist/contracts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiError, UsageError, } from './errors.js';
|
|
2
|
+
import { CATEGORY_TYPES, ICON_KEYS } from './category-metadata.js';
|
|
2
3
|
function isObject(value) {
|
|
3
4
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -128,6 +129,36 @@ function isCategoryResponse(value) {
|
|
|
128
129
|
&& isLineItemMap(value.personalLineItemsByCategoryId)
|
|
129
130
|
&& isLineItemMap(value.jointLineItemsByCategoryId));
|
|
130
131
|
}
|
|
132
|
+
function isCategoryMutationResponse(value) {
|
|
133
|
+
if (!isObject(value) || !hasOnlyFields(value, ['category']) || !isObject(value.category)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const category = value.category;
|
|
137
|
+
return (hasOnlyFields(category, ['id', 'name', 'iconKey', 'categoryType', 'source'])
|
|
138
|
+
&& typeof category.id === 'string'
|
|
139
|
+
&& category.id.trim().length > 0
|
|
140
|
+
&& typeof category.name === 'string'
|
|
141
|
+
&& category.name.trim().length > 0
|
|
142
|
+
&& typeof category.iconKey === 'string'
|
|
143
|
+
&& ICON_KEYS.includes(category.iconKey)
|
|
144
|
+
&& typeof category.categoryType === 'string'
|
|
145
|
+
&& CATEGORY_TYPES.includes(category.categoryType)
|
|
146
|
+
&& category.source === 'user');
|
|
147
|
+
}
|
|
148
|
+
function isLineItemMutationResponse(value) {
|
|
149
|
+
if (!isObject(value)
|
|
150
|
+
|| !hasOnlyFields(value, ['scope', 'categoryId', 'lineItem'])
|
|
151
|
+
|| (value.scope !== 'personal' && value.scope !== 'joint')
|
|
152
|
+
|| typeof value.categoryId !== 'string'
|
|
153
|
+
|| !value.categoryId.trim()
|
|
154
|
+
|| !isObject(value.lineItem))
|
|
155
|
+
return false;
|
|
156
|
+
return (hasOnlyFields(value.lineItem, ['id', 'name'])
|
|
157
|
+
&& typeof value.lineItem.id === 'string'
|
|
158
|
+
&& value.lineItem.id.trim().length > 0
|
|
159
|
+
&& typeof value.lineItem.name === 'string'
|
|
160
|
+
&& value.lineItem.name.trim().length > 0);
|
|
161
|
+
}
|
|
131
162
|
function isTransaction(value) {
|
|
132
163
|
return (isObject(value)
|
|
133
164
|
&& typeof value.transactionRef === 'string'
|
|
@@ -145,6 +176,8 @@ function isTransaction(value) {
|
|
|
145
176
|
&& (value.categoryId === null || typeof value.categoryId === 'string')
|
|
146
177
|
&& (value.lineItemId === null || typeof value.lineItemId === 'string')
|
|
147
178
|
&& Array.isArray(value.categorySplits)
|
|
179
|
+
&& Number.isInteger(value.personalBudgetAmountPence)
|
|
180
|
+
&& Number(value.personalBudgetAmountPence) >= 0
|
|
148
181
|
&& (value.jointBudgetContribution === null
|
|
149
182
|
|| (isObject(value.jointBudgetContribution)
|
|
150
183
|
&& typeof value.jointBudgetContribution.eligible === 'boolean'
|
|
@@ -197,20 +230,6 @@ function isAssignmentResponse(value) {
|
|
|
197
230
|
&& typeof item.error === 'string'
|
|
198
231
|
&& (item.transactionRef === undefined || typeof item.transactionRef === 'string'))));
|
|
199
232
|
}
|
|
200
|
-
export function validateJointBudgetSettingsResponse(value) {
|
|
201
|
-
if (!isObject(value)
|
|
202
|
-
|| Object.keys(value).some((key) => !new Set([
|
|
203
|
-
'includeSharedPersonalTransactions',
|
|
204
|
-
'updatedAt',
|
|
205
|
-
'updatedBy',
|
|
206
|
-
]).has(key))
|
|
207
|
-
|| typeof value.includeSharedPersonalTransactions !== 'boolean'
|
|
208
|
-
|| (value.updatedAt !== null && !isIsoDateTime(value.updatedAt))
|
|
209
|
-
|| (value.updatedBy !== null && typeof value.updatedBy !== 'string')) {
|
|
210
|
-
throw new ApiError('Invalid joint budget settings response from the Agent API');
|
|
211
|
-
}
|
|
212
|
-
return value;
|
|
213
|
-
}
|
|
214
233
|
function isHttpUrl(value) {
|
|
215
234
|
if (typeof value !== 'string')
|
|
216
235
|
return false;
|
|
@@ -266,6 +285,51 @@ function isGoal(value) {
|
|
|
266
285
|
function isCurrency(value) {
|
|
267
286
|
return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
|
|
268
287
|
}
|
|
288
|
+
function isNullableNonEmptyString(value) {
|
|
289
|
+
return value === null || (typeof value === 'string'
|
|
290
|
+
&& value.length > 0
|
|
291
|
+
&& value === value.trim());
|
|
292
|
+
}
|
|
293
|
+
function isAccount(value) {
|
|
294
|
+
return (isObject(value)
|
|
295
|
+
&& hasOnlyFields(value, [
|
|
296
|
+
'accountRef',
|
|
297
|
+
'accountName',
|
|
298
|
+
'institutionName',
|
|
299
|
+
'accountType',
|
|
300
|
+
'ownership',
|
|
301
|
+
'balanceAmount',
|
|
302
|
+
'currency',
|
|
303
|
+
'source',
|
|
304
|
+
'lastBalanceUpdatedAt',
|
|
305
|
+
'connectionState',
|
|
306
|
+
])
|
|
307
|
+
&& typeof value.accountRef === 'string'
|
|
308
|
+
&& /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef)
|
|
309
|
+
&& isNullableNonEmptyString(value.accountName)
|
|
310
|
+
&& isNullableNonEmptyString(value.institutionName)
|
|
311
|
+
&& (value.accountType === 'current'
|
|
312
|
+
|| value.accountType === 'savings'
|
|
313
|
+
|| value.accountType === 'investments')
|
|
314
|
+
&& (value.ownership === 'personal' || value.ownership === 'joint')
|
|
315
|
+
&& (value.balanceAmount === null
|
|
316
|
+
|| (typeof value.balanceAmount === 'number' && Number.isFinite(value.balanceAmount)))
|
|
317
|
+
&& (value.currency === null || isCurrency(value.currency))
|
|
318
|
+
&& (value.source === 'connected' || value.source === 'manual')
|
|
319
|
+
&& (value.lastBalanceUpdatedAt === null
|
|
320
|
+
|| isIsoDateTime(value.lastBalanceUpdatedAt))
|
|
321
|
+
&& (value.connectionState === 'active'
|
|
322
|
+
|| value.connectionState === 'expired'
|
|
323
|
+
|| value.connectionState === 'manual'
|
|
324
|
+
|| value.connectionState === 'unknown'));
|
|
325
|
+
}
|
|
326
|
+
function isAccountsResponse(value) {
|
|
327
|
+
return (isObject(value)
|
|
328
|
+
&& hasOnlyFields(value, ['asOf', 'accounts'])
|
|
329
|
+
&& isIsoDateTime(value.asOf)
|
|
330
|
+
&& Array.isArray(value.accounts)
|
|
331
|
+
&& value.accounts.every(isAccount));
|
|
332
|
+
}
|
|
269
333
|
function isGoalsResponse(value) {
|
|
270
334
|
return (isObject(value)
|
|
271
335
|
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
@@ -287,19 +351,25 @@ function isGoalDeleteResponse(value) {
|
|
|
287
351
|
&& value.deletedGoalId.trim().length > 0);
|
|
288
352
|
}
|
|
289
353
|
export function parseApiResponse(command, value) {
|
|
290
|
-
const valid = command === '
|
|
291
|
-
?
|
|
292
|
-
: command === '
|
|
293
|
-
?
|
|
294
|
-
: command === '
|
|
295
|
-
?
|
|
296
|
-
: command === '
|
|
297
|
-
?
|
|
298
|
-
: command === '
|
|
299
|
-
?
|
|
300
|
-
: command === '
|
|
301
|
-
?
|
|
302
|
-
:
|
|
354
|
+
const valid = command === 'accounts'
|
|
355
|
+
? isAccountsResponse(value)
|
|
356
|
+
: command === 'categories'
|
|
357
|
+
? isCategoryResponse(value)
|
|
358
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
359
|
+
? isCategoryMutationResponse(value)
|
|
360
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
361
|
+
? isLineItemMutationResponse(value)
|
|
362
|
+
: command === 'transactions'
|
|
363
|
+
? isTransactionsResponse(value)
|
|
364
|
+
: command === 'assign'
|
|
365
|
+
? isAssignmentResponse(value)
|
|
366
|
+
: command === 'ask-partner'
|
|
367
|
+
? isPartnerResponse(value)
|
|
368
|
+
: command === 'goals-list'
|
|
369
|
+
? isGoalsResponse(value)
|
|
370
|
+
: command === 'goals-delete'
|
|
371
|
+
? isGoalDeleteResponse(value)
|
|
372
|
+
: isGoalMutationResponse(value);
|
|
303
373
|
if (!valid) {
|
|
304
374
|
const label = command === 'assign' ? 'assignment' : command;
|
|
305
375
|
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.5.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
|
},
|