@slothmoney/agent-cli 0.2.0 → 0.3.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 +22 -0
- package/README.md +159 -41
- package/dist/args.js +302 -2
- package/dist/cli.js +607 -5
- package/dist/contracts.js +96 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
|
-
import { parseApiResponse, validateAssignmentPayload, } from './contracts.js';
|
|
3
|
+
import { parseApiResponse, validateAssignmentPayload, validateJointBudgetSettingsResponse, } from './contracts.js';
|
|
4
4
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
5
5
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
6
|
-
export const CLI_VERSION = '0.
|
|
6
|
+
export const CLI_VERSION = '0.3.0';
|
|
7
7
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
const API_ORIGIN_HELP_LINES = [
|
|
9
|
+
'',
|
|
10
|
+
'API origin:',
|
|
11
|
+
' --base-url overrides SLOTH_AGENT_API_BASE_URL; if neither is set, the',
|
|
12
|
+
' origin defaults to https://budget.slothmoney.app.',
|
|
13
|
+
' Use an origin-only URL with no credentials, path, query, or fragment.',
|
|
14
|
+
' HTTPS is required except for localhost development.',
|
|
15
|
+
];
|
|
8
16
|
export function usageText() {
|
|
9
17
|
return [
|
|
10
18
|
'Sloth Agent CLI',
|
|
11
19
|
'',
|
|
12
20
|
'Usage:',
|
|
21
|
+
' sloth-agent <command> [options]',
|
|
22
|
+
'',
|
|
23
|
+
'Commands:',
|
|
13
24
|
' sloth-agent auth login [--token-stdin | --from-env] [--base-url URL]',
|
|
14
25
|
' sloth-agent auth status [--base-url URL]',
|
|
15
26
|
' sloth-agent auth logout [--base-url URL]',
|
|
@@ -18,15 +29,475 @@ export function usageText() {
|
|
|
18
29
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
19
30
|
' [--account-id ID] [--category-id ID] [--cursor CURSOR] [--base-url URL]',
|
|
20
31
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
32
|
+
' sloth-agent joint-budget-settings [--include-shared-personal-transactions=true|false] [--apply]',
|
|
33
|
+
' sloth-agent goals [list] [--base-url URL]',
|
|
34
|
+
' sloth-agent goals create --name NAME [--target-amount AMOUNT]',
|
|
35
|
+
' [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
36
|
+
' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
|
|
37
|
+
' sloth-agent goals delete --goal-id ID [--apply] [--base-url URL]',
|
|
21
38
|
' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
|
|
22
|
-
'
|
|
23
|
-
'
|
|
39
|
+
'',
|
|
40
|
+
'Help:',
|
|
41
|
+
' Run sloth-agent <command> --help for options, inputs, output, and examples.',
|
|
42
|
+
' Auth and goal subcommands also have help, for example:',
|
|
43
|
+
' sloth-agent auth login --help',
|
|
44
|
+
' sloth-agent goals update --help',
|
|
45
|
+
'',
|
|
46
|
+
'Global options:',
|
|
47
|
+
' -h, --help Show top-level or command-specific help',
|
|
48
|
+
' -V, --version Show the CLI version',
|
|
49
|
+
' --base-url URL Use a different API origin',
|
|
24
50
|
'',
|
|
25
51
|
'Environment:',
|
|
26
52
|
' SLOTH_AGENT_TOKEN Personal access token from Settings > Developer access',
|
|
27
53
|
' SLOTH_AGENT_API_BASE_URL Optional API origin; defaults to https://budget.slothmoney.app',
|
|
28
54
|
].join('\n');
|
|
29
55
|
}
|
|
56
|
+
export function authHelpText() {
|
|
57
|
+
return [
|
|
58
|
+
'Sloth Agent CLI — auth',
|
|
59
|
+
'',
|
|
60
|
+
'Manage the personal access token used for Agent API requests.',
|
|
61
|
+
'',
|
|
62
|
+
'Commands:',
|
|
63
|
+
' sloth-agent auth login Validate and store a personal access token',
|
|
64
|
+
' sloth-agent auth status Check the active credential with a live API request',
|
|
65
|
+
' sloth-agent auth logout Remove the token from the native credential store',
|
|
66
|
+
'',
|
|
67
|
+
'Help:',
|
|
68
|
+
' Run sloth-agent auth <command> --help for command-specific details.',
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
export function authLoginHelpText() {
|
|
72
|
+
return [
|
|
73
|
+
'Sloth Agent CLI — auth login',
|
|
74
|
+
'',
|
|
75
|
+
'Validate a personal access token, then store it in the native credential store.',
|
|
76
|
+
'',
|
|
77
|
+
'Usage:',
|
|
78
|
+
' sloth-agent auth login [--token-stdin | --from-env] [--base-url URL]',
|
|
79
|
+
'',
|
|
80
|
+
'Options:',
|
|
81
|
+
' --token-stdin Optional. Read the token from stdin.',
|
|
82
|
+
' --from-env Optional. Read the token from SLOTH_AGENT_TOKEN.',
|
|
83
|
+
' --base-url URL Optional. Override the API origin.',
|
|
84
|
+
' -h, --help Show this help.',
|
|
85
|
+
'',
|
|
86
|
+
'Input:',
|
|
87
|
+
' With no input option, login uses a hidden prompt and requires an interactive TTY.',
|
|
88
|
+
' --token-stdin and --from-env are mutually exclusive.',
|
|
89
|
+
' Tokens must start with sloth_pat_v1_ and contain no whitespace.',
|
|
90
|
+
' Never pass a token as a command argument.',
|
|
91
|
+
' An existing stored credential is replaced only after remote validation succeeds.',
|
|
92
|
+
...API_ORIGIN_HELP_LINES,
|
|
93
|
+
'',
|
|
94
|
+
'Examples:',
|
|
95
|
+
' sloth-agent auth login',
|
|
96
|
+
' sloth-agent auth login --token-stdin',
|
|
97
|
+
' sloth-agent auth login --from-env',
|
|
98
|
+
'',
|
|
99
|
+
'Output:',
|
|
100
|
+
' JSON describing the API origin, stored state, and active credential source.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
export function authStatusHelpText() {
|
|
104
|
+
return [
|
|
105
|
+
'Sloth Agent CLI — auth status',
|
|
106
|
+
'',
|
|
107
|
+
'Check the active credential with a live API request.',
|
|
108
|
+
'',
|
|
109
|
+
'Usage:',
|
|
110
|
+
' sloth-agent auth status [--base-url URL]',
|
|
111
|
+
'',
|
|
112
|
+
'Options:',
|
|
113
|
+
' --base-url URL Optional. Override the API origin.',
|
|
114
|
+
' -h, --help Show this help.',
|
|
115
|
+
...API_ORIGIN_HELP_LINES,
|
|
116
|
+
'',
|
|
117
|
+
'Example:',
|
|
118
|
+
' sloth-agent auth status',
|
|
119
|
+
'',
|
|
120
|
+
'Output:',
|
|
121
|
+
' JSON containing origin, source, a masked token suffix, and remoteStatus.',
|
|
122
|
+
' Checking status updates the token last-used time.',
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
125
|
+
export function authLogoutHelpText() {
|
|
126
|
+
return [
|
|
127
|
+
'Sloth Agent CLI — auth logout',
|
|
128
|
+
'',
|
|
129
|
+
'Remove the token for an API origin from the native credential store.',
|
|
130
|
+
'',
|
|
131
|
+
'Usage:',
|
|
132
|
+
' sloth-agent auth logout [--base-url URL]',
|
|
133
|
+
'',
|
|
134
|
+
'Options:',
|
|
135
|
+
' --base-url URL Optional. Override the API origin.',
|
|
136
|
+
' -h, --help Show this help.',
|
|
137
|
+
...API_ORIGIN_HELP_LINES,
|
|
138
|
+
'',
|
|
139
|
+
'Important:',
|
|
140
|
+
' Logout does not revoke the token remotely or unset SLOTH_AGENT_TOKEN.',
|
|
141
|
+
' Revoke the token in Sloth Money Settings > Developer access.',
|
|
142
|
+
'',
|
|
143
|
+
'Example:',
|
|
144
|
+
' sloth-agent auth logout',
|
|
145
|
+
'',
|
|
146
|
+
'Output:',
|
|
147
|
+
' JSON describing local removal, any environment override, and revocation state.',
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
export function categoriesHelpText() {
|
|
151
|
+
return [
|
|
152
|
+
'Sloth Agent CLI — categories',
|
|
153
|
+
'',
|
|
154
|
+
'Read categories and the personal and joint line items within them.',
|
|
155
|
+
'',
|
|
156
|
+
'Usage:',
|
|
157
|
+
' sloth-agent categories [--base-url URL]',
|
|
158
|
+
'',
|
|
159
|
+
'Options:',
|
|
160
|
+
' --base-url URL Optional. Override the API origin.',
|
|
161
|
+
' -h, --help Show this help.',
|
|
162
|
+
...API_ORIGIN_HELP_LINES,
|
|
163
|
+
'',
|
|
164
|
+
'Budget taxonomy:',
|
|
165
|
+
' A category is the broader parent.',
|
|
166
|
+
' A line item is a child within one category.',
|
|
167
|
+
' Line-item names such as "Other" may repeat. Preserve the full choice as',
|
|
168
|
+
' (scope, categoryId, lineItemId), using the personal or joint line-item',
|
|
169
|
+
' map matching the transaction scope.',
|
|
170
|
+
'',
|
|
171
|
+
'Examples:',
|
|
172
|
+
' sloth-agent categories',
|
|
173
|
+
' Bills → Other',
|
|
174
|
+
' Subscriptions → Other',
|
|
175
|
+
'',
|
|
176
|
+
'Access:',
|
|
177
|
+
' This command is read-only.',
|
|
178
|
+
'',
|
|
179
|
+
'Output:',
|
|
180
|
+
' categories Parent categories',
|
|
181
|
+
' personalLineItemsByCategoryId Personal child line items keyed by category ID',
|
|
182
|
+
' jointLineItemsByCategoryId Joint child line items keyed by category ID',
|
|
183
|
+
].join('\n');
|
|
184
|
+
}
|
|
185
|
+
export function transactionsHelpText() {
|
|
186
|
+
return [
|
|
187
|
+
'Sloth Agent CLI — transactions',
|
|
188
|
+
'',
|
|
189
|
+
'Read transactions, optionally filtered or paginated.',
|
|
190
|
+
'',
|
|
191
|
+
'Usage:',
|
|
192
|
+
' sloth-agent transactions [options]',
|
|
193
|
+
'',
|
|
194
|
+
'Options:',
|
|
195
|
+
' --uncategorized[=true|false] Optional. Filter by state; with no value, use true.',
|
|
196
|
+
' --limit N Optional. Integer from 1 to 200; omit for API default.',
|
|
197
|
+
' --start-date YYYY-MM-DD Optional. Include transactions on or after this date.',
|
|
198
|
+
' --end-date YYYY-MM-DD Optional. Include transactions on or before this date.',
|
|
199
|
+
' --q TEXT Optional. Search transactions by text.',
|
|
200
|
+
' --account-id ID Optional. Filter by account ID.',
|
|
201
|
+
' --category-id ID Optional. Filter by category ID.',
|
|
202
|
+
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
203
|
+
' --cursor CURSOR Optional. Continue from a previous nextCursor.',
|
|
204
|
+
' --base-url URL Optional. Override the API origin.',
|
|
205
|
+
' -h, --help Show this help.',
|
|
206
|
+
...API_ORIGIN_HELP_LINES,
|
|
207
|
+
'',
|
|
208
|
+
'Constraints:',
|
|
209
|
+
' All filters are omitted by default.',
|
|
210
|
+
' --end-date must not be before --start-date.',
|
|
211
|
+
' This command is read-only.',
|
|
212
|
+
'',
|
|
213
|
+
'Output:',
|
|
214
|
+
' JSON containing transactions and nextCursor. Use nextCursor with --cursor',
|
|
215
|
+
' to request the next page. A null nextCursor means there are no more pages.',
|
|
216
|
+
'',
|
|
217
|
+
'Examples:',
|
|
218
|
+
' sloth-agent transactions --uncategorized --limit 50',
|
|
219
|
+
' sloth-agent transactions --assignment-scope joint --uncategorized',
|
|
220
|
+
' sloth-agent transactions --q "tesco" --start-date 2026-05-01 --end-date 2026-05-31',
|
|
221
|
+
].join('\n');
|
|
222
|
+
}
|
|
223
|
+
export function assignHelpText() {
|
|
224
|
+
return [
|
|
225
|
+
'Sloth Agent CLI — assign',
|
|
226
|
+
'',
|
|
227
|
+
'An assignment categorises an existing transaction e.g. assigning category Groceries to a transaction.',
|
|
228
|
+
'Validate, preview, or apply category assignments from a JSON file.',
|
|
229
|
+
'',
|
|
230
|
+
'Usage:',
|
|
231
|
+
' sloth-agent assign --input FILE [--apply] [--base-url URL]',
|
|
232
|
+
'',
|
|
233
|
+
'Options:',
|
|
234
|
+
' --input FILE Required. JSON assignment file containing 1 to 100 assignments.',
|
|
235
|
+
' --apply Optional. Write assignments to Sloth Money.',
|
|
236
|
+
' --base-url URL Optional. Override the API origin.',
|
|
237
|
+
' -h, --help Show this help.',
|
|
238
|
+
...API_ORIGIN_HELP_LINES,
|
|
239
|
+
'',
|
|
240
|
+
'Safety:',
|
|
241
|
+
' Without --apply, the command checks that the file is valid and returns',
|
|
242
|
+
' the payload it would send. It does not contact Sloth Money, verify the',
|
|
243
|
+
' transactionRef or category values, or write anything.',
|
|
244
|
+
' A successful preview does not guarantee that applying it will succeed.',
|
|
245
|
+
' With --apply, assignments are best-effort; any failed item makes the command',
|
|
246
|
+
' exit with code 1 while the complete result remains available on stdout.',
|
|
247
|
+
'',
|
|
248
|
+
'Input:',
|
|
249
|
+
' The top-level object must contain an assignments array.',
|
|
250
|
+
' Each assignment requires transactionRef and a categoryId or non-empty categorySplits.',
|
|
251
|
+
' Copy the exact transactionRef from transactions output and categoryId from',
|
|
252
|
+
' categories output. The example values below are placeholders.',
|
|
253
|
+
' Set categoryId to null to clear an assignment.',
|
|
254
|
+
' lineItemId is optional and accepts a non-empty string or null.',
|
|
255
|
+
' categorySplits is optional and accepts a non-empty array or null.',
|
|
256
|
+
' Each split requires categoryId and a positive integer amountPence;',
|
|
257
|
+
' a split lineItemId is optional.',
|
|
258
|
+
' incomeSubtype is optional and accepts "pay", "interest", or null.',
|
|
259
|
+
' assignmentScope is optional and accepts "personal" or "joint".',
|
|
260
|
+
'',
|
|
261
|
+
'Workflow:',
|
|
262
|
+
' sloth-agent categories',
|
|
263
|
+
' sloth-agent transactions --uncategorized --limit 50',
|
|
264
|
+
' sloth-agent assign --input assignments.json Preview only',
|
|
265
|
+
' sloth-agent assign --input assignments.json --apply Write assignments',
|
|
266
|
+
' sloth-agent transactions --limit 50 Read back the result',
|
|
267
|
+
'',
|
|
268
|
+
'Example:',
|
|
269
|
+
' {',
|
|
270
|
+
' "assignments": [',
|
|
271
|
+
' {',
|
|
272
|
+
' "transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",',
|
|
273
|
+
' "categoryId": "PASTE_A_CATEGORY_ID_HERE"',
|
|
274
|
+
' }',
|
|
275
|
+
' ]',
|
|
276
|
+
' }',
|
|
277
|
+
' These are placeholders. Replace both values with exact IDs from CLI output.',
|
|
278
|
+
'',
|
|
279
|
+
'Output:',
|
|
280
|
+
' Preview mode returns dryRun, endpoint, and the validated payload.',
|
|
281
|
+
' Apply mode returns succeeded and failed assignment arrays.',
|
|
282
|
+
' Successful assignments update the original transaction. See the result in',
|
|
283
|
+
' Sloth Money → Transactions or read the transaction again through the CLI.',
|
|
284
|
+
' Assignments do not create a separate list.',
|
|
285
|
+
].join('\n');
|
|
286
|
+
}
|
|
287
|
+
export function jointBudgetSettingsHelpText() {
|
|
288
|
+
return [
|
|
289
|
+
'Sloth Agent CLI — joint-budget-settings',
|
|
290
|
+
'',
|
|
291
|
+
'Read or update whether shared personal transactions count in the linked joint budget.',
|
|
292
|
+
'',
|
|
293
|
+
'Usage:',
|
|
294
|
+
' sloth-agent joint-budget-settings [options]',
|
|
295
|
+
'',
|
|
296
|
+
'Options:',
|
|
297
|
+
' --include-shared-personal-transactions=true|false',
|
|
298
|
+
' Optional. Preview the linked setting change.',
|
|
299
|
+
' --apply Optional. Apply the previewed setting change.',
|
|
300
|
+
' --base-url URL Optional. Override the API origin.',
|
|
301
|
+
' -h, --help Show this help.',
|
|
302
|
+
...API_ORIGIN_HELP_LINES,
|
|
303
|
+
'',
|
|
304
|
+
'Safety:',
|
|
305
|
+
' With no setting option, the command is read-only.',
|
|
306
|
+
' Without --apply, a setting option returns a dry-run preview and does not write.',
|
|
307
|
+
' --apply requires an explicit true or false setting value.',
|
|
308
|
+
'',
|
|
309
|
+
'Examples:',
|
|
310
|
+
' sloth-agent joint-budget-settings',
|
|
311
|
+
' sloth-agent joint-budget-settings --include-shared-personal-transactions=true',
|
|
312
|
+
' sloth-agent joint-budget-settings --include-shared-personal-transactions=true --apply',
|
|
313
|
+
'',
|
|
314
|
+
'Output:',
|
|
315
|
+
' JSON containing the linked setting, audit metadata, or a dry-run payload.',
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
318
|
+
export function goalsHelpText() {
|
|
319
|
+
return [
|
|
320
|
+
'Sloth Agent CLI — goals',
|
|
321
|
+
'',
|
|
322
|
+
'List, create, update, or delete your savings goals.',
|
|
323
|
+
'',
|
|
324
|
+
'Commands:',
|
|
325
|
+
' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
|
|
326
|
+
' sloth-agent goals create Preview or create a goal.',
|
|
327
|
+
' sloth-agent goals update Preview or update selected goal fields.',
|
|
328
|
+
' sloth-agent goals delete Preview or permanently delete a goal.',
|
|
329
|
+
'',
|
|
330
|
+
'Help:',
|
|
331
|
+
' Run sloth-agent goals <command> --help for command-specific details.',
|
|
332
|
+
...API_ORIGIN_HELP_LINES,
|
|
333
|
+
].join('\n');
|
|
334
|
+
}
|
|
335
|
+
export function goalsListHelpText() {
|
|
336
|
+
return [
|
|
337
|
+
'Sloth Agent CLI — goals list',
|
|
338
|
+
'',
|
|
339
|
+
'List your goals in their display order.',
|
|
340
|
+
'',
|
|
341
|
+
'Usage:',
|
|
342
|
+
' sloth-agent goals [list] [--base-url URL]',
|
|
343
|
+
'',
|
|
344
|
+
'Options:',
|
|
345
|
+
' --base-url URL Optional. Override the API origin.',
|
|
346
|
+
' -h, --help Show this help.',
|
|
347
|
+
...API_ORIGIN_HELP_LINES,
|
|
348
|
+
'',
|
|
349
|
+
'Constraints:',
|
|
350
|
+
' No filters or singular get are supported.',
|
|
351
|
+
' This command is read-only.',
|
|
352
|
+
'',
|
|
353
|
+
'Output:',
|
|
354
|
+
' JSON containing currency and goals. Each goal contains id, name,',
|
|
355
|
+
' targetAmount, targetMonthKey, isAchieved, and sharedWithPartner.',
|
|
356
|
+
].join('\n');
|
|
357
|
+
}
|
|
358
|
+
export function goalsCreateHelpText() {
|
|
359
|
+
return [
|
|
360
|
+
'Sloth Agent CLI — goals create',
|
|
361
|
+
'',
|
|
362
|
+
'Preview or create a goal.',
|
|
363
|
+
'',
|
|
364
|
+
'Usage:',
|
|
365
|
+
' sloth-agent goals create --name NAME [options]',
|
|
366
|
+
'',
|
|
367
|
+
'Options:',
|
|
368
|
+
' --name NAME Required. Goal name, 1 to 200 characters.',
|
|
369
|
+
' --target-amount AMOUNT Optional. Positive major-unit amount with up to 2 decimals.',
|
|
370
|
+
' --target-month YYYY-MM Optional. Target calendar month.',
|
|
371
|
+
' --apply Optional. Create the goal in Sloth Money.',
|
|
372
|
+
' --base-url URL Optional. Override the API origin.',
|
|
373
|
+
' -h, --help Show this help.',
|
|
374
|
+
...API_ORIGIN_HELP_LINES,
|
|
375
|
+
'',
|
|
376
|
+
'Safety:',
|
|
377
|
+
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
378
|
+
' New goals are private to the owner and appended to the existing goal order.',
|
|
379
|
+
'',
|
|
380
|
+
'Example:',
|
|
381
|
+
' sloth-agent goals create --name "Emergency fund" --target-amount 12000',
|
|
382
|
+
' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --apply',
|
|
383
|
+
'',
|
|
384
|
+
'Output:',
|
|
385
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
386
|
+
' Apply mode returns the persisted goal and currency from the 201 response.',
|
|
387
|
+
].join('\n');
|
|
388
|
+
}
|
|
389
|
+
export function goalsUpdateHelpText() {
|
|
390
|
+
return [
|
|
391
|
+
'Sloth Agent CLI — goals update',
|
|
392
|
+
'',
|
|
393
|
+
'Preview or partially update a goal.',
|
|
394
|
+
'',
|
|
395
|
+
'Usage:',
|
|
396
|
+
' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
|
|
397
|
+
'',
|
|
398
|
+
'Options:',
|
|
399
|
+
' --goal-id ID Required. Goal ID from goals list or create output.',
|
|
400
|
+
' --name NAME Optional. Replacement name, 1 to 200 characters.',
|
|
401
|
+
' --target-amount AMOUNT Optional. Positive amount with up to 2 decimals.',
|
|
402
|
+
' --clear-target-amount Optional. Remove the target amount.',
|
|
403
|
+
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
404
|
+
' --clear-target-month Optional. Remove the target month.',
|
|
405
|
+
' --achieved=true|false Optional. Mark the goal achieved or active.',
|
|
406
|
+
' --apply Optional. Write the partial update.',
|
|
407
|
+
' --base-url URL Optional. Override the API origin.',
|
|
408
|
+
' -h, --help Show this help.',
|
|
409
|
+
...API_ORIGIN_HELP_LINES,
|
|
410
|
+
'',
|
|
411
|
+
'Constraints:',
|
|
412
|
+
' Provide at least one field to update.',
|
|
413
|
+
' Set and clear options for the same field are mutually exclusive.',
|
|
414
|
+
' Marking a goal achieved removes its forecast assignment.',
|
|
415
|
+
' Marking it active again does not restore the previous assignment.',
|
|
416
|
+
' Change active shared pot target amounts in the Sloth Budget app, where',
|
|
417
|
+
' account balances can be reconciled across goals in priority order.',
|
|
418
|
+
' Sharing remains app-managed. Updates to an already shared goal remain visible',
|
|
419
|
+
' to the connected partner.',
|
|
420
|
+
'',
|
|
421
|
+
'Safety:',
|
|
422
|
+
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
423
|
+
'',
|
|
424
|
+
'Output:',
|
|
425
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
426
|
+
' Apply mode returns the complete persisted goal and currency.',
|
|
427
|
+
].join('\n');
|
|
428
|
+
}
|
|
429
|
+
export function goalsDeleteHelpText() {
|
|
430
|
+
return [
|
|
431
|
+
'Sloth Agent CLI — goals delete',
|
|
432
|
+
'',
|
|
433
|
+
'Preview or permanently delete a goal.',
|
|
434
|
+
'',
|
|
435
|
+
'Usage:',
|
|
436
|
+
' sloth-agent goals delete --goal-id ID [--apply] [--base-url URL]',
|
|
437
|
+
'',
|
|
438
|
+
'Options:',
|
|
439
|
+
' --goal-id ID Required. Goal ID from goals list or create output.',
|
|
440
|
+
' --apply Optional. Permanently delete the goal.',
|
|
441
|
+
' --base-url URL Optional. Override the API origin.',
|
|
442
|
+
' -h, --help Show this help.',
|
|
443
|
+
...API_ORIGIN_HELP_LINES,
|
|
444
|
+
'',
|
|
445
|
+
'Safety:',
|
|
446
|
+
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
447
|
+
' Applying deletion also removes the goal from forecast assignments and',
|
|
448
|
+
' removes its goal drift history. This operation cannot be undone.',
|
|
449
|
+
'',
|
|
450
|
+
'Output:',
|
|
451
|
+
' Preview mode returns dryRun, method, and endpoint.',
|
|
452
|
+
' Apply mode returns deleted and deletedGoalId.',
|
|
453
|
+
].join('\n');
|
|
454
|
+
}
|
|
455
|
+
export function askPartnerHelpText() {
|
|
456
|
+
return [
|
|
457
|
+
'Sloth Agent CLI — ask-partner',
|
|
458
|
+
'',
|
|
459
|
+
'Create a shareable link asking a partner to clarify a transaction.',
|
|
460
|
+
'',
|
|
461
|
+
'Usage:',
|
|
462
|
+
' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
|
|
463
|
+
'',
|
|
464
|
+
'Options:',
|
|
465
|
+
' --transaction-ref REF Required. Stable transactionRef from transaction output.',
|
|
466
|
+
' --base-url URL Optional. Override the API origin.',
|
|
467
|
+
' -h, --help Show this help.',
|
|
468
|
+
...API_ORIGIN_HELP_LINES,
|
|
469
|
+
'',
|
|
470
|
+
'Write behavior:',
|
|
471
|
+
' Running this command creates the request immediately. There is no preview mode.',
|
|
472
|
+
'',
|
|
473
|
+
'Example:',
|
|
474
|
+
' sloth-agent ask-partner --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE',
|
|
475
|
+
' The value shown is a placeholder. Copy the exact transactionRef from',
|
|
476
|
+
' sloth-agent transactions output.',
|
|
477
|
+
'',
|
|
478
|
+
'Output:',
|
|
479
|
+
' JSON containing requestId, publicUrl, message, expiresAt, and status.',
|
|
480
|
+
].join('\n');
|
|
481
|
+
}
|
|
482
|
+
export function commandHelpText(topic) {
|
|
483
|
+
const helpByTopic = {
|
|
484
|
+
auth: authHelpText,
|
|
485
|
+
'auth-login': authLoginHelpText,
|
|
486
|
+
'auth-status': authStatusHelpText,
|
|
487
|
+
'auth-logout': authLogoutHelpText,
|
|
488
|
+
categories: categoriesHelpText,
|
|
489
|
+
transactions: transactionsHelpText,
|
|
490
|
+
assign: assignHelpText,
|
|
491
|
+
'joint-budget-settings': jointBudgetSettingsHelpText,
|
|
492
|
+
goals: goalsHelpText,
|
|
493
|
+
'goals-list': goalsListHelpText,
|
|
494
|
+
'goals-create': goalsCreateHelpText,
|
|
495
|
+
'goals-update': goalsUpdateHelpText,
|
|
496
|
+
'goals-delete': goalsDeleteHelpText,
|
|
497
|
+
'ask-partner': askPartnerHelpText,
|
|
498
|
+
};
|
|
499
|
+
return helpByTopic[topic]();
|
|
500
|
+
}
|
|
30
501
|
function writeJson(write, data) {
|
|
31
502
|
write(`${JSON.stringify(data, null, 2)}\n`);
|
|
32
503
|
}
|
|
@@ -113,6 +584,9 @@ function buildTransactionsQuery(filters) {
|
|
|
113
584
|
params.set('accountId', filters.accountId);
|
|
114
585
|
if (filters.categoryId !== undefined)
|
|
115
586
|
params.set('categoryId', filters.categoryId);
|
|
587
|
+
if (filters.assignmentScope !== undefined) {
|
|
588
|
+
params.set('assignmentScope', filters.assignmentScope);
|
|
589
|
+
}
|
|
116
590
|
if (filters.cursor !== undefined)
|
|
117
591
|
params.set('cursor', filters.cursor);
|
|
118
592
|
return params.toString();
|
|
@@ -188,7 +662,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
188
662
|
try {
|
|
189
663
|
const parsed = parseArgs(argv);
|
|
190
664
|
if (parsed.command === 'help') {
|
|
191
|
-
writeStdout(`${usageText()}\n`);
|
|
665
|
+
writeStdout(`${parsed.topic ? commandHelpText(parsed.topic) : usageText()}\n`);
|
|
192
666
|
return 0;
|
|
193
667
|
}
|
|
194
668
|
if (parsed.command === 'version') {
|
|
@@ -261,6 +735,94 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
261
735
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
262
736
|
token = credential.token;
|
|
263
737
|
const headers = requestHeaders(token);
|
|
738
|
+
if (parsed.command === 'goals-create') {
|
|
739
|
+
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
740
|
+
const payload = {
|
|
741
|
+
name: parsed.name,
|
|
742
|
+
...(parsed.targetAmount === undefined
|
|
743
|
+
? {}
|
|
744
|
+
: { targetAmount: parsed.targetAmount }),
|
|
745
|
+
...(parsed.targetMonthKey === undefined
|
|
746
|
+
? {}
|
|
747
|
+
: { targetMonthKey: parsed.targetMonthKey }),
|
|
748
|
+
};
|
|
749
|
+
if (!parsed.apply) {
|
|
750
|
+
writeJson(writeStdout, {
|
|
751
|
+
dryRun: true,
|
|
752
|
+
endpoint,
|
|
753
|
+
method: 'POST',
|
|
754
|
+
payload,
|
|
755
|
+
});
|
|
756
|
+
return 0;
|
|
757
|
+
}
|
|
758
|
+
const response = await fetchImplementation(endpoint, {
|
|
759
|
+
method: 'POST',
|
|
760
|
+
headers: {
|
|
761
|
+
...headers,
|
|
762
|
+
'Content-Type': 'application/json',
|
|
763
|
+
},
|
|
764
|
+
body: JSON.stringify(payload),
|
|
765
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
766
|
+
});
|
|
767
|
+
const data = parseApiResponse('goals-create', await parseHttpResponse(response, token));
|
|
768
|
+
writeJson(writeStdout, data);
|
|
769
|
+
return 0;
|
|
770
|
+
}
|
|
771
|
+
if (parsed.command === 'goals-update') {
|
|
772
|
+
const endpoint = `${baseUrl}/api/agent/v1/goals/${encodeURIComponent(parsed.goalId)}`;
|
|
773
|
+
const payload = {
|
|
774
|
+
...(parsed.name === undefined ? {} : { name: parsed.name }),
|
|
775
|
+
...(parsed.targetAmount === undefined
|
|
776
|
+
? {}
|
|
777
|
+
: { targetAmount: parsed.targetAmount }),
|
|
778
|
+
...(parsed.targetMonthKey === undefined
|
|
779
|
+
? {}
|
|
780
|
+
: { targetMonthKey: parsed.targetMonthKey }),
|
|
781
|
+
...(parsed.isAchieved === undefined
|
|
782
|
+
? {}
|
|
783
|
+
: { isAchieved: parsed.isAchieved }),
|
|
784
|
+
};
|
|
785
|
+
if (!parsed.apply) {
|
|
786
|
+
writeJson(writeStdout, {
|
|
787
|
+
dryRun: true,
|
|
788
|
+
endpoint,
|
|
789
|
+
method: 'PATCH',
|
|
790
|
+
payload,
|
|
791
|
+
});
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
794
|
+
const response = await fetchImplementation(endpoint, {
|
|
795
|
+
method: 'PATCH',
|
|
796
|
+
headers: {
|
|
797
|
+
...headers,
|
|
798
|
+
'Content-Type': 'application/json',
|
|
799
|
+
},
|
|
800
|
+
body: JSON.stringify(payload),
|
|
801
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
802
|
+
});
|
|
803
|
+
const data = parseApiResponse('goals-update', await parseHttpResponse(response, token));
|
|
804
|
+
writeJson(writeStdout, data);
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
if (parsed.command === 'goals-delete') {
|
|
808
|
+
const endpoint = `${baseUrl}/api/agent/v1/goals/${encodeURIComponent(parsed.goalId)}`;
|
|
809
|
+
if (!parsed.apply) {
|
|
810
|
+
writeJson(writeStdout, {
|
|
811
|
+
dryRun: true,
|
|
812
|
+
endpoint,
|
|
813
|
+
method: 'DELETE',
|
|
814
|
+
});
|
|
815
|
+
return 0;
|
|
816
|
+
}
|
|
817
|
+
const response = await fetchImplementation(endpoint, {
|
|
818
|
+
method: 'DELETE',
|
|
819
|
+
headers,
|
|
820
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
821
|
+
});
|
|
822
|
+
const data = parseApiResponse('goals-delete', await parseHttpResponse(response, token));
|
|
823
|
+
writeJson(writeStdout, data);
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
264
826
|
if (parsed.command === 'assign') {
|
|
265
827
|
const payload = validateAssignmentPayload(readAssignmentFile(parsed.input));
|
|
266
828
|
const endpoint = `${baseUrl}/api/agent/v1/transaction-assignments`;
|
|
@@ -281,6 +843,36 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
281
843
|
writeJson(writeStdout, data);
|
|
282
844
|
return hasFailures(data) ? 1 : 0;
|
|
283
845
|
}
|
|
846
|
+
if (parsed.command === 'joint-budget-settings') {
|
|
847
|
+
const endpoint = `${baseUrl}/api/agent/v1/joint-budget-settings`;
|
|
848
|
+
if (parsed.includeSharedPersonalTransactions !== undefined && !parsed.apply) {
|
|
849
|
+
writeJson(writeStdout, {
|
|
850
|
+
dryRun: true,
|
|
851
|
+
endpoint,
|
|
852
|
+
payload: {
|
|
853
|
+
includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
return 0;
|
|
857
|
+
}
|
|
858
|
+
const response = await fetchImplementation(endpoint, {
|
|
859
|
+
method: parsed.apply ? 'PUT' : 'GET',
|
|
860
|
+
headers: parsed.apply
|
|
861
|
+
? { ...headers, 'Content-Type': 'application/json' }
|
|
862
|
+
: headers,
|
|
863
|
+
...(parsed.apply
|
|
864
|
+
? {
|
|
865
|
+
body: JSON.stringify({
|
|
866
|
+
includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
|
|
867
|
+
}),
|
|
868
|
+
}
|
|
869
|
+
: {}),
|
|
870
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
871
|
+
});
|
|
872
|
+
const data = validateJointBudgetSettingsResponse(await parseHttpResponse(response, token));
|
|
873
|
+
writeJson(writeStdout, data);
|
|
874
|
+
return 0;
|
|
875
|
+
}
|
|
284
876
|
if (parsed.command === 'ask-partner') {
|
|
285
877
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
|
|
286
878
|
method: 'POST',
|
|
@@ -295,6 +887,16 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
295
887
|
writeJson(writeStdout, data);
|
|
296
888
|
return 0;
|
|
297
889
|
}
|
|
890
|
+
if (parsed.command === 'goals-list') {
|
|
891
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/goals`, {
|
|
892
|
+
method: 'GET',
|
|
893
|
+
headers,
|
|
894
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
895
|
+
});
|
|
896
|
+
const data = parseApiResponse('goals-list', await parseHttpResponse(response, token));
|
|
897
|
+
writeJson(writeStdout, data);
|
|
898
|
+
return 0;
|
|
899
|
+
}
|
|
298
900
|
const path = parsed.command === 'categories'
|
|
299
901
|
? '/api/agent/v1/categories'
|
|
300
902
|
: `/api/agent/v1/transactions${(() => {
|