@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/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
- return { command: 'help' };
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) {