@open-loyalty/mcp-server 1.5.3 → 1.7.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/config.d.ts +4 -0
- package/dist/config.js +11 -0
- package/dist/index.js +0 -8
- package/dist/server.js +13 -0
- package/dist/tools/achievement/handlers.js +47 -0
- package/dist/tools/achievement/index.d.ts +11 -4
- package/dist/tools/achievement/index.js +12 -1
- package/dist/tools/achievement/schemas.d.ts +4 -4
- package/dist/tools/achievement/schemas.js +13 -12
- package/dist/tools/admin/handlers.d.ts +48 -0
- package/dist/tools/admin/handlers.js +159 -0
- package/dist/tools/admin/index.d.ts +86 -0
- package/dist/tools/admin/index.js +64 -0
- package/dist/tools/admin/schemas.d.ts +40 -0
- package/dist/tools/admin/schemas.js +40 -0
- package/dist/tools/analytics/handlers.d.ts +42 -0
- package/dist/tools/analytics/handlers.js +282 -0
- package/dist/tools/analytics/index.d.ts +108 -0
- package/dist/tools/analytics/index.js +91 -0
- package/dist/tools/analytics/schemas.d.ts +42 -0
- package/dist/tools/analytics/schemas.js +47 -0
- package/dist/tools/apikey/handlers.d.ts +15 -0
- package/dist/tools/apikey/handlers.js +53 -0
- package/dist/tools/apikey/index.d.ts +41 -0
- package/dist/tools/apikey/index.js +38 -0
- package/dist/tools/apikey/schemas.d.ts +31 -0
- package/dist/tools/apikey/schemas.js +15 -0
- package/dist/tools/audit/handlers.d.ts +20 -0
- package/dist/tools/audit/handlers.js +82 -0
- package/dist/tools/audit/index.d.ts +36 -0
- package/dist/tools/audit/index.js +28 -0
- package/dist/tools/audit/schemas.d.ts +62 -0
- package/dist/tools/audit/schemas.js +18 -0
- package/dist/tools/badge/handlers.d.ts +45 -0
- package/dist/tools/badge/handlers.js +135 -0
- package/dist/tools/badge/index.d.ts +68 -0
- package/dist/tools/badge/index.js +47 -0
- package/dist/tools/badge/schemas.d.ts +37 -0
- package/dist/tools/badge/schemas.js +31 -0
- package/dist/tools/campaign/handlers.js +61 -0
- package/dist/tools/campaign/index.d.ts +12 -0
- package/dist/tools/campaign/index.js +20 -1
- package/dist/tools/campaign/member-handlers.js +37 -1
- package/dist/tools/campaign/schemas.js +16 -14
- package/dist/tools/custom-event/handlers.d.ts +98 -0
- package/dist/tools/custom-event/handlers.js +238 -0
- package/dist/tools/custom-event/index.d.ts +139 -0
- package/dist/tools/custom-event/index.js +78 -0
- package/dist/tools/custom-event/schemas.d.ts +87 -0
- package/dist/tools/custom-event/schemas.js +59 -0
- package/dist/tools/export/handlers.d.ts +29 -0
- package/dist/tools/export/handlers.js +128 -0
- package/dist/tools/export/index.d.ts +56 -0
- package/dist/tools/export/index.js +46 -0
- package/dist/tools/export/schemas.d.ts +42 -0
- package/dist/tools/export/schemas.js +41 -0
- package/dist/tools/import/handlers.d.ts +22 -0
- package/dist/tools/import/handlers.js +123 -0
- package/dist/tools/import/index.d.ts +45 -0
- package/dist/tools/import/index.js +41 -0
- package/dist/tools/import/schemas.d.ts +57 -0
- package/dist/tools/import/schemas.js +39 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +11 -11
- package/dist/tools/member/handlers.js +30 -0
- package/dist/tools/member/index.d.ts +10 -0
- package/dist/tools/member/index.js +10 -0
- package/dist/tools/member/schemas.js +13 -13
- package/dist/tools/points/handlers.js +73 -0
- package/dist/tools/points/index.d.ts +6 -0
- package/dist/tools/points/index.js +6 -0
- package/dist/tools/points/schemas.js +1 -1
- package/dist/tools/referral/index.d.ts +3 -0
- package/dist/tools/referral/index.js +3 -0
- package/dist/tools/reward/handlers.js +21 -13
- package/dist/tools/reward/index.d.ts +9 -0
- package/dist/tools/reward/index.js +12 -1
- package/dist/tools/reward/schemas.js +2 -2
- package/dist/tools/role/handlers.d.ts +35 -0
- package/dist/tools/role/handlers.js +127 -0
- package/dist/tools/role/index.d.ts +99 -0
- package/dist/tools/role/index.js +65 -0
- package/dist/tools/role/schemas.d.ts +56 -0
- package/dist/tools/role/schemas.js +35 -0
- package/dist/tools/segment/handlers.js +68 -1
- package/dist/tools/segment/index.d.ts +9 -0
- package/dist/tools/segment/index.js +13 -0
- package/dist/tools/segment/schemas.js +8 -5
- package/dist/tools/store/handlers.d.ts +25 -0
- package/dist/tools/store/handlers.js +89 -0
- package/dist/tools/store/index.d.ts +55 -0
- package/dist/tools/store/index.js +46 -0
- package/dist/tools/store/schemas.d.ts +38 -0
- package/dist/tools/store/schemas.js +23 -0
- package/dist/tools/tierset/handlers.js +92 -1
- package/dist/tools/tierset/index.d.ts +6 -0
- package/dist/tools/tierset/index.js +8 -1
- package/dist/tools/transaction/handlers.js +40 -0
- package/dist/tools/transaction/index.d.ts +4 -0
- package/dist/tools/transaction/index.js +4 -0
- package/dist/tools/transaction/schemas.js +3 -3
- package/dist/tools/wallet-type/index.d.ts +4 -0
- package/dist/tools/wallet-type/index.js +5 -1
- package/dist/tools/webhook/handlers.d.ts +34 -0
- package/dist/tools/webhook/handlers.js +147 -0
- package/dist/tools/webhook/index.d.ts +97 -0
- package/dist/tools/webhook/index.js +65 -0
- package/dist/tools/webhook/schemas.d.ts +72 -0
- package/dist/tools/{webhook.js → webhook/schemas.js} +0 -140
- package/dist/types/schemas/tierset.js +3 -1
- package/package.json +1 -1
- package/dist/tools/admin.d.ts +0 -165
- package/dist/tools/admin.js +0 -205
- package/dist/tools/analytics.d.ts +0 -180
- package/dist/tools/analytics.js +0 -255
- package/dist/tools/apikey.d.ts +0 -79
- package/dist/tools/apikey.js +0 -85
- package/dist/tools/audit.d.ts +0 -111
- package/dist/tools/audit.js +0 -94
- package/dist/tools/badge.d.ts +0 -143
- package/dist/tools/badge.js +0 -174
- package/dist/tools/custom-event.d.ts +0 -315
- package/dist/tools/custom-event.js +0 -271
- package/dist/tools/export.d.ts +0 -118
- package/dist/tools/export.js +0 -152
- package/dist/tools/import.d.ts +0 -116
- package/dist/tools/import.js +0 -143
- package/dist/tools/role.d.ts +0 -180
- package/dist/tools/role.js +0 -173
- package/dist/tools/store.d.ts +0 -109
- package/dist/tools/store.js +0 -125
- package/dist/tools/webhook.d.ts +0 -192
package/dist/config.d.ts
CHANGED
|
@@ -27,5 +27,9 @@ export declare function runWithConfig<T>(override: {
|
|
|
27
27
|
* Throws a clear error if no store code is available from either source.
|
|
28
28
|
*/
|
|
29
29
|
export declare function getStoreCode(storeCode?: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Returns true if required env vars are set (non-throwing check).
|
|
32
|
+
*/
|
|
33
|
+
export declare function isConfigured(): boolean;
|
|
30
34
|
export declare function getConfig(): Config;
|
|
31
35
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -32,6 +32,17 @@ export function getStoreCode(storeCode) {
|
|
|
32
32
|
}
|
|
33
33
|
return code;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if required env vars are set (non-throwing check).
|
|
37
|
+
*/
|
|
38
|
+
export function isConfigured() {
|
|
39
|
+
if (config)
|
|
40
|
+
return true;
|
|
41
|
+
const requestConfig = configStorage.getStore();
|
|
42
|
+
if (requestConfig)
|
|
43
|
+
return true;
|
|
44
|
+
return !!(process.env.OPENLOYALTY_API_URL && process.env.OPENLOYALTY_API_TOKEN);
|
|
45
|
+
}
|
|
35
46
|
export function getConfig() {
|
|
36
47
|
// Return request-scoped config if set (OAuth mode)
|
|
37
48
|
const requestConfig = configStorage.getStore();
|
package/dist/index.js
CHANGED
|
@@ -2,15 +2,7 @@
|
|
|
2
2
|
import "dotenv/config";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createServer } from "./server.js";
|
|
5
|
-
import { getConfig } from "./config.js";
|
|
6
5
|
async function main() {
|
|
7
|
-
try {
|
|
8
|
-
getConfig();
|
|
9
|
-
}
|
|
10
|
-
catch (error) {
|
|
11
|
-
console.error("Configuration error:", error instanceof Error ? error.message : error);
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
6
|
const server = createServer();
|
|
15
7
|
const transport = new StdioServerTransport();
|
|
16
8
|
await server.connect(transport);
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { getAllTools, getToolHandler } from "./tools/index.js";
|
|
4
4
|
import { OpenLoyaltyError } from "./utils/errors.js";
|
|
5
5
|
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
6
|
+
import { isConfigured } from "./config.js";
|
|
6
7
|
/**
|
|
7
8
|
* Convert an inputSchema object (Record<string, ZodType>) to a Zod object schema.
|
|
8
9
|
* This allows centralized validation of all tool inputs.
|
|
@@ -50,9 +51,21 @@ export function createServer() {
|
|
|
50
51
|
annotations: {
|
|
51
52
|
readOnlyHint: tool.readOnly,
|
|
52
53
|
destructiveHint: tool.destructive,
|
|
54
|
+
idempotentHint: tool.idempotent,
|
|
53
55
|
openWorldHint: true,
|
|
54
56
|
},
|
|
55
57
|
}, async (args) => {
|
|
58
|
+
if (!isConfigured()) {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: "Open Loyalty MCP server is not configured yet.\n\nRequired environment variables:\n- OPENLOYALTY_API_URL: Your OL instance API endpoint (e.g. https://your-instance.openloyalty.io/api)\n- OPENLOYALTY_API_TOKEN: API authentication token (Admin Panel > Settings > API Keys)\n- OPENLOYALTY_DEFAULT_STORE_CODE: Store identifier (optional, defaults to 'default')\n\nRun /openloyalty:setup in Claude Code to configure.",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
56
69
|
const handler = getToolHandler(tool.name);
|
|
57
70
|
if (!handler) {
|
|
58
71
|
return {
|
|
@@ -125,6 +125,15 @@ export async function achievementGet(input) {
|
|
|
125
125
|
return response;
|
|
126
126
|
}
|
|
127
127
|
catch (error) {
|
|
128
|
+
const axiosError = error;
|
|
129
|
+
if (axiosError.response?.status === 404) {
|
|
130
|
+
throw new OpenLoyaltyError({
|
|
131
|
+
code: "NOT_FOUND",
|
|
132
|
+
message: `Achievement '${input.achievementId}' not found`,
|
|
133
|
+
hint: "Use ol_achievement_list() to find existing achievements and their IDs.",
|
|
134
|
+
relatedTool: "ol_achievement_get",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
128
137
|
throw formatApiError(error, "ol_achievement_get");
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -147,6 +156,29 @@ export async function achievementUpdate(input) {
|
|
|
147
156
|
await apiPut(`/${storeCode}/achievement/${input.achievementId}`, { achievement: achievementPayload });
|
|
148
157
|
}
|
|
149
158
|
catch (error) {
|
|
159
|
+
const axiosError = error;
|
|
160
|
+
if (axiosError.response?.status === 404) {
|
|
161
|
+
throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Achievement '${input.achievementId}' not found`,
|
|
162
|
+
hint: "Use ol_achievement_list() to find existing achievements and their IDs.", relatedTool: "ol_achievement_update" });
|
|
163
|
+
}
|
|
164
|
+
const apiErrors = axiosError.response?.data?.errors || [];
|
|
165
|
+
const allMessages = [error instanceof Error ? error.message : "", axiosError.response?.data?.message || "",
|
|
166
|
+
...apiErrors.map(e => `${e.path || ""}: ${e.message}`)].join(" ").toLowerCase();
|
|
167
|
+
if (allMessages.includes("period") && (allMessages.includes("choice is invalid") || allMessages.includes("not valid"))) {
|
|
168
|
+
throw new OpenLoyaltyError({ code: "INVALID_PERIOD_TYPE", message: "Invalid period type in achievement rules",
|
|
169
|
+
hint: "For period.type use: 'day' (NOT 'days'), 'week', 'month', 'year', 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'.",
|
|
170
|
+
relatedTool: "ol_achievement_update" });
|
|
171
|
+
}
|
|
172
|
+
if (allMessages.includes("interval") && (allMessages.includes("choice is invalid") || allMessages.includes("not valid"))) {
|
|
173
|
+
throw new OpenLoyaltyError({ code: "INVALID_INTERVAL_TYPE", message: "Invalid interval type in achievement rule limit",
|
|
174
|
+
hint: "For limit.interval.type use: 'calendarDays' (NOT 'days'), 'calendarWeeks', 'calendarMonths', 'calendarYears'.",
|
|
175
|
+
relatedTool: "ol_achievement_update" });
|
|
176
|
+
}
|
|
177
|
+
if (allMessages.includes("choice is invalid") || allMessages.includes("selected choice is invalid")) {
|
|
178
|
+
throw new OpenLoyaltyError({ code: "INVALID_ENUM_VALUE", message: "An enum value in the achievement configuration is invalid",
|
|
179
|
+
hint: "Check: period.type ('day' not 'days'), limit.interval.type ('calendarDays' not 'days'), trigger, aggregation.type ('quantity').",
|
|
180
|
+
relatedTool: "ol_achievement_update" });
|
|
181
|
+
}
|
|
150
182
|
throw formatApiError(error, "ol_achievement_update");
|
|
151
183
|
}
|
|
152
184
|
}
|
|
@@ -162,6 +194,11 @@ export async function achievementPatch(input) {
|
|
|
162
194
|
await apiPatch(`/${storeCode}/achievement/${input.achievementId}`, { achievement: achievementPayload });
|
|
163
195
|
}
|
|
164
196
|
catch (error) {
|
|
197
|
+
const axiosError = error;
|
|
198
|
+
if (axiosError.response?.status === 404) {
|
|
199
|
+
throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Achievement '${input.achievementId}' not found`,
|
|
200
|
+
hint: "Use ol_achievement_list() to find existing achievements and their IDs.", relatedTool: "ol_achievement_patch" });
|
|
201
|
+
}
|
|
165
202
|
throw formatApiError(error, "ol_achievement_patch");
|
|
166
203
|
}
|
|
167
204
|
}
|
|
@@ -186,6 +223,11 @@ export async function achievementGetMemberProgress(input) {
|
|
|
186
223
|
return achievements[0];
|
|
187
224
|
}
|
|
188
225
|
catch (error) {
|
|
226
|
+
const axiosError = error;
|
|
227
|
+
if (axiosError.response?.status === 404) {
|
|
228
|
+
throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Member '${input.memberId}' not found`,
|
|
229
|
+
hint: "Use ol_member_list() to search for the member by email, name, or phone.", relatedTool: "ol_achievement_get_member_progress" });
|
|
230
|
+
}
|
|
189
231
|
throw formatApiError(error, "ol_achievement_get_member_progress");
|
|
190
232
|
}
|
|
191
233
|
}
|
|
@@ -215,6 +257,11 @@ export async function achievementListMemberAchievements(input) {
|
|
|
215
257
|
};
|
|
216
258
|
}
|
|
217
259
|
catch (error) {
|
|
260
|
+
const axiosError = error;
|
|
261
|
+
if (axiosError.response?.status === 404) {
|
|
262
|
+
throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Member '${input.memberId}' not found`,
|
|
263
|
+
hint: "Use ol_member_list() to search for the member by email, name, or phone.", relatedTool: "ol_achievement_list_member_achievements" });
|
|
264
|
+
}
|
|
218
265
|
throw formatApiError(error, "ol_achievement_list_member_achievements");
|
|
219
266
|
}
|
|
220
267
|
}
|
|
@@ -7,6 +7,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
7
7
|
readonly title: "List Achievements";
|
|
8
8
|
readonly description: "List achievements. Achievements gamify member behavior by setting goals (e.g., 'Make 5 purchases this month'). Returns achievementId, name, active status, and associated badge. Use achievement_get for full rules and configuration.";
|
|
9
9
|
readonly readOnly: true;
|
|
10
|
+
readonly idempotent: true;
|
|
10
11
|
readonly inputSchema: {
|
|
11
12
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
12
13
|
page: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
@@ -20,6 +21,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
20
21
|
readonly title: "Create Achievement";
|
|
21
22
|
readonly description: string;
|
|
22
23
|
readonly readOnly: false;
|
|
24
|
+
readonly idempotent: false;
|
|
23
25
|
readonly inputSchema: {
|
|
24
26
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
25
27
|
translations: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
|
|
@@ -155,7 +157,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
155
157
|
uniqueReferee: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
156
158
|
}, "strip", import("zod").ZodTypeAny, {
|
|
157
159
|
type: "direct" | "referral";
|
|
158
|
-
trigger: "
|
|
160
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
159
161
|
completeRule: {
|
|
160
162
|
periodGoal: string | number;
|
|
161
163
|
period: {
|
|
@@ -185,7 +187,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
185
187
|
uniqueReferee?: boolean | undefined;
|
|
186
188
|
}, {
|
|
187
189
|
type: "direct" | "referral";
|
|
188
|
-
trigger: "
|
|
190
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
189
191
|
completeRule: {
|
|
190
192
|
periodGoal: string | number;
|
|
191
193
|
period: {
|
|
@@ -222,6 +224,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
222
224
|
readonly title: "Get Achievement Details";
|
|
223
225
|
readonly description: "Get achievement details including all rules, conditions, activity period, limits, and completions count.";
|
|
224
226
|
readonly readOnly: true;
|
|
227
|
+
readonly idempotent: true;
|
|
225
228
|
readonly inputSchema: {
|
|
226
229
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
227
230
|
achievementId: import("zod").ZodString;
|
|
@@ -232,6 +235,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
232
235
|
readonly title: "Update Achievement";
|
|
233
236
|
readonly description: "Update achievement configuration. Requires full achievement object (translations, rules). Use achievement_get first to retrieve current configuration.";
|
|
234
237
|
readonly readOnly: false;
|
|
238
|
+
readonly idempotent: true;
|
|
235
239
|
readonly inputSchema: {
|
|
236
240
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
237
241
|
achievementId: import("zod").ZodString;
|
|
@@ -368,7 +372,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
368
372
|
uniqueReferee: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
369
373
|
}, "strip", import("zod").ZodTypeAny, {
|
|
370
374
|
type: "direct" | "referral";
|
|
371
|
-
trigger: "
|
|
375
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
372
376
|
completeRule: {
|
|
373
377
|
periodGoal: string | number;
|
|
374
378
|
period: {
|
|
@@ -398,7 +402,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
398
402
|
uniqueReferee?: boolean | undefined;
|
|
399
403
|
}, {
|
|
400
404
|
type: "direct" | "referral";
|
|
401
|
-
trigger: "
|
|
405
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
402
406
|
completeRule: {
|
|
403
407
|
periodGoal: string | number;
|
|
404
408
|
period: {
|
|
@@ -435,6 +439,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
435
439
|
readonly title: "Patch Achievement";
|
|
436
440
|
readonly description: "Partial update of achievement. Use for simple changes like activating/deactivating or updating translations without providing full rules.";
|
|
437
441
|
readonly readOnly: false;
|
|
442
|
+
readonly idempotent: true;
|
|
438
443
|
readonly inputSchema: {
|
|
439
444
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
440
445
|
achievementId: import("zod").ZodString;
|
|
@@ -456,6 +461,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
456
461
|
readonly title: "Get Member Achievement Progress";
|
|
457
462
|
readonly description: "Get member's progress on a specific achievement. Returns completedCount, and for each rule: periodGoal, currentPeriodValue, and consecutive period tracking.";
|
|
458
463
|
readonly readOnly: true;
|
|
464
|
+
readonly idempotent: true;
|
|
459
465
|
readonly inputSchema: {
|
|
460
466
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
461
467
|
memberId: import("zod").ZodString;
|
|
@@ -467,6 +473,7 @@ export declare const achievementToolDefinitions: readonly [{
|
|
|
467
473
|
readonly title: "List Member Achievements";
|
|
468
474
|
readonly description: "List all achievements with member's progress. Returns each achievement's status, completion count, and per-rule progress. Use for displaying gamification dashboard.";
|
|
469
475
|
readonly readOnly: true;
|
|
476
|
+
readonly idempotent: true;
|
|
470
477
|
readonly inputSchema: {
|
|
471
478
|
storeCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
472
479
|
memberId: import("zod").ZodString;
|
|
@@ -10,6 +10,7 @@ export const achievementToolDefinitions = [
|
|
|
10
10
|
title: "List Achievements",
|
|
11
11
|
description: "List achievements. Achievements gamify member behavior by setting goals (e.g., 'Make 5 purchases this month'). Returns achievementId, name, active status, and associated badge. Use achievement_get for full rules and configuration.",
|
|
12
12
|
readOnly: true,
|
|
13
|
+
idempotent: true,
|
|
13
14
|
inputSchema: AchievementListInputSchema,
|
|
14
15
|
handler: achievementList,
|
|
15
16
|
},
|
|
@@ -25,8 +26,13 @@ export const achievementToolDefinitions = [
|
|
|
25
26
|
"PATTERNS: " +
|
|
26
27
|
"ONE-TIME: period: { type: 'day', consecutive: 1 }, limit: { value: 1 }, NO rule-level limit. " +
|
|
27
28
|
"STREAK: period: { type: 'calendarDays', consecutive: 7 }, rule limit: { value: 1, interval: { type: 'calendarDays', value: 1 } }. " +
|
|
28
|
-
"Every rule requires: type='direct', aggregation: { type: 'quantity' }, completeRule with periodGoal."
|
|
29
|
+
"Every rule requires: type='direct', aggregation: { type: 'quantity' }, completeRule with periodGoal. " +
|
|
30
|
+
"COMPLETE EXAMPLE for one-time badge: " +
|
|
31
|
+
"rules: [{ type: 'direct', trigger: 'transaction', aggregation: { type: 'quantity' }, " +
|
|
32
|
+
"completeRule: { periodGoal: 1, period: { type: 'day', consecutive: 1 } } }], " +
|
|
33
|
+
"limit: { value: 1 }",
|
|
29
34
|
readOnly: false,
|
|
35
|
+
idempotent: false,
|
|
30
36
|
inputSchema: AchievementCreateInputSchema,
|
|
31
37
|
handler: achievementCreate,
|
|
32
38
|
},
|
|
@@ -35,6 +41,7 @@ export const achievementToolDefinitions = [
|
|
|
35
41
|
title: "Get Achievement Details",
|
|
36
42
|
description: "Get achievement details including all rules, conditions, activity period, limits, and completions count.",
|
|
37
43
|
readOnly: true,
|
|
44
|
+
idempotent: true,
|
|
38
45
|
inputSchema: AchievementGetInputSchema,
|
|
39
46
|
handler: achievementGet,
|
|
40
47
|
},
|
|
@@ -43,6 +50,7 @@ export const achievementToolDefinitions = [
|
|
|
43
50
|
title: "Update Achievement",
|
|
44
51
|
description: "Update achievement configuration. Requires full achievement object (translations, rules). Use achievement_get first to retrieve current configuration.",
|
|
45
52
|
readOnly: false,
|
|
53
|
+
idempotent: true,
|
|
46
54
|
inputSchema: AchievementUpdateInputSchema,
|
|
47
55
|
handler: achievementUpdate,
|
|
48
56
|
},
|
|
@@ -51,6 +59,7 @@ export const achievementToolDefinitions = [
|
|
|
51
59
|
title: "Patch Achievement",
|
|
52
60
|
description: "Partial update of achievement. Use for simple changes like activating/deactivating or updating translations without providing full rules.",
|
|
53
61
|
readOnly: false,
|
|
62
|
+
idempotent: true,
|
|
54
63
|
inputSchema: AchievementPatchInputSchema,
|
|
55
64
|
handler: achievementPatch,
|
|
56
65
|
},
|
|
@@ -59,6 +68,7 @@ export const achievementToolDefinitions = [
|
|
|
59
68
|
title: "Get Member Achievement Progress",
|
|
60
69
|
description: "Get member's progress on a specific achievement. Returns completedCount, and for each rule: periodGoal, currentPeriodValue, and consecutive period tracking.",
|
|
61
70
|
readOnly: true,
|
|
71
|
+
idempotent: true,
|
|
62
72
|
inputSchema: AchievementGetMemberProgressInputSchema,
|
|
63
73
|
handler: achievementGetMemberProgress,
|
|
64
74
|
},
|
|
@@ -67,6 +77,7 @@ export const achievementToolDefinitions = [
|
|
|
67
77
|
title: "List Member Achievements",
|
|
68
78
|
description: "List all achievements with member's progress. Returns each achievement's status, completion count, and per-rule progress. Use for displaying gamification dashboard.",
|
|
69
79
|
readOnly: true,
|
|
80
|
+
idempotent: true,
|
|
70
81
|
inputSchema: AchievementListMemberAchievementsInputSchema,
|
|
71
82
|
handler: achievementListMemberAchievements,
|
|
72
83
|
},
|
|
@@ -141,7 +141,7 @@ export declare const AchievementCreateInputSchema: {
|
|
|
141
141
|
uniqueReferee: z.ZodOptional<z.ZodBoolean>;
|
|
142
142
|
}, "strip", z.ZodTypeAny, {
|
|
143
143
|
type: "direct" | "referral";
|
|
144
|
-
trigger: "
|
|
144
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
145
145
|
completeRule: {
|
|
146
146
|
periodGoal: string | number;
|
|
147
147
|
period: {
|
|
@@ -171,7 +171,7 @@ export declare const AchievementCreateInputSchema: {
|
|
|
171
171
|
uniqueReferee?: boolean | undefined;
|
|
172
172
|
}, {
|
|
173
173
|
type: "direct" | "referral";
|
|
174
|
-
trigger: "
|
|
174
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
175
175
|
completeRule: {
|
|
176
176
|
periodGoal: string | number;
|
|
177
177
|
period: {
|
|
@@ -342,7 +342,7 @@ export declare const AchievementUpdateInputSchema: {
|
|
|
342
342
|
uniqueReferee: z.ZodOptional<z.ZodBoolean>;
|
|
343
343
|
}, "strip", z.ZodTypeAny, {
|
|
344
344
|
type: "direct" | "referral";
|
|
345
|
-
trigger: "
|
|
345
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
346
346
|
completeRule: {
|
|
347
347
|
periodGoal: string | number;
|
|
348
348
|
period: {
|
|
@@ -372,7 +372,7 @@ export declare const AchievementUpdateInputSchema: {
|
|
|
372
372
|
uniqueReferee?: boolean | undefined;
|
|
373
373
|
}, {
|
|
374
374
|
type: "direct" | "referral";
|
|
375
|
-
trigger: "
|
|
375
|
+
trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
|
|
376
376
|
completeRule: {
|
|
377
377
|
periodGoal: string | number;
|
|
378
378
|
period: {
|
|
@@ -83,15 +83,15 @@ export const AchievementCreateInputSchema = {
|
|
|
83
83
|
})).describe("Achievement name and description. At least 'en' key required."),
|
|
84
84
|
active: z.boolean().optional().describe("Whether achievement is active (default: false)."),
|
|
85
85
|
activity: z.object({
|
|
86
|
-
startsAt: z.string().optional().describe("ISO datetime when achievement becomes active."),
|
|
87
|
-
endsAt: z.string().optional().describe("ISO datetime when achievement ends."),
|
|
88
|
-
operator: z.string().optional().describe("Activity condition operator."),
|
|
86
|
+
startsAt: z.string().optional().describe("ISO 8601 datetime when achievement becomes active (e.g., '2026-01-01T00:00:00+00:00')."),
|
|
87
|
+
endsAt: z.string().optional().describe("ISO 8601 datetime when achievement ends (e.g., '2026-12-31T23:59:59+00:00')."),
|
|
88
|
+
operator: z.string().optional().describe("Activity condition operator: 'between' (both startsAt and endsAt), 'from' (start only), or 'to' (end only)."),
|
|
89
89
|
}).optional().describe("Time period configuration."),
|
|
90
90
|
limit: z.object({
|
|
91
|
-
value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges)."),
|
|
91
|
+
value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges). Omit for unlimited."),
|
|
92
92
|
interval: z.object({
|
|
93
|
-
type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'."),
|
|
94
|
-
value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period)."),
|
|
93
|
+
type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'. NOT 'days'/'weeks' -- must use 'calendar' prefix."),
|
|
94
|
+
value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period, 7 for every 7 calendar days)."),
|
|
95
95
|
}).optional().describe("Interval for repeatable achievements (omit for one-time badges)."),
|
|
96
96
|
}).optional().describe("How many times a member can earn this achievement. " +
|
|
97
97
|
"BUSINESS DECISION: For one-time badges ('First Purchase'): set value=1 with NO interval. For repeatable challenges: omit or set higher. " +
|
|
@@ -115,18 +115,19 @@ export const AchievementUpdateInputSchema = {
|
|
|
115
115
|
active: z.boolean().optional().describe("Whether achievement is active."),
|
|
116
116
|
activity: z.object({
|
|
117
117
|
startsAt: z.string().optional().describe("ISO datetime when achievement becomes active (e.g., '2026-01-01T00:00:00+00:00')."),
|
|
118
|
-
endsAt: z.string().optional().describe("ISO datetime when achievement ends."),
|
|
119
|
-
operator: z.string().optional().describe("Activity condition operator: 'between' (both
|
|
118
|
+
endsAt: z.string().optional().describe("ISO datetime when achievement ends (e.g., '2026-12-31T23:59:59+00:00')."),
|
|
119
|
+
operator: z.string().optional().describe("Activity condition operator: 'between' (both startsAt and endsAt), 'from' (start only), or 'to' (end only)."),
|
|
120
120
|
}).optional().describe("Time period configuration."),
|
|
121
121
|
limit: z.object({
|
|
122
|
-
value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges)."),
|
|
122
|
+
value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges). Omit for unlimited."),
|
|
123
123
|
interval: z.object({
|
|
124
|
-
type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'."),
|
|
125
|
-
value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period)."),
|
|
124
|
+
type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'. NOT 'days'/'weeks' -- must use 'calendar' prefix."),
|
|
125
|
+
value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period, 7 for every 7 calendar days)."),
|
|
126
126
|
}).optional().describe("Interval for repeatable achievements (omit for one-time badges)."),
|
|
127
127
|
}).optional().describe("Achievement limit. For ONE-TIME badges: { value: 1 } with NO interval. " +
|
|
128
128
|
"For repeatable: omit or set higher value."),
|
|
129
|
-
rules: z.array(AchievementRuleInputSchema).describe("Achievement rules. Use achievement_get first to retrieve current rules and include achievementRuleId for existing rules."
|
|
129
|
+
rules: z.array(AchievementRuleInputSchema).describe("Achievement rules defining completion criteria. Use achievement_get first to retrieve current rules and include achievementRuleId for existing rules. " +
|
|
130
|
+
"See AchievementCreateInputSchema rules for full pattern guidance (ONE-TIME vs STREAK)."),
|
|
130
131
|
badgeTypeId: z.string().optional().describe("Badge type ID to award. Use ol_badge_list() to find valid IDs. " +
|
|
131
132
|
"May cause 'extra fields' errors in some API versions - try without if creation fails."),
|
|
132
133
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AdminUser, AdminUserListItem, AdminPermission } from "../../types/schemas/admin.js";
|
|
2
|
+
export declare function adminList(input: {
|
|
3
|
+
page?: number;
|
|
4
|
+
perPage?: number;
|
|
5
|
+
email?: string;
|
|
6
|
+
isActive?: boolean;
|
|
7
|
+
firstName?: string;
|
|
8
|
+
lastName?: string;
|
|
9
|
+
role?: string;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
items: AdminUserListItem[];
|
|
12
|
+
total?: number;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function adminCreate(input: {
|
|
15
|
+
email: string;
|
|
16
|
+
password: string;
|
|
17
|
+
firstName?: string;
|
|
18
|
+
lastName?: string;
|
|
19
|
+
phone?: string;
|
|
20
|
+
roles?: (string | number)[];
|
|
21
|
+
isActive?: boolean;
|
|
22
|
+
external?: boolean;
|
|
23
|
+
notificationsEnabled?: boolean;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
adminId: string;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function adminGet(input: {
|
|
28
|
+
adminId: string;
|
|
29
|
+
}): Promise<AdminUser>;
|
|
30
|
+
export declare function adminUpdate(input: {
|
|
31
|
+
adminId: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
firstName?: string;
|
|
34
|
+
lastName?: string;
|
|
35
|
+
phone?: string;
|
|
36
|
+
roles?: (string | number)[];
|
|
37
|
+
isActive?: boolean;
|
|
38
|
+
external?: boolean;
|
|
39
|
+
notificationsEnabled?: boolean;
|
|
40
|
+
password?: string;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
adminId: string;
|
|
43
|
+
}>;
|
|
44
|
+
export declare function adminChangePassword(input: {
|
|
45
|
+
currentPassword: string;
|
|
46
|
+
newPassword: string;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
export declare function adminGetPermissions(): Promise<AdminPermission>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { apiGet, apiPost, apiPut } from "../../client/http.js";
|
|
2
|
+
import { formatApiError, OpenLoyaltyError } from "../../utils/errors.js";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
export async function adminList(input) {
|
|
5
|
+
const params = new URLSearchParams();
|
|
6
|
+
if (input.page)
|
|
7
|
+
params.append("_page", String(input.page));
|
|
8
|
+
if (input.perPage)
|
|
9
|
+
params.append("_itemsOnPage", String(input.perPage));
|
|
10
|
+
if (input.email)
|
|
11
|
+
params.append("email", input.email);
|
|
12
|
+
if (input.isActive !== undefined)
|
|
13
|
+
params.append("isActive", String(input.isActive));
|
|
14
|
+
if (input.firstName)
|
|
15
|
+
params.append("firstName", input.firstName);
|
|
16
|
+
if (input.lastName)
|
|
17
|
+
params.append("lastName", input.lastName);
|
|
18
|
+
if (input.role)
|
|
19
|
+
params.append("role", input.role);
|
|
20
|
+
const queryString = params.toString();
|
|
21
|
+
const url = `/admin${queryString ? `?${queryString}` : ""}`;
|
|
22
|
+
try {
|
|
23
|
+
const response = await apiGet(url);
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (axios.isAxiosError(error) && error.response?.status === 403) {
|
|
28
|
+
throw new OpenLoyaltyError({
|
|
29
|
+
code: "ADMIN_PERMISSION_DENIED",
|
|
30
|
+
message: "You don't have permission to list admin users",
|
|
31
|
+
hint: "Admin management requires the ADMIN:VIEW permission or super admin access. Use ol_admin_get_permissions() to check your current access level.",
|
|
32
|
+
relatedTool: "ol_admin_list",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
throw formatApiError(error, "ol_admin_list");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function adminCreate(input) {
|
|
39
|
+
const payload = {
|
|
40
|
+
email: input.email,
|
|
41
|
+
plainPassword: input.password,
|
|
42
|
+
};
|
|
43
|
+
if (input.firstName)
|
|
44
|
+
payload.firstName = input.firstName;
|
|
45
|
+
if (input.lastName)
|
|
46
|
+
payload.lastName = input.lastName;
|
|
47
|
+
if (input.phone)
|
|
48
|
+
payload.phone = input.phone;
|
|
49
|
+
if (input.roles)
|
|
50
|
+
payload.roles = input.roles;
|
|
51
|
+
if (input.isActive !== undefined)
|
|
52
|
+
payload.isActive = input.isActive;
|
|
53
|
+
if (input.external !== undefined)
|
|
54
|
+
payload.external = input.external;
|
|
55
|
+
if (input.notificationsEnabled !== undefined)
|
|
56
|
+
payload.notificationsEnabled = input.notificationsEnabled;
|
|
57
|
+
try {
|
|
58
|
+
const response = await apiPost("/admin/data", { admin: payload });
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (axios.isAxiosError(error)) {
|
|
63
|
+
const allMessages = [error.response?.data?.message || "", ...(error.response?.data?.errors || []).map((e) => e.message)].join(" ").toLowerCase();
|
|
64
|
+
if (allMessages.includes("email") && (allMessages.includes("already") || allMessages.includes("unique") || allMessages.includes("exists"))) {
|
|
65
|
+
throw new OpenLoyaltyError({ code: "DUPLICATE_EMAIL", message: `Admin with email '${input.email}' already exists`,
|
|
66
|
+
hint: `Use ol_admin_list(email: "${input.email}") to find the existing admin.`, relatedTool: "ol_admin_create" });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw formatApiError(error, "ol_admin_create");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function adminGet(input) {
|
|
73
|
+
try {
|
|
74
|
+
const response = await apiGet(`/admin/data/${input.adminId}`);
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
79
|
+
throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Admin '${input.adminId}' not found`,
|
|
80
|
+
hint: "Use ol_admin_list() to find existing admin users and their IDs.", relatedTool: "ol_admin_get" });
|
|
81
|
+
}
|
|
82
|
+
throw formatApiError(error, "ol_admin_get");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function adminUpdate(input) {
|
|
86
|
+
const payload = {};
|
|
87
|
+
if (input.email)
|
|
88
|
+
payload.email = input.email;
|
|
89
|
+
if (input.firstName)
|
|
90
|
+
payload.firstName = input.firstName;
|
|
91
|
+
if (input.lastName)
|
|
92
|
+
payload.lastName = input.lastName;
|
|
93
|
+
if (input.phone)
|
|
94
|
+
payload.phone = input.phone;
|
|
95
|
+
if (input.roles)
|
|
96
|
+
payload.roles = input.roles;
|
|
97
|
+
if (input.isActive !== undefined)
|
|
98
|
+
payload.isActive = input.isActive;
|
|
99
|
+
if (input.external !== undefined)
|
|
100
|
+
payload.external = input.external;
|
|
101
|
+
if (input.notificationsEnabled !== undefined)
|
|
102
|
+
payload.notificationsEnabled = input.notificationsEnabled;
|
|
103
|
+
if (input.password)
|
|
104
|
+
payload.plainPassword = input.password;
|
|
105
|
+
try {
|
|
106
|
+
const response = await apiPut(`/admin/data/${input.adminId}`, { admin: payload });
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
111
|
+
throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Admin '${input.adminId}' not found`,
|
|
112
|
+
hint: "Use ol_admin_list() to find existing admin users and their IDs.", relatedTool: "ol_admin_update" });
|
|
113
|
+
}
|
|
114
|
+
throw formatApiError(error, "ol_admin_update");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function adminChangePassword(input) {
|
|
118
|
+
try {
|
|
119
|
+
await apiPut("/admin/password", {
|
|
120
|
+
currentPassword: input.currentPassword,
|
|
121
|
+
plainPassword: input.newPassword,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (axios.isAxiosError(error)) {
|
|
126
|
+
const allMessages = [error.response?.data?.message || "", ...(error.response?.data?.errors || []).map((e) => e.message)].join(" ").toLowerCase();
|
|
127
|
+
if (allMessages.includes("password") && (allMessages.includes("invalid") || allMessages.includes("incorrect") || allMessages.includes("wrong") || allMessages.includes("mismatch"))) {
|
|
128
|
+
throw new OpenLoyaltyError({ code: "INVALID_PASSWORD", message: "Current password is incorrect",
|
|
129
|
+
hint: "The currentPassword you provided does not match. Verify the current password and try again.", relatedTool: "ol_admin_change_password" });
|
|
130
|
+
}
|
|
131
|
+
if (allMessages.includes("password") && (allMessages.includes("short") || allMessages.includes("weak") || allMessages.includes("length") || allMessages.includes("requirements") || allMessages.includes("strong"))) {
|
|
132
|
+
throw new OpenLoyaltyError({
|
|
133
|
+
code: "WEAK_PASSWORD",
|
|
134
|
+
message: "The new password does not meet security requirements",
|
|
135
|
+
hint: "Password must be at least 8 characters and include a mix of uppercase, lowercase, numbers, and special characters.",
|
|
136
|
+
relatedTool: "ol_admin_change_password",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw formatApiError(error, "ol_admin_change_password");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export async function adminGetPermissions() {
|
|
144
|
+
try {
|
|
145
|
+
const response = await apiGet("/admin/permissions");
|
|
146
|
+
return response;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
|
150
|
+
throw new OpenLoyaltyError({
|
|
151
|
+
code: "UNAUTHORIZED",
|
|
152
|
+
message: "Cannot retrieve permissions - authentication failed",
|
|
153
|
+
hint: "Your API token may be invalid or expired. Check the OL_API_TOKEN environment variable. If using OAuth, the token may need to be refreshed.",
|
|
154
|
+
relatedTool: "ol_admin_get_permissions",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
throw formatApiError(error, "ol_admin_get_permissions");
|
|
158
|
+
}
|
|
159
|
+
}
|