@slothmoney/agent-cli 0.1.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 +31 -0
- package/README.md +192 -30
- package/dist/args.js +345 -2
- package/dist/cli.js +766 -19
- package/dist/contracts.js +96 -2
- package/dist/credential-store.js +43 -0
- package/dist/errors.js +3 -1
- package/package.json +5 -1
package/dist/args.js
CHANGED
|
@@ -47,6 +47,47 @@ function requireNonEmpty(value, name) {
|
|
|
47
47
|
throw new UsageError(`${name} requires a value`);
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
|
+
function parseGoalAmount(value, name) {
|
|
51
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
52
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
53
|
+
}
|
|
54
|
+
const amount = Number(value);
|
|
55
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
56
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
57
|
+
}
|
|
58
|
+
return amount;
|
|
59
|
+
}
|
|
60
|
+
function parseGoalMonthKey(value, name) {
|
|
61
|
+
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
|
|
62
|
+
throw new UsageError(`${name} must be a valid YYYY-MM month`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseExplicitBoolean(value, name) {
|
|
67
|
+
if (value !== 'true' && value !== 'false') {
|
|
68
|
+
throw new UsageError(`${name} must be true or false`);
|
|
69
|
+
}
|
|
70
|
+
return value === 'true';
|
|
71
|
+
}
|
|
72
|
+
function parseGoalName(value) {
|
|
73
|
+
const name = value.trim();
|
|
74
|
+
if (name.length > 200) {
|
|
75
|
+
throw new UsageError('--name must be at most 200 characters');
|
|
76
|
+
}
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
function parseGoalId(value) {
|
|
80
|
+
const goalId = value.trim();
|
|
81
|
+
if (goalId.length > 500
|
|
82
|
+
|| goalId.includes('/')
|
|
83
|
+
|| Array.from(goalId).some((character) => {
|
|
84
|
+
const codePoint = character.codePointAt(0);
|
|
85
|
+
return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
|
|
86
|
+
})) {
|
|
87
|
+
throw new UsageError('--goal-id must be a valid goal document ID');
|
|
88
|
+
}
|
|
89
|
+
return goalId;
|
|
90
|
+
}
|
|
50
91
|
function parseTransactions(args) {
|
|
51
92
|
const filters = {};
|
|
52
93
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -73,6 +114,7 @@ function parseTransactions(args) {
|
|
|
73
114
|
'--q',
|
|
74
115
|
'--account-id',
|
|
75
116
|
'--category-id',
|
|
117
|
+
'--assignment-scope',
|
|
76
118
|
'--cursor',
|
|
77
119
|
]);
|
|
78
120
|
if (!name || !supported.has(name)) {
|
|
@@ -108,6 +150,12 @@ function parseTransactions(args) {
|
|
|
108
150
|
else if (name === '--category-id') {
|
|
109
151
|
filters.categoryId = setOnce(filters.categoryId, value, name);
|
|
110
152
|
}
|
|
153
|
+
else if (name === '--assignment-scope') {
|
|
154
|
+
if (value !== 'personal' && value !== 'joint') {
|
|
155
|
+
throw new UsageError('--assignment-scope must be personal or joint');
|
|
156
|
+
}
|
|
157
|
+
filters.assignmentScope = setOnce(filters.assignmentScope, value, name);
|
|
158
|
+
}
|
|
111
159
|
else if (name === '--cursor') {
|
|
112
160
|
filters.cursor = setOnce(filters.cursor, value, name);
|
|
113
161
|
}
|
|
@@ -122,15 +170,273 @@ function parseTransactions(args) {
|
|
|
122
170
|
function withBaseUrl(value, baseUrl) {
|
|
123
171
|
return baseUrl === undefined ? value : { ...value, baseUrl };
|
|
124
172
|
}
|
|
173
|
+
function parseAuth(args, baseUrl) {
|
|
174
|
+
const authCommand = args.shift();
|
|
175
|
+
if (!authCommand) {
|
|
176
|
+
throw new UsageError('auth requires login, status, or logout');
|
|
177
|
+
}
|
|
178
|
+
if (authCommand === 'login') {
|
|
179
|
+
let input;
|
|
180
|
+
for (const argument of args) {
|
|
181
|
+
if (argument === '--token-stdin') {
|
|
182
|
+
if (input === 'stdin') {
|
|
183
|
+
throw new UsageError('--token-stdin may only be provided once');
|
|
184
|
+
}
|
|
185
|
+
if (input === 'environment') {
|
|
186
|
+
throw new UsageError('--token-stdin and --from-env are mutually exclusive');
|
|
187
|
+
}
|
|
188
|
+
input = 'stdin';
|
|
189
|
+
}
|
|
190
|
+
else if (argument === '--from-env') {
|
|
191
|
+
if (input === 'environment') {
|
|
192
|
+
throw new UsageError('--from-env may only be provided once');
|
|
193
|
+
}
|
|
194
|
+
if (input === 'stdin') {
|
|
195
|
+
throw new UsageError('--token-stdin and --from-env are mutually exclusive');
|
|
196
|
+
}
|
|
197
|
+
input = 'environment';
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
throw new UsageError(`Unknown auth login option: ${argument}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return withBaseUrl({ command: 'auth-login', input: input ?? 'prompt' }, baseUrl);
|
|
204
|
+
}
|
|
205
|
+
if (authCommand === 'status' || authCommand === 'logout') {
|
|
206
|
+
if (args.length > 0) {
|
|
207
|
+
throw new UsageError(`Unknown auth ${authCommand} option: ${args[0]}`);
|
|
208
|
+
}
|
|
209
|
+
return withBaseUrl({ command: `auth-${authCommand}` }, baseUrl);
|
|
210
|
+
}
|
|
211
|
+
throw new UsageError(`Unknown auth command: ${authCommand}`);
|
|
212
|
+
}
|
|
213
|
+
function parseGoals(args, baseUrl) {
|
|
214
|
+
const subcommand = args.shift();
|
|
215
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
216
|
+
if (args.length > 0) {
|
|
217
|
+
throw new UsageError(`Unknown goals list option: ${args[0]}`);
|
|
218
|
+
}
|
|
219
|
+
return withBaseUrl({ command: 'goals-list' }, baseUrl);
|
|
220
|
+
}
|
|
221
|
+
if (subcommand === 'create') {
|
|
222
|
+
let name;
|
|
223
|
+
let targetAmount;
|
|
224
|
+
let targetMonthKey;
|
|
225
|
+
let apply = false;
|
|
226
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
227
|
+
const argument = args[index];
|
|
228
|
+
if (argument === '--apply') {
|
|
229
|
+
if (apply)
|
|
230
|
+
throw new UsageError('--apply may only be provided once');
|
|
231
|
+
apply = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const [option, inlineValue] = argument.includes('=')
|
|
235
|
+
? argument.split(/=(.*)/s, 2)
|
|
236
|
+
: [argument, undefined];
|
|
237
|
+
if (option !== '--name'
|
|
238
|
+
&& option !== '--target-amount'
|
|
239
|
+
&& option !== '--target-month') {
|
|
240
|
+
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
241
|
+
}
|
|
242
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
243
|
+
if (inlineValue === undefined)
|
|
244
|
+
index += 1;
|
|
245
|
+
if (option === '--name') {
|
|
246
|
+
name = setOnce(name, parseGoalName(value), option);
|
|
247
|
+
}
|
|
248
|
+
else if (option === '--target-amount') {
|
|
249
|
+
targetAmount = setOnce(targetAmount, parseGoalAmount(value, option), option);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!name)
|
|
256
|
+
throw new UsageError('goals create requires --name <name>');
|
|
257
|
+
return withBaseUrl({
|
|
258
|
+
command: 'goals-create',
|
|
259
|
+
name,
|
|
260
|
+
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
261
|
+
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
262
|
+
apply,
|
|
263
|
+
}, baseUrl);
|
|
264
|
+
}
|
|
265
|
+
if (subcommand === 'update') {
|
|
266
|
+
let goalId;
|
|
267
|
+
let name;
|
|
268
|
+
let targetAmount;
|
|
269
|
+
let targetMonthKey;
|
|
270
|
+
let isAchieved;
|
|
271
|
+
let apply = false;
|
|
272
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
273
|
+
const argument = args[index];
|
|
274
|
+
if (argument === '--apply') {
|
|
275
|
+
if (apply)
|
|
276
|
+
throw new UsageError('--apply may only be provided once');
|
|
277
|
+
apply = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (argument === '--clear-target-amount') {
|
|
281
|
+
if (targetAmount !== undefined) {
|
|
282
|
+
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
283
|
+
}
|
|
284
|
+
targetAmount = null;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (argument === '--clear-target-month') {
|
|
288
|
+
if (targetMonthKey !== undefined) {
|
|
289
|
+
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
290
|
+
}
|
|
291
|
+
targetMonthKey = null;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [option, inlineValue] = argument.includes('=')
|
|
295
|
+
? argument.split(/=(.*)/s, 2)
|
|
296
|
+
: [argument, undefined];
|
|
297
|
+
if (option !== '--goal-id'
|
|
298
|
+
&& option !== '--name'
|
|
299
|
+
&& option !== '--target-amount'
|
|
300
|
+
&& option !== '--target-month'
|
|
301
|
+
&& option !== '--achieved') {
|
|
302
|
+
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
303
|
+
}
|
|
304
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
305
|
+
if (inlineValue === undefined)
|
|
306
|
+
index += 1;
|
|
307
|
+
if (option === '--goal-id') {
|
|
308
|
+
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
309
|
+
}
|
|
310
|
+
else if (option === '--name') {
|
|
311
|
+
name = setOnce(name, parseGoalName(value), option);
|
|
312
|
+
}
|
|
313
|
+
else if (option === '--target-amount') {
|
|
314
|
+
if (targetAmount !== undefined) {
|
|
315
|
+
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
316
|
+
}
|
|
317
|
+
targetAmount = parseGoalAmount(value, option);
|
|
318
|
+
}
|
|
319
|
+
else if (option === '--target-month') {
|
|
320
|
+
if (targetMonthKey !== undefined) {
|
|
321
|
+
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
322
|
+
}
|
|
323
|
+
targetMonthKey = parseGoalMonthKey(value, option);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!goalId)
|
|
330
|
+
throw new UsageError('goals update requires --goal-id <id>');
|
|
331
|
+
if (name === undefined
|
|
332
|
+
&& targetAmount === undefined
|
|
333
|
+
&& targetMonthKey === undefined
|
|
334
|
+
&& isAchieved === undefined) {
|
|
335
|
+
throw new UsageError('goals update requires at least one field to update');
|
|
336
|
+
}
|
|
337
|
+
return withBaseUrl({
|
|
338
|
+
command: 'goals-update',
|
|
339
|
+
goalId,
|
|
340
|
+
...(name === undefined ? {} : { name }),
|
|
341
|
+
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
342
|
+
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
343
|
+
...(isAchieved === undefined ? {} : { isAchieved }),
|
|
344
|
+
apply,
|
|
345
|
+
}, baseUrl);
|
|
346
|
+
}
|
|
347
|
+
if (subcommand === 'delete') {
|
|
348
|
+
let goalId;
|
|
349
|
+
let apply = false;
|
|
350
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
351
|
+
const argument = args[index];
|
|
352
|
+
if (argument === '--apply') {
|
|
353
|
+
if (apply)
|
|
354
|
+
throw new UsageError('--apply may only be provided once');
|
|
355
|
+
apply = true;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const [option, inlineValue] = argument.includes('=')
|
|
359
|
+
? argument.split(/=(.*)/s, 2)
|
|
360
|
+
: [argument, undefined];
|
|
361
|
+
if (option !== '--goal-id') {
|
|
362
|
+
throw new UsageError(`Unknown goals delete option: ${argument}`);
|
|
363
|
+
}
|
|
364
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
365
|
+
if (inlineValue === undefined)
|
|
366
|
+
index += 1;
|
|
367
|
+
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
368
|
+
}
|
|
369
|
+
if (!goalId)
|
|
370
|
+
throw new UsageError('goals delete requires --goal-id <id>');
|
|
371
|
+
return withBaseUrl({
|
|
372
|
+
command: 'goals-delete',
|
|
373
|
+
goalId,
|
|
374
|
+
apply,
|
|
375
|
+
}, baseUrl);
|
|
376
|
+
}
|
|
377
|
+
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
378
|
+
}
|
|
379
|
+
function helpTopic(argv) {
|
|
380
|
+
const positionals = [];
|
|
381
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
382
|
+
const argument = argv[index];
|
|
383
|
+
if (argument === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
if (argument === '--base-url') {
|
|
386
|
+
index += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (argument.startsWith('--base-url=') || argument.startsWith('-')) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
positionals.push(argument);
|
|
393
|
+
}
|
|
394
|
+
const [command, subcommand] = positionals;
|
|
395
|
+
if (command === 'auth') {
|
|
396
|
+
if (subcommand === 'login'
|
|
397
|
+
|| subcommand === 'status'
|
|
398
|
+
|| subcommand === 'logout') {
|
|
399
|
+
return `auth-${subcommand}`;
|
|
400
|
+
}
|
|
401
|
+
return 'auth';
|
|
402
|
+
}
|
|
403
|
+
if (command === 'goals') {
|
|
404
|
+
if (subcommand === 'list')
|
|
405
|
+
return 'goals-list';
|
|
406
|
+
if (subcommand === 'create')
|
|
407
|
+
return 'goals-create';
|
|
408
|
+
if (subcommand === 'update')
|
|
409
|
+
return 'goals-update';
|
|
410
|
+
if (subcommand === 'delete')
|
|
411
|
+
return 'goals-delete';
|
|
412
|
+
return 'goals';
|
|
413
|
+
}
|
|
414
|
+
if (command === 'categories'
|
|
415
|
+
|| command === 'transactions'
|
|
416
|
+
|| command === 'assign'
|
|
417
|
+
|| command === 'joint-budget-settings'
|
|
418
|
+
|| command === 'ask-partner') {
|
|
419
|
+
return command;
|
|
420
|
+
}
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
125
423
|
export function parseArgs(argv) {
|
|
126
|
-
if (argv.includes('--help') || argv.includes('-h'))
|
|
127
|
-
|
|
424
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
425
|
+
const topic = helpTopic(argv);
|
|
426
|
+
return topic ? { command: 'help', topic } : { command: 'help' };
|
|
427
|
+
}
|
|
128
428
|
if (argv.includes('--version') || argv.includes('-V'))
|
|
129
429
|
return { command: 'version' };
|
|
130
430
|
const { args, baseUrl } = parseGlobalOptions(argv);
|
|
131
431
|
const command = args.shift();
|
|
132
432
|
if (!command)
|
|
133
433
|
return { command: 'help' };
|
|
434
|
+
if (command === 'auth') {
|
|
435
|
+
return parseAuth(args, baseUrl);
|
|
436
|
+
}
|
|
437
|
+
if (command === 'goals') {
|
|
438
|
+
return parseGoals(args, baseUrl);
|
|
439
|
+
}
|
|
134
440
|
if (command === 'categories') {
|
|
135
441
|
if (args.length > 0) {
|
|
136
442
|
throw new UsageError(`Unknown categories option: ${args[0]}`);
|
|
@@ -165,6 +471,43 @@ export function parseArgs(argv) {
|
|
|
165
471
|
throw new UsageError('assign requires --input <file>');
|
|
166
472
|
return withBaseUrl({ command, input, apply }, baseUrl);
|
|
167
473
|
}
|
|
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
|
+
}
|
|
168
511
|
if (command === 'ask-partner') {
|
|
169
512
|
let transactionRef;
|
|
170
513
|
for (let index = 0; index < args.length; index += 1) {
|