@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/contracts.js
CHANGED
|
@@ -38,12 +38,18 @@ function validateAssignment(value, index) {
|
|
|
38
38
|
const assignment = requireObject(value, label);
|
|
39
39
|
rejectUnknownFields(assignment, new Set([
|
|
40
40
|
'transactionRef',
|
|
41
|
+
'assignmentScope',
|
|
41
42
|
'categoryId',
|
|
42
43
|
'lineItemId',
|
|
43
44
|
'categorySplits',
|
|
44
45
|
'incomeSubtype',
|
|
45
46
|
]), label);
|
|
46
47
|
requireString(assignment.transactionRef, `${label}.transactionRef`);
|
|
48
|
+
if (assignment.assignmentScope !== undefined
|
|
49
|
+
&& assignment.assignmentScope !== 'personal'
|
|
50
|
+
&& assignment.assignmentScope !== 'joint') {
|
|
51
|
+
throw new UsageError(`${label}.assignmentScope must be personal or joint`);
|
|
52
|
+
}
|
|
47
53
|
if (assignment.categoryId !== undefined && assignment.categoryId !== null) {
|
|
48
54
|
requireString(assignment.categoryId, `${label}.categoryId`);
|
|
49
55
|
}
|
|
@@ -74,6 +80,9 @@ function validateAssignment(value, index) {
|
|
|
74
80
|
}
|
|
75
81
|
return {
|
|
76
82
|
transactionRef: assignment.transactionRef,
|
|
83
|
+
...(assignment.assignmentScope !== undefined
|
|
84
|
+
? { assignmentScope: assignment.assignmentScope }
|
|
85
|
+
: {}),
|
|
77
86
|
...(assignment.categoryId !== undefined ? { categoryId: assignment.categoryId } : {}),
|
|
78
87
|
...(assignment.lineItemId !== undefined ? { lineItemId: assignment.lineItemId } : {}),
|
|
79
88
|
...(categorySplits !== undefined ? { categorySplits } : {}),
|
|
@@ -130,6 +139,20 @@ function isTransaction(value) {
|
|
|
130
139
|
&& (value.categoryId === null || typeof value.categoryId === 'string')
|
|
131
140
|
&& (value.lineItemId === null || typeof value.lineItemId === 'string')
|
|
132
141
|
&& Array.isArray(value.categorySplits)
|
|
142
|
+
&& (value.jointBudgetContribution === null
|
|
143
|
+
|| (isObject(value.jointBudgetContribution)
|
|
144
|
+
&& typeof value.jointBudgetContribution.eligible === 'boolean'
|
|
145
|
+
&& typeof value.jointBudgetContribution.included === 'boolean'
|
|
146
|
+
&& Number.isInteger(value.jointBudgetContribution.amountPence)
|
|
147
|
+
&& Number(value.jointBudgetContribution.amountPence) > 0
|
|
148
|
+
&& (value.jointBudgetContribution.categoryId === null
|
|
149
|
+
|| typeof value.jointBudgetContribution.categoryId === 'string')
|
|
150
|
+
&& (value.jointBudgetContribution.lineItemId === null
|
|
151
|
+
|| typeof value.jointBudgetContribution.lineItemId === 'string')
|
|
152
|
+
&& Array.isArray(value.jointBudgetContribution.categorySplits)
|
|
153
|
+
&& (value.jointBudgetContribution.incomeSubtype === null
|
|
154
|
+
|| value.jointBudgetContribution.incomeSubtype === 'pay'
|
|
155
|
+
|| value.jointBudgetContribution.incomeSubtype === 'interest')))
|
|
133
156
|
&& (value.incomeSubtype === null
|
|
134
157
|
|| value.incomeSubtype === 'pay'
|
|
135
158
|
|| value.incomeSubtype === 'interest'));
|
|
@@ -143,12 +166,28 @@ function isTransactionsResponse(value) {
|
|
|
143
166
|
function isAssignmentResponse(value) {
|
|
144
167
|
return (isObject(value)
|
|
145
168
|
&& Array.isArray(value.succeeded)
|
|
146
|
-
&& value.succeeded.every((item) => isObject(item)
|
|
169
|
+
&& value.succeeded.every((item) => (isObject(item)
|
|
170
|
+
&& typeof item.transactionRef === 'string'
|
|
171
|
+
&& (item.assignmentScope === 'personal' || item.assignmentScope === 'joint')))
|
|
147
172
|
&& Array.isArray(value.failed)
|
|
148
173
|
&& value.failed.every((item) => (isObject(item)
|
|
149
174
|
&& typeof item.error === 'string'
|
|
150
175
|
&& (item.transactionRef === undefined || typeof item.transactionRef === 'string'))));
|
|
151
176
|
}
|
|
177
|
+
export function validateJointBudgetSettingsResponse(value) {
|
|
178
|
+
if (!isObject(value)
|
|
179
|
+
|| Object.keys(value).some((key) => !new Set([
|
|
180
|
+
'includeSharedPersonalTransactions',
|
|
181
|
+
'updatedAt',
|
|
182
|
+
'updatedBy',
|
|
183
|
+
]).has(key))
|
|
184
|
+
|| typeof value.includeSharedPersonalTransactions !== 'boolean'
|
|
185
|
+
|| (value.updatedAt !== null && !isIsoDateTime(value.updatedAt))
|
|
186
|
+
|| (value.updatedBy !== null && typeof value.updatedBy !== 'string')) {
|
|
187
|
+
throw new ApiError('Invalid joint budget settings response from the Agent API');
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
152
191
|
function isHttpUrl(value) {
|
|
153
192
|
if (typeof value !== 'string')
|
|
154
193
|
return false;
|
|
@@ -175,6 +214,55 @@ function isPartnerResponse(value) {
|
|
|
175
214
|
&& isIsoDateTime(value.expiresAt)
|
|
176
215
|
&& value.status === 'open');
|
|
177
216
|
}
|
|
217
|
+
function hasOnlyFields(value, fields) {
|
|
218
|
+
const allowed = new Set(fields);
|
|
219
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
220
|
+
}
|
|
221
|
+
function isGoal(value) {
|
|
222
|
+
return (isObject(value)
|
|
223
|
+
&& hasOnlyFields(value, [
|
|
224
|
+
'id',
|
|
225
|
+
'name',
|
|
226
|
+
'targetAmount',
|
|
227
|
+
'targetMonthKey',
|
|
228
|
+
'isAchieved',
|
|
229
|
+
'sharedWithPartner',
|
|
230
|
+
])
|
|
231
|
+
&& typeof value.id === 'string'
|
|
232
|
+
&& value.id.trim().length > 0
|
|
233
|
+
&& typeof value.name === 'string'
|
|
234
|
+
&& value.name.trim().length > 0
|
|
235
|
+
&& (value.targetAmount === null
|
|
236
|
+
|| (typeof value.targetAmount === 'number' && Number.isFinite(value.targetAmount)))
|
|
237
|
+
&& (value.targetMonthKey === null
|
|
238
|
+
|| (typeof value.targetMonthKey === 'string'
|
|
239
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.targetMonthKey)))
|
|
240
|
+
&& typeof value.isAchieved === 'boolean'
|
|
241
|
+
&& typeof value.sharedWithPartner === 'boolean');
|
|
242
|
+
}
|
|
243
|
+
function isCurrency(value) {
|
|
244
|
+
return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
|
|
245
|
+
}
|
|
246
|
+
function isGoalsResponse(value) {
|
|
247
|
+
return (isObject(value)
|
|
248
|
+
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
249
|
+
&& isCurrency(value.currency)
|
|
250
|
+
&& Array.isArray(value.goals)
|
|
251
|
+
&& value.goals.every(isGoal));
|
|
252
|
+
}
|
|
253
|
+
function isGoalMutationResponse(value) {
|
|
254
|
+
return (isObject(value)
|
|
255
|
+
&& hasOnlyFields(value, ['currency', 'goal'])
|
|
256
|
+
&& isCurrency(value.currency)
|
|
257
|
+
&& isGoal(value.goal));
|
|
258
|
+
}
|
|
259
|
+
function isGoalDeleteResponse(value) {
|
|
260
|
+
return (isObject(value)
|
|
261
|
+
&& hasOnlyFields(value, ['deleted', 'deletedGoalId'])
|
|
262
|
+
&& value.deleted === true
|
|
263
|
+
&& typeof value.deletedGoalId === 'string'
|
|
264
|
+
&& value.deletedGoalId.trim().length > 0);
|
|
265
|
+
}
|
|
178
266
|
export function parseApiResponse(command, value) {
|
|
179
267
|
const valid = command === 'categories'
|
|
180
268
|
? isCategoryResponse(value)
|
|
@@ -182,7 +270,13 @@ export function parseApiResponse(command, value) {
|
|
|
182
270
|
? isTransactionsResponse(value)
|
|
183
271
|
: command === 'assign'
|
|
184
272
|
? isAssignmentResponse(value)
|
|
185
|
-
:
|
|
273
|
+
: command === 'ask-partner'
|
|
274
|
+
? isPartnerResponse(value)
|
|
275
|
+
: command === 'goals-list'
|
|
276
|
+
? isGoalsResponse(value)
|
|
277
|
+
: command === 'goals-delete'
|
|
278
|
+
? isGoalDeleteResponse(value)
|
|
279
|
+
: isGoalMutationResponse(value);
|
|
186
280
|
if (!valid) {
|
|
187
281
|
const label = command === 'assign' ? 'assignment' : command;
|
|
188
282
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ApiError, ConfigError, } from './errors.js';
|
|
2
|
+
const KEYCHAIN_SERVICE = 'app.slothmoney.agent-cli';
|
|
3
|
+
export function secureStorageUnavailableError() {
|
|
4
|
+
return new ConfigError('Native secure credential storage is unavailable. Set SLOTH_AGENT_TOKEN instead.');
|
|
5
|
+
}
|
|
6
|
+
export async function createSystemCredentialStore() {
|
|
7
|
+
let keytar;
|
|
8
|
+
try {
|
|
9
|
+
const module = await import('@github/keytar');
|
|
10
|
+
keytar = ('default' in module
|
|
11
|
+
? module.default
|
|
12
|
+
: module);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw secureStorageUnavailableError();
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
delete: async (origin) => {
|
|
19
|
+
try {
|
|
20
|
+
return await keytar.deletePassword(KEYCHAIN_SERVICE, origin);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new ApiError('Failed to delete the credential from native secure storage');
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
get: async (origin) => {
|
|
27
|
+
try {
|
|
28
|
+
return await keytar.getPassword(KEYCHAIN_SERVICE, origin);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw secureStorageUnavailableError();
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
set: async (origin, token) => {
|
|
35
|
+
try {
|
|
36
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, origin, token);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new ApiError('Failed to save the credential to native secure storage');
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/errors.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slothmoney/agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Command-line access to the Sloth Money Agent API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,5 +50,9 @@
|
|
|
50
50
|
"typescript": "^5.8.3",
|
|
51
51
|
"typescript-eslint": "^8.35.1",
|
|
52
52
|
"vitest": "^3.2.4"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@github/keytar": "7.10.6",
|
|
56
|
+
"@inquirer/password": "4.0.18"
|
|
53
57
|
}
|
|
54
58
|
}
|