@slothmoney/agent-cli 0.4.0 → 0.6.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 +24 -0
- package/README.md +100 -3
- package/dist/args.js +200 -9
- package/dist/category-metadata.js +11 -0
- package/dist/cli.js +231 -12
- package/dist/contracts.js +105 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.0 - 2026-08-08
|
|
6
|
+
|
|
7
|
+
- Expose goal-savings membership on account inventory rows and preview or apply
|
|
8
|
+
owner-authorized changes through opaque account references.
|
|
9
|
+
- Read cache-only linked investment portfolios with provider-native holdings,
|
|
10
|
+
quantities, valuations, currencies, and freshness metadata.
|
|
11
|
+
- Keep strict response validation, JSON-only stdout, command-specific help,
|
|
12
|
+
and clean-install package coverage synchronized with Agent API v1.
|
|
13
|
+
|
|
14
|
+
## 0.5.0 - 2026-08-07
|
|
15
|
+
|
|
16
|
+
- Create and rename custom categories, with existing icon and category type
|
|
17
|
+
validation and preview-only writes unless `--apply` is supplied.
|
|
18
|
+
- Create and rename personal or joint budget line items while preserving
|
|
19
|
+
historical snapshots and future plan amounts.
|
|
20
|
+
- Filter transactions directly by `--line-item-id`, including paired
|
|
21
|
+
`--category-id` matching for split assignments.
|
|
22
|
+
- Strictly validate category and line-item mutation responses and extend
|
|
23
|
+
command-specific and packed-binary help coverage.
|
|
24
|
+
|
|
25
|
+
## 0.4.0 - 2026-08-01
|
|
26
|
+
|
|
27
|
+
- Document that new Sloth Money personal access tokens are view-only by
|
|
28
|
+
default, and identify the CLI operations that require explicit write access.
|
|
5
29
|
- Add the read-only `sloth-agent accounts` command with strict runtime
|
|
6
30
|
validation for opaque references, ownership, native balances, sources, and
|
|
7
31
|
freshness metadata.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to inspect
|
|
3
|
+
Use your own agent to inspect accounts and investment holdings, 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.6.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`, `investments`,
|
|
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, change goal-savings account membership, 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
|
|
@@ -98,6 +104,8 @@ options, output, and examples:
|
|
|
98
104
|
sloth-agent auth login --help
|
|
99
105
|
sloth-agent accounts --help
|
|
100
106
|
sloth-agent categories --help
|
|
107
|
+
sloth-agent categories create --help
|
|
108
|
+
sloth-agent line-items create --help
|
|
101
109
|
sloth-agent transactions --help
|
|
102
110
|
sloth-agent assign --help
|
|
103
111
|
sloth-agent goals create --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,63 @@ 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
|
+
|
|
177
244
|
Read the existing Sloth account inventory:
|
|
178
245
|
|
|
179
246
|
```bash
|
|
@@ -183,11 +250,41 @@ sloth-agent accounts
|
|
|
183
250
|
The command is read-only and cache-only: it does not refresh linked banks or
|
|
184
251
|
change account data. Each result contains an opaque `accountRef`, personal or
|
|
185
252
|
joint ownership, connected or manual source, native balance/currency when
|
|
186
|
-
known, `lastBalanceUpdatedAt`, and `
|
|
253
|
+
known, `lastBalanceUpdatedAt`, `connectionState`, and `isGoalSavingsSource`.
|
|
254
|
+
Missing values are JSON
|
|
187
255
|
`null`; currencies are never converted or combined. Partner personal accounts
|
|
188
256
|
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
189
257
|
visibility rules.
|
|
190
258
|
|
|
259
|
+
Goal-savings changes are previews unless `--apply` is present. Only
|
|
260
|
+
caller-owned connected accounts can be changed; partner-owned shared accounts
|
|
261
|
+
and fixed manual accounts return an explanatory error.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
sloth-agent accounts update \
|
|
265
|
+
--account-ref sloth_account_v1_... \
|
|
266
|
+
--goal-savings-source true
|
|
267
|
+
|
|
268
|
+
sloth-agent accounts update \
|
|
269
|
+
--account-ref sloth_account_v1_... \
|
|
270
|
+
--goal-savings-source true \
|
|
271
|
+
--apply
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Read linked investment accounts and their cached holdings:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
sloth-agent investments
|
|
278
|
+
sloth-agent investments --account-ref sloth_account_v1_...
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Investment reads are cache-only and do not refresh a brokerage. Holding
|
|
282
|
+
quantities, unit prices, market values, currencies, and freshness are returned
|
|
283
|
+
in provider-native terms. They are not converted or guaranteed to reconcile
|
|
284
|
+
to an account total reported in another currency. Caller-owned personal and
|
|
285
|
+
joint linked investment accounts are included; partner-owned accounts, manual
|
|
286
|
+
holdings, and investment activities are not.
|
|
287
|
+
|
|
191
288
|
List your goals:
|
|
192
289
|
|
|
193
290
|
```bash
|
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,153 @@ 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 parseAccountRef(value) {
|
|
230
|
+
if (!/^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value)) {
|
|
231
|
+
throw new UsageError('--account-ref must be a valid accountRef from sloth-agent accounts');
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function parseAccounts(args, baseUrl) {
|
|
236
|
+
const subcommand = args.shift();
|
|
237
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
238
|
+
if (args.length > 0)
|
|
239
|
+
throw new UsageError(`Unknown accounts option: ${args[0]}`);
|
|
240
|
+
return withBaseUrl({ command: 'accounts' }, baseUrl);
|
|
241
|
+
}
|
|
242
|
+
if (subcommand === 'update') {
|
|
243
|
+
const { values, apply } = parseNamedOptions(args, 'accounts update', new Set(['--account-ref', '--goal-savings-source']));
|
|
244
|
+
const source = requiredOption(values, '--goal-savings-source', 'accounts update');
|
|
245
|
+
if (source !== 'true' && source !== 'false') {
|
|
246
|
+
throw new UsageError('--goal-savings-source must be true or false');
|
|
247
|
+
}
|
|
248
|
+
return withBaseUrl({
|
|
249
|
+
command: 'accounts-update',
|
|
250
|
+
accountRef: parseAccountRef(requiredOption(values, '--account-ref', 'accounts update')),
|
|
251
|
+
isGoalSavingsSource: source === 'true',
|
|
252
|
+
apply,
|
|
253
|
+
}, baseUrl);
|
|
254
|
+
}
|
|
255
|
+
if (subcommand.startsWith('-')) {
|
|
256
|
+
throw new UsageError(`Unknown accounts option: ${subcommand}`);
|
|
257
|
+
}
|
|
258
|
+
throw new UsageError(`Unknown accounts command: ${subcommand}`);
|
|
259
|
+
}
|
|
260
|
+
function parseInvestments(args, baseUrl) {
|
|
261
|
+
let accountRef;
|
|
262
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
263
|
+
const argument = args[index];
|
|
264
|
+
if (argument === '--account-ref') {
|
|
265
|
+
accountRef = setOnce(accountRef, parseAccountRef(readOptionValue(args, index, '--account-ref')), '--account-ref');
|
|
266
|
+
index += 1;
|
|
267
|
+
}
|
|
268
|
+
else if (argument.startsWith('--account-ref=')) {
|
|
269
|
+
accountRef = setOnce(accountRef, parseAccountRef(requireNonEmpty(argument.slice('--account-ref='.length), '--account-ref')), '--account-ref');
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
throw new UsageError(`Unknown investments option: ${argument}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
276
|
+
}
|
|
277
|
+
function parseCategories(args, baseUrl) {
|
|
278
|
+
const subcommand = args.shift();
|
|
279
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
280
|
+
if (args.length > 0)
|
|
281
|
+
throw new UsageError(`Unknown categories list option: ${args[0]}`);
|
|
282
|
+
return withBaseUrl({ command: 'categories' }, baseUrl);
|
|
283
|
+
}
|
|
284
|
+
if (subcommand === 'create') {
|
|
285
|
+
const { values, apply } = parseNamedOptions(args, 'categories create', new Set(['--name', '--icon-key', '--type']));
|
|
286
|
+
const iconKey = requiredOption(values, '--icon-key', 'categories create');
|
|
287
|
+
const categoryType = requiredOption(values, '--type', 'categories create');
|
|
288
|
+
if (!ICON_KEYS.includes(iconKey)) {
|
|
289
|
+
throw new UsageError(`--icon-key must be one of: ${ICON_KEYS.join(', ')}`);
|
|
290
|
+
}
|
|
291
|
+
if (!CATEGORY_TYPES.includes(categoryType)) {
|
|
292
|
+
throw new UsageError(`--type must be one of: ${CATEGORY_TYPES.join(', ')}`);
|
|
293
|
+
}
|
|
294
|
+
return withBaseUrl({
|
|
295
|
+
command: 'categories-create',
|
|
296
|
+
name: parseResourceName(requiredOption(values, '--name', 'categories create')),
|
|
297
|
+
iconKey: iconKey,
|
|
298
|
+
categoryType: categoryType,
|
|
299
|
+
apply,
|
|
300
|
+
}, baseUrl);
|
|
301
|
+
}
|
|
302
|
+
if (subcommand === 'rename') {
|
|
303
|
+
const { values, apply } = parseNamedOptions(args, 'categories rename', new Set(['--category-id', '--name']));
|
|
304
|
+
return withBaseUrl({
|
|
305
|
+
command: 'categories-rename',
|
|
306
|
+
categoryId: parseResourceId(requiredOption(values, '--category-id', 'categories rename'), '--category-id'),
|
|
307
|
+
name: parseResourceName(requiredOption(values, '--name', 'categories rename')),
|
|
308
|
+
apply,
|
|
309
|
+
}, baseUrl);
|
|
310
|
+
}
|
|
311
|
+
if (subcommand.startsWith('-')) {
|
|
312
|
+
throw new UsageError(`Unknown categories option: ${subcommand}`);
|
|
313
|
+
}
|
|
314
|
+
throw new UsageError(`Unknown categories command: ${subcommand}`);
|
|
315
|
+
}
|
|
316
|
+
function parseLineItems(args, baseUrl) {
|
|
317
|
+
const subcommand = args.shift();
|
|
318
|
+
if (subcommand !== 'create' && subcommand !== 'rename') {
|
|
319
|
+
throw new UsageError('line-items requires create or rename');
|
|
320
|
+
}
|
|
321
|
+
const allowed = new Set(['--scope', '--category-id', '--name']);
|
|
322
|
+
if (subcommand === 'rename')
|
|
323
|
+
allowed.add('--line-item-id');
|
|
324
|
+
const { values, apply } = parseNamedOptions(args, `line-items ${subcommand}`, allowed);
|
|
325
|
+
const scope = requiredOption(values, '--scope', `line-items ${subcommand}`);
|
|
326
|
+
if (scope !== 'personal' && scope !== 'joint') {
|
|
327
|
+
throw new UsageError('--scope must be personal or joint');
|
|
328
|
+
}
|
|
329
|
+
const common = {
|
|
330
|
+
scope: scope,
|
|
331
|
+
categoryId: parseResourceId(requiredOption(values, '--category-id', `line-items ${subcommand}`), '--category-id'),
|
|
332
|
+
name: parseResourceName(requiredOption(values, '--name', `line-items ${subcommand}`)),
|
|
333
|
+
apply,
|
|
334
|
+
};
|
|
335
|
+
if (subcommand === 'create') {
|
|
336
|
+
return withBaseUrl({ command: 'line-items-create', ...common }, baseUrl);
|
|
337
|
+
}
|
|
338
|
+
return withBaseUrl({
|
|
339
|
+
command: 'line-items-rename',
|
|
340
|
+
...common,
|
|
341
|
+
lineItemId: parseResourceId(requiredOption(values, '--line-item-id', 'line-items rename'), '--line-item-id'),
|
|
342
|
+
}, baseUrl);
|
|
343
|
+
}
|
|
170
344
|
function withBaseUrl(value, baseUrl) {
|
|
171
345
|
return baseUrl === undefined ? value : { ...value, baseUrl };
|
|
172
346
|
}
|
|
@@ -411,13 +585,30 @@ function helpTopic(argv) {
|
|
|
411
585
|
return 'goals-delete';
|
|
412
586
|
return 'goals';
|
|
413
587
|
}
|
|
588
|
+
if (command === 'categories') {
|
|
589
|
+
if (subcommand === 'create')
|
|
590
|
+
return 'categories-create';
|
|
591
|
+
if (subcommand === 'rename')
|
|
592
|
+
return 'categories-rename';
|
|
593
|
+
return 'categories';
|
|
594
|
+
}
|
|
595
|
+
if (command === 'line-items') {
|
|
596
|
+
if (subcommand === 'create')
|
|
597
|
+
return 'line-items-create';
|
|
598
|
+
if (subcommand === 'rename')
|
|
599
|
+
return 'line-items-rename';
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
414
602
|
if (command === 'accounts'
|
|
415
|
-
|| command === 'categories'
|
|
416
603
|
|| command === 'transactions'
|
|
417
604
|
|| command === 'assign'
|
|
418
605
|
|| command === 'ask-partner') {
|
|
606
|
+
if (command === 'accounts' && subcommand === 'update')
|
|
607
|
+
return 'accounts-update';
|
|
419
608
|
return command;
|
|
420
609
|
}
|
|
610
|
+
if (command === 'investments')
|
|
611
|
+
return 'investments';
|
|
421
612
|
return undefined;
|
|
422
613
|
}
|
|
423
614
|
export function parseArgs(argv) {
|
|
@@ -438,16 +629,16 @@ export function parseArgs(argv) {
|
|
|
438
629
|
return parseGoals(args, baseUrl);
|
|
439
630
|
}
|
|
440
631
|
if (command === 'categories') {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return
|
|
632
|
+
return parseCategories(args, baseUrl);
|
|
633
|
+
}
|
|
634
|
+
if (command === 'line-items') {
|
|
635
|
+
return parseLineItems(args, baseUrl);
|
|
445
636
|
}
|
|
446
637
|
if (command === 'accounts') {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return
|
|
638
|
+
return parseAccounts(args, baseUrl);
|
|
639
|
+
}
|
|
640
|
+
if (command === 'investments') {
|
|
641
|
+
return parseInvestments(args, baseUrl);
|
|
451
642
|
}
|
|
452
643
|
if (command === 'transactions') {
|
|
453
644
|
return withBaseUrl({ command, filters: parseTransactions(args) }, baseUrl);
|
|
@@ -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 { ICON_KEYS } from './category-metadata.js';
|
|
3
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.6.0';
|
|
7
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
8
9
|
const API_ORIGIN_HELP_LINES = [
|
|
9
10
|
'',
|
|
@@ -24,11 +25,18 @@ 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 accounts [--base-url URL]',
|
|
28
|
-
' sloth-agent
|
|
28
|
+
' sloth-agent accounts [list] [--base-url URL]',
|
|
29
|
+
' sloth-agent accounts update --account-ref REF --goal-savings-source true|false [--apply]',
|
|
30
|
+
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
31
|
+
' sloth-agent categories [list] [--base-url URL]',
|
|
32
|
+
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply]',
|
|
33
|
+
' sloth-agent categories rename --category-id ID --name NAME [--apply]',
|
|
34
|
+
' sloth-agent line-items create --scope personal|joint --category-id ID --name NAME [--apply]',
|
|
35
|
+
' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply]',
|
|
29
36
|
' sloth-agent transactions [--uncategorized[=true|false]] [--limit N]',
|
|
30
37
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
31
|
-
' [--account-id ID] [--category-id ID] [--
|
|
38
|
+
' [--account-id ID] [--category-id ID] [--line-item-id ID]',
|
|
39
|
+
' [--cursor CURSOR] [--base-url URL]',
|
|
32
40
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
33
41
|
' sloth-agent goals [list] [--base-url URL]',
|
|
34
42
|
' sloth-agent goals create --name NAME [--target-amount AMOUNT]',
|
|
@@ -51,6 +59,10 @@ export function usageText() {
|
|
|
51
59
|
'Environment:',
|
|
52
60
|
' SLOTH_AGENT_TOKEN Personal access token from Settings > Developer access',
|
|
53
61
|
' SLOTH_AGENT_API_BASE_URL Optional API origin; defaults to https://budget.slothmoney.app',
|
|
62
|
+
'',
|
|
63
|
+
'Access:',
|
|
64
|
+
' New tokens are view-only. Enable Allow changes in Sloth Money when the',
|
|
65
|
+
' CLI needs to call write endpoints.',
|
|
54
66
|
].join('\n');
|
|
55
67
|
}
|
|
56
68
|
export function authHelpText() {
|
|
@@ -89,6 +101,8 @@ export function authLoginHelpText() {
|
|
|
89
101
|
' Tokens must start with sloth_pat_v1_ and contain no whitespace.',
|
|
90
102
|
' Never pass a token as a command argument.',
|
|
91
103
|
' An existing stored credential is replaced only after remote validation succeeds.',
|
|
104
|
+
' Remote validation requires agent:read. Commands that write also require',
|
|
105
|
+
' agent:write, selected with Allow changes when the token is created.',
|
|
92
106
|
...API_ORIGIN_HELP_LINES,
|
|
93
107
|
'',
|
|
94
108
|
'Examples:',
|
|
@@ -154,7 +168,7 @@ export function categoriesHelpText() {
|
|
|
154
168
|
'Read categories and the personal and joint line items within them.',
|
|
155
169
|
'',
|
|
156
170
|
'Usage:',
|
|
157
|
-
' sloth-agent categories [--base-url URL]',
|
|
171
|
+
' sloth-agent categories [list] [--base-url URL]',
|
|
158
172
|
'',
|
|
159
173
|
'Options:',
|
|
160
174
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -182,6 +196,85 @@ export function categoriesHelpText() {
|
|
|
182
196
|
' jointLineItemsByCategoryId Joint child line items keyed by category ID',
|
|
183
197
|
].join('\n');
|
|
184
198
|
}
|
|
199
|
+
export function categoriesCreateHelpText() {
|
|
200
|
+
return [
|
|
201
|
+
'Sloth Agent CLI — categories create',
|
|
202
|
+
'',
|
|
203
|
+
'Create a custom category.',
|
|
204
|
+
'',
|
|
205
|
+
'Usage:',
|
|
206
|
+
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply] [--base-url URL]',
|
|
207
|
+
'',
|
|
208
|
+
'Required inputs:',
|
|
209
|
+
' --name NAME Category name, up to 200 characters.',
|
|
210
|
+
` --icon-key KEY One of: ${ICON_KEYS.join(', ')}.`,
|
|
211
|
+
' --type TYPE Needs, Debts, Savings & Investments, or Wants.',
|
|
212
|
+
'',
|
|
213
|
+
'Write behavior:',
|
|
214
|
+
' Without --apply, returns a JSON preview and makes no mutation request.',
|
|
215
|
+
' With --apply, creates the category globally and requires a write-enabled token.',
|
|
216
|
+
...API_ORIGIN_HELP_LINES,
|
|
217
|
+
'',
|
|
218
|
+
'Output:',
|
|
219
|
+
' JSON containing category.id, name, iconKey, categoryType, and source.',
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
export function categoriesRenameHelpText() {
|
|
223
|
+
return [
|
|
224
|
+
'Sloth Agent CLI — categories rename',
|
|
225
|
+
'',
|
|
226
|
+
'Rename a user-created category. Built-in categories are immutable.',
|
|
227
|
+
'',
|
|
228
|
+
'Usage:',
|
|
229
|
+
' sloth-agent categories rename --category-id ID --name NAME [--apply] [--base-url URL]',
|
|
230
|
+
'',
|
|
231
|
+
'Required inputs:',
|
|
232
|
+
' --category-id ID Custom category document ID.',
|
|
233
|
+
' --name NAME New category name, up to 200 characters.',
|
|
234
|
+
'',
|
|
235
|
+
'Write behavior:',
|
|
236
|
+
' Without --apply, returns a JSON preview and makes no mutation request.',
|
|
237
|
+
' With --apply, renames the canonical category and requires a write-enabled token.',
|
|
238
|
+
...API_ORIGIN_HELP_LINES,
|
|
239
|
+
'',
|
|
240
|
+
'Output:',
|
|
241
|
+
' JSON containing the renamed category.',
|
|
242
|
+
].join('\n');
|
|
243
|
+
}
|
|
244
|
+
function lineItemsMutationHelpText(operation) {
|
|
245
|
+
const rename = operation === 'rename';
|
|
246
|
+
return [
|
|
247
|
+
`Sloth Agent CLI — line-items ${operation}`,
|
|
248
|
+
'',
|
|
249
|
+
rename ? 'Rename a scoped budget line item.' : 'Create a scoped budget line item at zero.',
|
|
250
|
+
'',
|
|
251
|
+
'Usage:',
|
|
252
|
+
rename
|
|
253
|
+
? ' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply] [--base-url URL]'
|
|
254
|
+
: ' sloth-agent line-items create --scope personal|joint --category-id ID --name NAME [--apply] [--base-url URL]',
|
|
255
|
+
'',
|
|
256
|
+
'Required inputs:',
|
|
257
|
+
' --scope SCOPE personal or joint.',
|
|
258
|
+
' --category-id ID Parent category ID.',
|
|
259
|
+
...(rename ? [' --line-item-id ID Existing child line-item ID.'] : []),
|
|
260
|
+
' --name NAME Line-item name, up to 200 characters.',
|
|
261
|
+
'',
|
|
262
|
+
'Write behavior:',
|
|
263
|
+
' Without --apply, returns a JSON preview and makes no mutation request.',
|
|
264
|
+
' With --apply, updates the current period and explicit future plans.',
|
|
265
|
+
' Historical snapshots remain unchanged. A write-enabled token is required.',
|
|
266
|
+
...API_ORIGIN_HELP_LINES,
|
|
267
|
+
'',
|
|
268
|
+
'Output:',
|
|
269
|
+
' JSON containing scope, categoryId, and lineItem.id and name.',
|
|
270
|
+
].join('\n');
|
|
271
|
+
}
|
|
272
|
+
export function lineItemsCreateHelpText() {
|
|
273
|
+
return lineItemsMutationHelpText('create');
|
|
274
|
+
}
|
|
275
|
+
export function lineItemsRenameHelpText() {
|
|
276
|
+
return lineItemsMutationHelpText('rename');
|
|
277
|
+
}
|
|
185
278
|
export function accountsHelpText() {
|
|
186
279
|
return [
|
|
187
280
|
'Sloth Agent CLI — accounts',
|
|
@@ -189,7 +282,7 @@ export function accountsHelpText() {
|
|
|
189
282
|
'Read the existing Sloth account inventory known to the authenticated user.',
|
|
190
283
|
'',
|
|
191
284
|
'Usage:',
|
|
192
|
-
' sloth-agent accounts [--base-url URL]',
|
|
285
|
+
' sloth-agent accounts [list] [--base-url URL]',
|
|
193
286
|
'',
|
|
194
287
|
'Options:',
|
|
195
288
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -205,6 +298,57 @@ export function accountsHelpText() {
|
|
|
205
298
|
' accounts[].ownership personal or joint',
|
|
206
299
|
' accounts[].balanceAmount and currency in the native currency when known',
|
|
207
300
|
' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
|
|
301
|
+
' accounts[].isGoalSavingsSource whether the owner uses it for goal savings',
|
|
302
|
+
].join('\n');
|
|
303
|
+
}
|
|
304
|
+
export function accountsUpdateHelpText() {
|
|
305
|
+
return [
|
|
306
|
+
'Sloth Agent CLI — accounts update',
|
|
307
|
+
'',
|
|
308
|
+
'Preview or update whether an owned connected account is used for goal savings.',
|
|
309
|
+
'',
|
|
310
|
+
'Usage:',
|
|
311
|
+
' sloth-agent accounts update --account-ref REF --goal-savings-source true|false [--apply] [--base-url URL]',
|
|
312
|
+
'',
|
|
313
|
+
'Required inputs:',
|
|
314
|
+
' --account-ref REF Opaque accountRef from sloth-agent accounts.',
|
|
315
|
+
' --goal-savings-source true|false Enable or disable goal-savings membership.',
|
|
316
|
+
'',
|
|
317
|
+
'Write behavior:',
|
|
318
|
+
' Without --apply, returns a JSON preview without credentials or a network request.',
|
|
319
|
+
' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
|
|
320
|
+
' Partner-owned shared accounts and manual accounts cannot be changed.',
|
|
321
|
+
' Unknown, disconnected, or inaccessible references return Account not found.',
|
|
322
|
+
...API_ORIGIN_HELP_LINES,
|
|
323
|
+
'',
|
|
324
|
+
'Output:',
|
|
325
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
326
|
+
' Apply mode returns changed and the complete persisted account.',
|
|
327
|
+
].join('\n');
|
|
328
|
+
}
|
|
329
|
+
export function investmentsHelpText() {
|
|
330
|
+
return [
|
|
331
|
+
'Sloth Agent CLI — investments',
|
|
332
|
+
'',
|
|
333
|
+
'Read linked investment accounts and their cached provider-native holdings.',
|
|
334
|
+
'',
|
|
335
|
+
'Usage:',
|
|
336
|
+
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
337
|
+
'',
|
|
338
|
+
'Options:',
|
|
339
|
+
' --account-ref REF Optional. Return one linked investment account.',
|
|
340
|
+
' --base-url URL Optional. Override the API origin.',
|
|
341
|
+
' -h, --help Show this help.',
|
|
342
|
+
...API_ORIGIN_HELP_LINES,
|
|
343
|
+
'',
|
|
344
|
+
'Access:',
|
|
345
|
+
' This command requires agent:read and is read-only and cache-only; it never refreshes a brokerage.',
|
|
346
|
+
' An unknown or non-investment filter returns Investment account not found.',
|
|
347
|
+
'',
|
|
348
|
+
'Output:',
|
|
349
|
+
' investmentAccounts contains account totals and nested holdings.',
|
|
350
|
+
' Holding quantities, prices, market values, currencies, and freshness are',
|
|
351
|
+
' provider-native and are not converted or guaranteed to reconcile to totals.',
|
|
208
352
|
].join('\n');
|
|
209
353
|
}
|
|
210
354
|
export function transactionsHelpText() {
|
|
@@ -224,6 +368,7 @@ export function transactionsHelpText() {
|
|
|
224
368
|
' --q TEXT Optional. Search transactions by text.',
|
|
225
369
|
' --account-id ID Optional. Filter by account ID.',
|
|
226
370
|
' --category-id ID Optional. Filter by category ID.',
|
|
371
|
+
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
227
372
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
228
373
|
' --cursor CURSOR Optional. Continue from a previous nextCursor.',
|
|
229
374
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -273,6 +418,7 @@ export function assignHelpText() {
|
|
|
273
418
|
' A successful preview does not guarantee that applying it will succeed.',
|
|
274
419
|
' With --apply, assignments are best-effort; any failed item makes the command',
|
|
275
420
|
' exit with code 1 while the complete result remains available on stdout.',
|
|
421
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
276
422
|
'',
|
|
277
423
|
'Input:',
|
|
278
424
|
' The top-level object must contain an assignments array.',
|
|
@@ -373,6 +519,7 @@ export function goalsCreateHelpText() {
|
|
|
373
519
|
'',
|
|
374
520
|
'Safety:',
|
|
375
521
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
522
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
376
523
|
' New goals are private to the owner and appended to the existing goal order.',
|
|
377
524
|
'',
|
|
378
525
|
'Example:',
|
|
@@ -418,6 +565,7 @@ export function goalsUpdateHelpText() {
|
|
|
418
565
|
'',
|
|
419
566
|
'Safety:',
|
|
420
567
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
568
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
421
569
|
'',
|
|
422
570
|
'Output:',
|
|
423
571
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
@@ -442,6 +590,7 @@ export function goalsDeleteHelpText() {
|
|
|
442
590
|
'',
|
|
443
591
|
'Safety:',
|
|
444
592
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
593
|
+
' Applying requires a write-enabled token created with Allow changes.',
|
|
445
594
|
' Applying deletion also removes the goal from forecast assignments and',
|
|
446
595
|
' removes its goal drift history. This operation cannot be undone.',
|
|
447
596
|
'',
|
|
@@ -467,6 +616,7 @@ export function askPartnerHelpText() {
|
|
|
467
616
|
'',
|
|
468
617
|
'Write behavior:',
|
|
469
618
|
' Running this command creates the request immediately. There is no preview mode.',
|
|
619
|
+
' Creating the request requires a write-enabled token created with Allow changes.',
|
|
470
620
|
'',
|
|
471
621
|
'Example:',
|
|
472
622
|
' sloth-agent ask-partner --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE',
|
|
@@ -484,7 +634,13 @@ export function commandHelpText(topic) {
|
|
|
484
634
|
'auth-status': authStatusHelpText,
|
|
485
635
|
'auth-logout': authLogoutHelpText,
|
|
486
636
|
accounts: accountsHelpText,
|
|
637
|
+
'accounts-update': accountsUpdateHelpText,
|
|
638
|
+
investments: investmentsHelpText,
|
|
487
639
|
categories: categoriesHelpText,
|
|
640
|
+
'categories-create': categoriesCreateHelpText,
|
|
641
|
+
'categories-rename': categoriesRenameHelpText,
|
|
642
|
+
'line-items-create': lineItemsCreateHelpText,
|
|
643
|
+
'line-items-rename': lineItemsRenameHelpText,
|
|
488
644
|
transactions: transactionsHelpText,
|
|
489
645
|
assign: assignHelpText,
|
|
490
646
|
goals: goalsHelpText,
|
|
@@ -582,6 +738,8 @@ function buildTransactionsQuery(filters) {
|
|
|
582
738
|
params.set('accountId', filters.accountId);
|
|
583
739
|
if (filters.categoryId !== undefined)
|
|
584
740
|
params.set('categoryId', filters.categoryId);
|
|
741
|
+
if (filters.lineItemId !== undefined)
|
|
742
|
+
params.set('lineItemId', filters.lineItemId);
|
|
585
743
|
if (filters.assignmentScope !== undefined) {
|
|
586
744
|
params.set('assignmentScope', filters.assignmentScope);
|
|
587
745
|
}
|
|
@@ -730,9 +888,66 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
730
888
|
});
|
|
731
889
|
return 0;
|
|
732
890
|
}
|
|
891
|
+
if (parsed.command === 'accounts-update' && !parsed.apply) {
|
|
892
|
+
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
893
|
+
writeJson(writeStdout, {
|
|
894
|
+
dryRun: true,
|
|
895
|
+
endpoint,
|
|
896
|
+
method: 'PATCH',
|
|
897
|
+
payload: { isGoalSavingsSource: parsed.isGoalSavingsSource },
|
|
898
|
+
});
|
|
899
|
+
return 0;
|
|
900
|
+
}
|
|
733
901
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
734
902
|
token = credential.token;
|
|
735
903
|
const headers = requestHeaders(token);
|
|
904
|
+
if (parsed.command === 'accounts-update') {
|
|
905
|
+
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
906
|
+
const payload = { isGoalSavingsSource: parsed.isGoalSavingsSource };
|
|
907
|
+
const response = await fetchImplementation(endpoint, {
|
|
908
|
+
method: 'PATCH',
|
|
909
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
910
|
+
body: JSON.stringify(payload),
|
|
911
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
912
|
+
});
|
|
913
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
914
|
+
writeJson(writeStdout, data);
|
|
915
|
+
return 0;
|
|
916
|
+
}
|
|
917
|
+
if (parsed.command === 'categories-create'
|
|
918
|
+
|| parsed.command === 'categories-rename'
|
|
919
|
+
|| parsed.command === 'line-items-create'
|
|
920
|
+
|| parsed.command === 'line-items-rename') {
|
|
921
|
+
const isCategory = parsed.command.startsWith('categories-');
|
|
922
|
+
const isCreate = parsed.command.endsWith('-create');
|
|
923
|
+
const resourceId = parsed.command === 'categories-rename'
|
|
924
|
+
? parsed.categoryId
|
|
925
|
+
: parsed.command === 'line-items-rename'
|
|
926
|
+
? parsed.lineItemId
|
|
927
|
+
: null;
|
|
928
|
+
const endpoint = isCreate
|
|
929
|
+
? `${baseUrl}/api/agent/v1/${isCategory ? 'categories' : 'line-items'}`
|
|
930
|
+
: `${baseUrl}/api/agent/v1/${isCategory ? 'categories' : 'line-items'}/${encodeURIComponent(resourceId)}`;
|
|
931
|
+
const payload = parsed.command === 'categories-create'
|
|
932
|
+
? { name: parsed.name, iconKey: parsed.iconKey, categoryType: parsed.categoryType }
|
|
933
|
+
: parsed.command === 'categories-rename'
|
|
934
|
+
? { name: parsed.name }
|
|
935
|
+
: { scope: parsed.scope, categoryId: parsed.categoryId, name: parsed.name };
|
|
936
|
+
const method = isCreate ? 'POST' : 'PATCH';
|
|
937
|
+
if (!parsed.apply) {
|
|
938
|
+
writeJson(writeStdout, { dryRun: true, endpoint, method, payload });
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
const response = await fetchImplementation(endpoint, {
|
|
942
|
+
method,
|
|
943
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
944
|
+
body: JSON.stringify(payload),
|
|
945
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
946
|
+
});
|
|
947
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
948
|
+
writeJson(writeStdout, data);
|
|
949
|
+
return 0;
|
|
950
|
+
}
|
|
736
951
|
if (parsed.command === 'goals-create') {
|
|
737
952
|
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
738
953
|
const payload = {
|
|
@@ -867,12 +1082,16 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
867
1082
|
}
|
|
868
1083
|
const path = parsed.command === 'accounts'
|
|
869
1084
|
? '/api/agent/v1/accounts'
|
|
870
|
-
: parsed.command === '
|
|
871
|
-
?
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1085
|
+
: parsed.command === 'investments'
|
|
1086
|
+
? `/api/agent/v1/investments${parsed.accountRef
|
|
1087
|
+
? `?${new URLSearchParams({ accountRef: parsed.accountRef }).toString()}`
|
|
1088
|
+
: ''}`
|
|
1089
|
+
: parsed.command === 'categories'
|
|
1090
|
+
? '/api/agent/v1/categories'
|
|
1091
|
+
: `/api/agent/v1/transactions${(() => {
|
|
1092
|
+
const query = buildTransactionsQuery(parsed.filters);
|
|
1093
|
+
return query ? `?${query}` : '';
|
|
1094
|
+
})()}`;
|
|
876
1095
|
const response = await fetchImplementation(`${baseUrl}${path}`, {
|
|
877
1096
|
method: 'GET',
|
|
878
1097
|
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'
|
|
@@ -272,6 +303,7 @@ function isAccount(value) {
|
|
|
272
303
|
'source',
|
|
273
304
|
'lastBalanceUpdatedAt',
|
|
274
305
|
'connectionState',
|
|
306
|
+
'isGoalSavingsSource',
|
|
275
307
|
])
|
|
276
308
|
&& typeof value.accountRef === 'string'
|
|
277
309
|
&& /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef)
|
|
@@ -290,7 +322,8 @@ function isAccount(value) {
|
|
|
290
322
|
&& (value.connectionState === 'active'
|
|
291
323
|
|| value.connectionState === 'expired'
|
|
292
324
|
|| value.connectionState === 'manual'
|
|
293
|
-
|| value.connectionState === 'unknown')
|
|
325
|
+
|| value.connectionState === 'unknown')
|
|
326
|
+
&& typeof value.isGoalSavingsSource === 'boolean');
|
|
294
327
|
}
|
|
295
328
|
function isAccountsResponse(value) {
|
|
296
329
|
return (isObject(value)
|
|
@@ -299,6 +332,56 @@ function isAccountsResponse(value) {
|
|
|
299
332
|
&& Array.isArray(value.accounts)
|
|
300
333
|
&& value.accounts.every(isAccount));
|
|
301
334
|
}
|
|
335
|
+
function isAccountMutationResponse(value) {
|
|
336
|
+
return (isObject(value)
|
|
337
|
+
&& hasOnlyFields(value, ['changed', 'account'])
|
|
338
|
+
&& typeof value.changed === 'boolean'
|
|
339
|
+
&& isAccount(value.account));
|
|
340
|
+
}
|
|
341
|
+
function isInvestmentHolding(value) {
|
|
342
|
+
return (isObject(value)
|
|
343
|
+
&& hasOnlyFields(value, [
|
|
344
|
+
'instrumentType',
|
|
345
|
+
'symbol',
|
|
346
|
+
'name',
|
|
347
|
+
'units',
|
|
348
|
+
'unitPriceAmount',
|
|
349
|
+
'marketValueAmount',
|
|
350
|
+
'currency',
|
|
351
|
+
'providerFreshnessAsOf',
|
|
352
|
+
'syncedAt',
|
|
353
|
+
])
|
|
354
|
+
&& typeof value.instrumentType === 'string'
|
|
355
|
+
&& value.instrumentType.trim().length > 0
|
|
356
|
+
&& (value.symbol === null || (typeof value.symbol === 'string' && value.symbol.trim().length > 0))
|
|
357
|
+
&& typeof value.name === 'string'
|
|
358
|
+
&& value.name.trim().length > 0
|
|
359
|
+
&& typeof value.units === 'number'
|
|
360
|
+
&& Number.isFinite(value.units)
|
|
361
|
+
&& typeof value.unitPriceAmount === 'number'
|
|
362
|
+
&& Number.isFinite(value.unitPriceAmount)
|
|
363
|
+
&& typeof value.marketValueAmount === 'number'
|
|
364
|
+
&& Number.isFinite(value.marketValueAmount)
|
|
365
|
+
&& isCurrency(value.currency)
|
|
366
|
+
&& (value.providerFreshnessAsOf === null || isIsoDateTime(value.providerFreshnessAsOf))
|
|
367
|
+
&& isIsoDateTime(value.syncedAt));
|
|
368
|
+
}
|
|
369
|
+
function isInvestmentsResponse(value) {
|
|
370
|
+
return (isObject(value)
|
|
371
|
+
&& hasOnlyFields(value, ['asOf', 'investmentAccounts'])
|
|
372
|
+
&& isIsoDateTime(value.asOf)
|
|
373
|
+
&& Array.isArray(value.investmentAccounts)
|
|
374
|
+
&& value.investmentAccounts.every((account) => {
|
|
375
|
+
if (!isObject(account))
|
|
376
|
+
return false;
|
|
377
|
+
const { holdings, ...baseAccount } = account;
|
|
378
|
+
return (isAccount(baseAccount)
|
|
379
|
+
&& account.accountType === 'investments'
|
|
380
|
+
&& account.source === 'connected'
|
|
381
|
+
&& Array.isArray(holdings)
|
|
382
|
+
&& holdings.every(isInvestmentHolding));
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
302
385
|
function isGoalsResponse(value) {
|
|
303
386
|
return (isObject(value)
|
|
304
387
|
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
@@ -322,19 +405,27 @@ function isGoalDeleteResponse(value) {
|
|
|
322
405
|
export function parseApiResponse(command, value) {
|
|
323
406
|
const valid = command === 'accounts'
|
|
324
407
|
? isAccountsResponse(value)
|
|
325
|
-
: command === '
|
|
326
|
-
?
|
|
327
|
-
: command === '
|
|
328
|
-
?
|
|
329
|
-
: command === '
|
|
330
|
-
?
|
|
331
|
-
: command === '
|
|
332
|
-
?
|
|
333
|
-
: command === '
|
|
334
|
-
?
|
|
335
|
-
: command === '
|
|
336
|
-
?
|
|
337
|
-
:
|
|
408
|
+
: command === 'accounts-update'
|
|
409
|
+
? isAccountMutationResponse(value)
|
|
410
|
+
: command === 'investments'
|
|
411
|
+
? isInvestmentsResponse(value)
|
|
412
|
+
: command === 'categories'
|
|
413
|
+
? isCategoryResponse(value)
|
|
414
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
415
|
+
? isCategoryMutationResponse(value)
|
|
416
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
417
|
+
? isLineItemMutationResponse(value)
|
|
418
|
+
: command === 'transactions'
|
|
419
|
+
? isTransactionsResponse(value)
|
|
420
|
+
: command === 'assign'
|
|
421
|
+
? isAssignmentResponse(value)
|
|
422
|
+
: command === 'ask-partner'
|
|
423
|
+
? isPartnerResponse(value)
|
|
424
|
+
: command === 'goals-list'
|
|
425
|
+
? isGoalsResponse(value)
|
|
426
|
+
: command === 'goals-delete'
|
|
427
|
+
? isGoalDeleteResponse(value)
|
|
428
|
+
: isGoalMutationResponse(value);
|
|
338
429
|
if (!valid) {
|
|
339
430
|
const label = command === 'assign' ? 'assignment' : command;
|
|
340
431
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|