@jclvsh/iris 0.2.4 → 0.4.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/client.js +36 -1
- package/dist/commands/channels.js +1 -1
- package/dist/commands/conversations.js +1 -1
- package/dist/commands/keys.js +1 -1
- package/dist/commands/messages.js +1 -1
- package/dist/tools/ai.js +5 -5
- package/dist/tools/analytics.js +5 -5
- package/dist/tools/billing.js +1 -1
- package/dist/tools/campaigns.js +12 -12
- package/dist/tools/channels.js +4 -4
- package/dist/tools/contacts.js +8 -8
- package/dist/tools/conversations.js +5 -5
- package/dist/tools/discovery.js +50 -9
- package/dist/tools/keys.js +6 -29
- package/dist/tools/messages.js +6 -6
- package/dist/tools/notifications.js +4 -4
- package/dist/tools/webhooks.js +8 -8
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
function safeStringify(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
const parts = value
|
|
4
|
+
.map((entry) => {
|
|
5
|
+
if (typeof entry === "string")
|
|
6
|
+
return entry;
|
|
7
|
+
if (entry && typeof entry === "object") {
|
|
8
|
+
const msg = entry.msg;
|
|
9
|
+
if (typeof msg === "string")
|
|
10
|
+
return msg;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(entry);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
.filter((p) => typeof p === "string" && p.length > 0);
|
|
20
|
+
if (parts.length > 0)
|
|
21
|
+
return parts.join("; ");
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
1
30
|
export class IrisClient {
|
|
2
31
|
apiKey;
|
|
3
32
|
baseUrl;
|
|
@@ -77,7 +106,13 @@ export class IrisClient {
|
|
|
77
106
|
try {
|
|
78
107
|
const body = await response.json();
|
|
79
108
|
if (body?.error) {
|
|
80
|
-
const
|
|
109
|
+
const rawMessage = body.error.message;
|
|
110
|
+
const messageStr = typeof rawMessage === "string" && rawMessage.trim() !== ""
|
|
111
|
+
? rawMessage
|
|
112
|
+
: rawMessage != null
|
|
113
|
+
? safeStringify(rawMessage)
|
|
114
|
+
: "unknown error";
|
|
115
|
+
const parts = [messageStr];
|
|
81
116
|
if (body.error.code)
|
|
82
117
|
parts.unshift(`[${body.error.code}]`);
|
|
83
118
|
if (body.error.field)
|
|
@@ -28,7 +28,7 @@ export function registerChannelCommands(program, client) {
|
|
|
28
28
|
channels
|
|
29
29
|
.command("send")
|
|
30
30
|
.description("send a message through a specific channel in a conversation")
|
|
31
|
-
.requiredOption("--channel <channel>", "channel: email, instagram, twitter
|
|
31
|
+
.requiredOption("--channel <channel>", "channel: email, instagram, twitter")
|
|
32
32
|
.requiredOption("--conversation-id <id>", "conversation ID")
|
|
33
33
|
.requiredOption("--recipient-id <id>", "recipient (email for email channel; platform handle/ID for social)")
|
|
34
34
|
.requiredOption("-c, --content <text>", "message content")
|
|
@@ -11,7 +11,7 @@ export function registerConversationCommands(program, client) {
|
|
|
11
11
|
.option("-s, --page-size <n>", "items per page (max 100)", "50")
|
|
12
12
|
.option("--status <status>", "filter by status")
|
|
13
13
|
.option("--campaign-id <id>", "filter by campaign")
|
|
14
|
-
.option("--channel <channel>", "filter by channel: email, instagram, twitter
|
|
14
|
+
.option("--channel <channel>", "filter by channel: email, instagram, twitter")
|
|
15
15
|
.action(async (opts) => {
|
|
16
16
|
try {
|
|
17
17
|
const params = {};
|
package/dist/commands/keys.js
CHANGED
|
@@ -29,7 +29,7 @@ export function registerKeyCommands(program, client) {
|
|
|
29
29
|
.command("create")
|
|
30
30
|
.description("create a new API key (requires admin scope)")
|
|
31
31
|
.requiredOption("-n, --name <name>", "key name")
|
|
32
|
-
.option("--scopes <list>", "comma-separated scopes:
|
|
32
|
+
.option("--scopes <list>", "comma-separated scopes: read, write, delete, generate, send, admin (defaults to all)", parseList)
|
|
33
33
|
.action(async (opts) => {
|
|
34
34
|
try {
|
|
35
35
|
const body = { name: opts.name };
|
|
@@ -7,7 +7,7 @@ export function registerMessageCommands(program, client) {
|
|
|
7
7
|
.option("-p, --page <n>", "page number", "1")
|
|
8
8
|
.option("-s, --page-size <n>", "items per page (max 100)", "50")
|
|
9
9
|
.option("--campaign-id <id>", "filter by campaign")
|
|
10
|
-
.option("--channel <channel>", "filter by channel: email, instagram, twitter
|
|
10
|
+
.option("--channel <channel>", "filter by channel: email, instagram, twitter")
|
|
11
11
|
.option("--status <status>", "queue status (default: pending_approval)")
|
|
12
12
|
.action(async (opts) => {
|
|
13
13
|
try {
|
package/dist/tools/ai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerAiTools(server, client) {
|
|
3
|
-
server.tool("generate_content", "generate AI content for a specific contact conversation (email draft, follow-up, reply). requires
|
|
3
|
+
server.tool("generate_content", "generate AI content for a specific contact conversation (email draft, follow-up, reply). requires generate scope.", {
|
|
4
4
|
contact_id: z.string().uuid().describe("contact ID"),
|
|
5
5
|
conversation_id: z.string().uuid().describe("conversation ID"),
|
|
6
6
|
generation_type: z
|
|
@@ -19,7 +19,7 @@ export function registerAiTools(server, client) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
|
-
server.tool("generate_campaign_content", "generate AI content at the campaign level (email templates, subject lines). requires
|
|
22
|
+
server.tool("generate_campaign_content", "generate AI content at the campaign level (email templates, subject lines). requires generate scope.", {
|
|
23
23
|
campaign_id: z.string().uuid().describe("campaign ID"),
|
|
24
24
|
content_type: z
|
|
25
25
|
.enum(["email_template", "subject_lines", "campaign_brief"])
|
|
@@ -36,7 +36,7 @@ export function registerAiTools(server, client) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
-
server.tool("analyze_response", "analyze an inbound message using AI to determine sentiment, intent, and suggested actions. requires
|
|
39
|
+
server.tool("analyze_response", "analyze an inbound message using AI to determine sentiment, intent, and suggested actions. requires generate scope.", {
|
|
40
40
|
conversation_id: z.string().uuid().describe("conversation ID"),
|
|
41
41
|
message_content: z.string().min(1).max(10000).describe("message content to analyze"),
|
|
42
42
|
}, async (args) => {
|
|
@@ -51,7 +51,7 @@ export function registerAiTools(server, client) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
-
server.tool("get_ai_usage", "get AI generation usage limits and current counts for the billing period. requires
|
|
54
|
+
server.tool("get_ai_usage", "get AI generation usage limits and current counts for the billing period. requires read scope.", {}, async () => {
|
|
55
55
|
try {
|
|
56
56
|
const result = await client.get("/ai/usage");
|
|
57
57
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -63,7 +63,7 @@ export function registerAiTools(server, client) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
|
-
server.tool("get_ai_suggestions", "get AI-powered suggestions for a conversation (next steps, reply ideas). requires
|
|
66
|
+
server.tool("get_ai_suggestions", "get AI-powered suggestions for a conversation (next steps, reply ideas). requires read scope.", {
|
|
67
67
|
conversation_id: z.string().uuid().describe("conversation ID"),
|
|
68
68
|
}, async ({ conversation_id }) => {
|
|
69
69
|
try {
|
package/dist/tools/analytics.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerAnalyticsTools(server, client) {
|
|
3
|
-
server.tool("get_analytics_overview", "get a high-level overview of all campaign analytics. requires
|
|
3
|
+
server.tool("get_analytics_overview", "get a high-level overview of all campaign analytics. requires read scope.", {}, async () => {
|
|
4
4
|
try {
|
|
5
5
|
const result = await client.get("/analytics/overview");
|
|
6
6
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -12,7 +12,7 @@ export function registerAnalyticsTools(server, client) {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
});
|
|
15
|
-
server.tool("get_campaign_analytics", "get detailed analytics for a specific campaign. requires
|
|
15
|
+
server.tool("get_campaign_analytics", "get detailed analytics for a specific campaign. requires read scope.", {
|
|
16
16
|
id: z.string().uuid().describe("campaign ID"),
|
|
17
17
|
}, async ({ id }) => {
|
|
18
18
|
try {
|
|
@@ -26,7 +26,7 @@ export function registerAnalyticsTools(server, client) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
|
-
server.tool("get_performance_metrics", "get email performance metrics over time. requires
|
|
29
|
+
server.tool("get_performance_metrics", "get email performance metrics over time. requires read scope.", {
|
|
30
30
|
days: z
|
|
31
31
|
.number()
|
|
32
32
|
.int()
|
|
@@ -52,7 +52,7 @@ export function registerAnalyticsTools(server, client) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
-
server.tool("get_response_time", "get average response time analytics. requires
|
|
55
|
+
server.tool("get_response_time", "get average response time analytics. requires read scope.", {
|
|
56
56
|
campaign_id: z.string().uuid().optional().describe("filter by campaign ID"),
|
|
57
57
|
}, async ({ campaign_id }) => {
|
|
58
58
|
try {
|
|
@@ -69,7 +69,7 @@ export function registerAnalyticsTools(server, client) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
-
server.tool("get_contact_types", "get breakdown of contacts by type/source. requires
|
|
72
|
+
server.tool("get_contact_types", "get breakdown of contacts by type/source. requires read scope.", {}, async () => {
|
|
73
73
|
try {
|
|
74
74
|
const result = await client.get("/analytics/contact-types");
|
|
75
75
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
package/dist/tools/billing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function registerBillingTools(server, client) {
|
|
2
|
-
server.tool("get_billing_plans", "get all available billing plans with pricing and feature limits. requires
|
|
2
|
+
server.tool("get_billing_plans", "get all available billing plans with pricing and feature limits. requires read scope.", {}, async () => {
|
|
3
3
|
try {
|
|
4
4
|
const result = await client.get("/billing/plans");
|
|
5
5
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -13,7 +13,7 @@ const TARGET_PROFILE_TYPES = [
|
|
|
13
13
|
"other",
|
|
14
14
|
];
|
|
15
15
|
export function registerCampaignTools(server, client) {
|
|
16
|
-
server.tool("list_campaigns", "list all campaigns with optional filtering by status. requires
|
|
16
|
+
server.tool("list_campaigns", "list all campaigns with optional filtering by status. requires read scope.", {
|
|
17
17
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
18
18
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
19
19
|
status: z.enum(CAMPAIGN_STATUSES).optional().describe("filter by campaign status"),
|
|
@@ -36,7 +36,7 @@ export function registerCampaignTools(server, client) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
-
server.tool("create_campaign", "create a new outreach campaign. requires
|
|
39
|
+
server.tool("create_campaign", "create a new outreach campaign. requires write scope.", {
|
|
40
40
|
name: z.string().min(1).max(100).describe("campaign name"),
|
|
41
41
|
objective_type: z.enum(OBJECTIVE_TYPES).describe("campaign objective type"),
|
|
42
42
|
product_name: z.string().max(200).optional().describe("product name (for product_promotion objective)"),
|
|
@@ -54,7 +54,7 @@ export function registerCampaignTools(server, client) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
|
-
server.tool("get_campaign", "get a campaign by ID with stats. requires
|
|
57
|
+
server.tool("get_campaign", "get a campaign by ID with stats. requires read scope.", {
|
|
58
58
|
id: z.string().uuid().describe("campaign ID"),
|
|
59
59
|
}, async ({ id }) => {
|
|
60
60
|
try {
|
|
@@ -68,7 +68,7 @@ export function registerCampaignTools(server, client) {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
|
-
server.tool("update_campaign", "update a campaign's details. requires
|
|
71
|
+
server.tool("update_campaign", "update a campaign's details. requires write scope.", {
|
|
72
72
|
id: z.string().uuid().describe("campaign ID"),
|
|
73
73
|
name: z.string().min(1).max(100).optional().describe("new campaign name"),
|
|
74
74
|
status: z.enum(CAMPAIGN_STATUSES).optional().describe("new campaign status"),
|
|
@@ -87,7 +87,7 @@ export function registerCampaignTools(server, client) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
|
-
server.tool("delete_campaign", "soft-delete a campaign (sets status to deleted). requires
|
|
90
|
+
server.tool("delete_campaign", "soft-delete a campaign (sets status to deleted). requires write scope.", {
|
|
91
91
|
id: z.string().uuid().describe("campaign ID"),
|
|
92
92
|
}, async ({ id }) => {
|
|
93
93
|
try {
|
|
@@ -101,7 +101,7 @@ export function registerCampaignTools(server, client) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
|
-
server.tool("list_campaign_contacts", "list contacts in a campaign with optional status filter. requires
|
|
104
|
+
server.tool("list_campaign_contacts", "list contacts in a campaign with optional status filter. requires read scope.", {
|
|
105
105
|
id: z.string().uuid().describe("campaign ID"),
|
|
106
106
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
107
107
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
@@ -128,7 +128,7 @@ export function registerCampaignTools(server, client) {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
|
-
server.tool("get_campaign_usage", "get usage limits and current counts for a campaign. requires
|
|
131
|
+
server.tool("get_campaign_usage", "get usage limits and current counts for a campaign. requires read scope.", {
|
|
132
132
|
id: z.string().uuid().describe("campaign ID"),
|
|
133
133
|
}, async ({ id }) => {
|
|
134
134
|
try {
|
|
@@ -142,7 +142,7 @@ export function registerCampaignTools(server, client) {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
|
-
server.tool("analyze_campaign", "trigger AI analysis of a campaign to generate target profiles and recommendations. requires
|
|
145
|
+
server.tool("analyze_campaign", "trigger AI analysis of a campaign to generate target profiles and recommendations. requires write scope.", {
|
|
146
146
|
id: z.string().uuid().describe("campaign ID"),
|
|
147
147
|
}, async ({ id }) => {
|
|
148
148
|
try {
|
|
@@ -156,7 +156,7 @@ export function registerCampaignTools(server, client) {
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
|
-
server.tool("list_target_profiles", "list target profiles for a campaign. requires
|
|
159
|
+
server.tool("list_target_profiles", "list target profiles for a campaign. requires read scope.", {
|
|
160
160
|
id: z.string().uuid().describe("campaign ID"),
|
|
161
161
|
}, async ({ id }) => {
|
|
162
162
|
try {
|
|
@@ -170,7 +170,7 @@ export function registerCampaignTools(server, client) {
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
|
-
server.tool("create_target_profile", "create a target profile for a campaign. requires
|
|
173
|
+
server.tool("create_target_profile", "create a target profile for a campaign. requires write scope.", {
|
|
174
174
|
id: z.string().uuid().describe("campaign ID"),
|
|
175
175
|
name: z.string().min(1).max(200).describe("profile name"),
|
|
176
176
|
profile_type: z.enum(TARGET_PROFILE_TYPES).describe("type of target profile"),
|
|
@@ -178,7 +178,7 @@ export function registerCampaignTools(server, client) {
|
|
|
178
178
|
industries: z.array(z.string()).optional().describe("target industries"),
|
|
179
179
|
discovery_keywords: z.array(z.string()).optional().describe("keywords for contact discovery"),
|
|
180
180
|
recommended_channels: z
|
|
181
|
-
.array(z.enum(["email", "instagram", "twitter"
|
|
181
|
+
.array(z.enum(["email", "instagram", "twitter"]))
|
|
182
182
|
.optional()
|
|
183
183
|
.describe("recommended outreach channels"),
|
|
184
184
|
}, async ({ id, ...body }) => {
|
|
@@ -193,7 +193,7 @@ export function registerCampaignTools(server, client) {
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
});
|
|
196
|
-
server.tool("delete_target_profile", "delete a target profile from a campaign. requires
|
|
196
|
+
server.tool("delete_target_profile", "delete a target profile from a campaign. requires write scope.", {
|
|
197
197
|
id: z.string().uuid().describe("campaign ID"),
|
|
198
198
|
profile_id: z.string().uuid().describe("target profile ID"),
|
|
199
199
|
}, async ({ id, profile_id }) => {
|
package/dist/tools/channels.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
const CHANNELS = ["email", "instagram", "twitter"
|
|
2
|
+
const CHANNELS = ["email", "instagram", "twitter"];
|
|
3
3
|
export function registerChannelTools(server, client) {
|
|
4
|
-
server.tool("list_connected_accounts", "list all connected social media accounts. requires
|
|
4
|
+
server.tool("list_connected_accounts", "list all connected social media accounts. requires read scope.", {}, async () => {
|
|
5
5
|
try {
|
|
6
6
|
const result = await client.get("/channels/accounts");
|
|
7
7
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -13,7 +13,7 @@ export function registerChannelTools(server, client) {
|
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
|
-
server.tool("check_channel_availability", "check which outreach channels are available and configured. requires
|
|
16
|
+
server.tool("check_channel_availability", "check which outreach channels are available and configured. requires read scope.", {}, async () => {
|
|
17
17
|
try {
|
|
18
18
|
const result = await client.get("/channels/availability");
|
|
19
19
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -25,7 +25,7 @@ export function registerChannelTools(server, client) {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
|
-
server.tool("send_channel_message", "send a message through a specific channel in a conversation. requires
|
|
28
|
+
server.tool("send_channel_message", "send a message through a specific channel in a conversation. requires send scope.", {
|
|
29
29
|
channel: z.enum(CHANNELS).describe("outreach channel to use"),
|
|
30
30
|
conversation_id: z.string().uuid().describe("conversation ID"),
|
|
31
31
|
recipient_id: z
|
package/dist/tools/contacts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerContactTools(server, client) {
|
|
3
|
-
server.tool("list_contacts", "list all contacts with optional search. requires
|
|
3
|
+
server.tool("list_contacts", "list all contacts with optional search. requires read scope.", {
|
|
4
4
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
5
5
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
6
6
|
search: z.string().optional().describe("search by name, email, or company"),
|
|
@@ -23,7 +23,7 @@ export function registerContactTools(server, client) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
|
-
server.tool("create_contact", "create a new contact. requires
|
|
26
|
+
server.tool("create_contact", "create a new contact. requires write scope.", {
|
|
27
27
|
email: z.string().email().describe("contact email address"),
|
|
28
28
|
first_name: z.string().max(100).optional().describe("first name"),
|
|
29
29
|
last_name: z.string().max(100).optional().describe("last name"),
|
|
@@ -44,7 +44,7 @@ export function registerContactTools(server, client) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
|
-
server.tool("get_contact", "get a contact by ID. requires
|
|
47
|
+
server.tool("get_contact", "get a contact by ID. requires read scope.", {
|
|
48
48
|
id: z.string().uuid().describe("contact ID"),
|
|
49
49
|
}, async ({ id }) => {
|
|
50
50
|
try {
|
|
@@ -58,7 +58,7 @@ export function registerContactTools(server, client) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
|
-
server.tool("update_contact", "update a contact's details. requires
|
|
61
|
+
server.tool("update_contact", "update a contact's details. requires write scope.", {
|
|
62
62
|
id: z.string().uuid().describe("contact ID"),
|
|
63
63
|
email: z.string().email().optional().describe("email address"),
|
|
64
64
|
first_name: z.string().max(100).optional().describe("first name"),
|
|
@@ -80,7 +80,7 @@ export function registerContactTools(server, client) {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
|
-
server.tool("delete_contact", "soft-delete a contact. requires
|
|
83
|
+
server.tool("delete_contact", "soft-delete a contact. requires write scope.", {
|
|
84
84
|
id: z.string().uuid().describe("contact ID"),
|
|
85
85
|
}, async ({ id }) => {
|
|
86
86
|
try {
|
|
@@ -94,7 +94,7 @@ export function registerContactTools(server, client) {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
|
-
server.tool("bulk_import_contacts", "import multiple contacts at once, optionally adding them to a campaign. requires
|
|
97
|
+
server.tool("bulk_import_contacts", "import multiple contacts at once, optionally adding them to a campaign. requires write scope.", {
|
|
98
98
|
contacts: z
|
|
99
99
|
.array(z.object({
|
|
100
100
|
email: z.string().email().describe("contact email"),
|
|
@@ -120,7 +120,7 @@ export function registerContactTools(server, client) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
});
|
|
123
|
-
server.tool("get_contact_usage", "get contact usage limits and current counts for the billing period. requires
|
|
123
|
+
server.tool("get_contact_usage", "get contact usage limits and current counts for the billing period. requires read scope.", {}, async () => {
|
|
124
124
|
try {
|
|
125
125
|
const result = await client.get("/contacts/usage");
|
|
126
126
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -132,7 +132,7 @@ export function registerContactTools(server, client) {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
});
|
|
135
|
-
server.tool("list_contact_conversations", "list all conversations for a specific contact. requires
|
|
135
|
+
server.tool("list_contact_conversations", "list all conversations for a specific contact. requires read scope.", {
|
|
136
136
|
id: z.string().uuid().describe("contact ID"),
|
|
137
137
|
}, async ({ id }) => {
|
|
138
138
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const CONVERSATION_STATUSES = ["active", "completed", "failed", "human_review"];
|
|
3
3
|
export function registerConversationTools(server, client) {
|
|
4
|
-
server.tool("list_conversations", "list conversations with optional filtering. requires
|
|
4
|
+
server.tool("list_conversations", "list conversations with optional filtering. requires read scope.", {
|
|
5
5
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
6
6
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
7
7
|
status: z.enum(CONVERSATION_STATUSES).optional().describe("filter by conversation status"),
|
|
@@ -27,7 +27,7 @@ export function registerConversationTools(server, client) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
-
server.tool("get_conversation", "get a conversation by ID with contact and campaign details. requires
|
|
30
|
+
server.tool("get_conversation", "get a conversation by ID with contact and campaign details. requires read scope.", {
|
|
31
31
|
id: z.string().uuid().describe("conversation ID"),
|
|
32
32
|
}, async ({ id }) => {
|
|
33
33
|
try {
|
|
@@ -41,7 +41,7 @@ export function registerConversationTools(server, client) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
|
-
server.tool("get_conversation_messages", "get all messages in a conversation. requires
|
|
44
|
+
server.tool("get_conversation_messages", "get all messages in a conversation. requires read scope.", {
|
|
45
45
|
id: z.string().uuid().describe("conversation ID"),
|
|
46
46
|
}, async ({ id }) => {
|
|
47
47
|
try {
|
|
@@ -55,7 +55,7 @@ export function registerConversationTools(server, client) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
|
-
server.tool("mark_conversation_read", "mark a conversation as read. requires
|
|
58
|
+
server.tool("mark_conversation_read", "mark a conversation as read. requires write scope.", {
|
|
59
59
|
id: z.string().uuid().describe("conversation ID"),
|
|
60
60
|
}, async ({ id }) => {
|
|
61
61
|
try {
|
|
@@ -69,7 +69,7 @@ export function registerConversationTools(server, client) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
-
server.tool("get_engagement_metrics", "get overall engagement metrics across all conversations. requires
|
|
72
|
+
server.tool("get_engagement_metrics", "get overall engagement metrics across all conversations. requires read scope.", {}, async () => {
|
|
73
73
|
try {
|
|
74
74
|
const result = await client.get("/conversations/engagement");
|
|
75
75
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
package/dist/tools/discovery.js
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
const DISCOVERY_SOURCES = ["google_maps", "instagram", "twitter", "
|
|
2
|
+
const DISCOVERY_SOURCES = ["google_maps", "instagram", "twitter", "tiktok", "reddit"];
|
|
3
|
+
const PROFILE_TYPES = [
|
|
4
|
+
"local_business",
|
|
5
|
+
"content_creator",
|
|
6
|
+
"influencer",
|
|
7
|
+
"b2b_professional",
|
|
8
|
+
"ecommerce",
|
|
9
|
+
"saas_company",
|
|
10
|
+
"agency",
|
|
11
|
+
"freelancer",
|
|
12
|
+
"other",
|
|
13
|
+
];
|
|
3
14
|
export function registerDiscoveryTools(server, client) {
|
|
4
|
-
server.tool("search_contacts", "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
server.tool("search_contacts", "kick off a multi-source contact discovery job (Apify-backed). returns a job_id and contacts when ready (waits up to 90s; if status=queued, poll get_discovery_job). closed source list: google_maps, instagram, twitter, tiktok, reddit. requires generate scope.", {
|
|
16
|
+
keywords: z
|
|
17
|
+
.array(z.string().min(1))
|
|
18
|
+
.min(1)
|
|
19
|
+
.describe("non-empty array of search keywords (e.g. ['boutique hotel', 'rooftop venue'])"),
|
|
20
|
+
location: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("location for geo-scoped goals (local_business / venue / brick-and-mortar). omit for global goals (creators, b2b, saas)."),
|
|
24
|
+
radius_miles: z.number().positive().optional().default(25).describe("search radius in miles (default 25)"),
|
|
25
|
+
max_results: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.min(10)
|
|
29
|
+
.max(500)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("max contacts to return (10-500, default 50; server clamps to plan cap)"),
|
|
32
|
+
sources: z
|
|
33
|
+
.array(z.enum(DISCOVERY_SOURCES))
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("override the default per-profile_type source mix. valid: google_maps, instagram, twitter, tiktok, reddit. omit to let the orchestrator pick."),
|
|
36
|
+
profile_type: z
|
|
37
|
+
.enum(PROFILE_TYPES)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("ICP profile type — drives default source mix when sources is omitted"),
|
|
40
|
+
campaign_id: z
|
|
41
|
+
.string()
|
|
42
|
+
.uuid()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("campaign to attach discovered contacts to on finalize"),
|
|
45
|
+
enrich_emails: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.optional()
|
|
48
|
+
.default(true)
|
|
49
|
+
.describe("run waterfall email enrichment (BetterContact → FullEnrich → ZeroBounce). default true."),
|
|
9
50
|
}, async (args) => {
|
|
10
51
|
try {
|
|
11
52
|
const result = await client.post("/discovery/search", args);
|
|
@@ -18,7 +59,7 @@ export function registerDiscoveryTools(server, client) {
|
|
|
18
59
|
};
|
|
19
60
|
}
|
|
20
61
|
});
|
|
21
|
-
server.tool("search_campaign_contacts", "search for contacts matching a campaign's target profiles. requires
|
|
62
|
+
server.tool("search_campaign_contacts", "search for contacts matching a campaign's target profiles. requires generate scope.", {
|
|
22
63
|
campaign_id: z.string().uuid().describe("campaign ID"),
|
|
23
64
|
target_profile_id: z.string().uuid().optional().describe("specific target profile to search for"),
|
|
24
65
|
}, async (args) => {
|
|
@@ -33,7 +74,7 @@ export function registerDiscoveryTools(server, client) {
|
|
|
33
74
|
};
|
|
34
75
|
}
|
|
35
76
|
});
|
|
36
|
-
server.tool("get_discovery_job", "get the status and
|
|
77
|
+
server.tool("get_discovery_job", "get the status, progress, and contacts of a discovery job. status values: pending, running, completed, failed. contacts populated only when status=completed. requires read scope.", {
|
|
37
78
|
id: z.string().uuid().describe("discovery job ID"),
|
|
38
79
|
}, async ({ id }) => {
|
|
39
80
|
try {
|
|
@@ -47,7 +88,7 @@ export function registerDiscoveryTools(server, client) {
|
|
|
47
88
|
};
|
|
48
89
|
}
|
|
49
90
|
});
|
|
50
|
-
server.tool("list_discovery_sources", "list available discovery data sources and their status. requires
|
|
91
|
+
server.tool("list_discovery_sources", "list available discovery data sources and their status. requires read scope.", {}, async () => {
|
|
51
92
|
try {
|
|
52
93
|
const result = await client.get("/discovery/sources");
|
|
53
94
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
package/dist/tools/keys.js
CHANGED
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
const SCOPES = [
|
|
3
|
-
"campaigns:read",
|
|
4
|
-
"campaigns:write",
|
|
5
|
-
"contacts:read",
|
|
6
|
-
"contacts:write",
|
|
7
|
-
"conversations:read",
|
|
8
|
-
"conversations:write",
|
|
9
|
-
"messages:read",
|
|
10
|
-
"messages:write",
|
|
11
|
-
"ai:read",
|
|
12
|
-
"ai:write",
|
|
13
|
-
"discovery:read",
|
|
14
|
-
"discovery:write",
|
|
15
|
-
"analytics:read",
|
|
16
|
-
"billing:read",
|
|
17
|
-
"channels:read",
|
|
18
|
-
"channels:write",
|
|
19
|
-
"notifications:read",
|
|
20
|
-
"notifications:write",
|
|
21
|
-
"webhooks:read",
|
|
22
|
-
"webhooks:write",
|
|
23
|
-
"keys:read",
|
|
24
|
-
"keys:write",
|
|
25
|
-
];
|
|
2
|
+
const SCOPES = ["read", "write", "delete", "generate", "send", "admin"];
|
|
26
3
|
export function registerKeyTools(server, client) {
|
|
27
|
-
server.tool("get_current_api_key", "get details about the currently authenticated API key.
|
|
4
|
+
server.tool("get_current_api_key", "get details about the currently authenticated API key.", {}, async () => {
|
|
28
5
|
try {
|
|
29
6
|
const result = await client.get("/keys/me");
|
|
30
7
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -36,7 +13,7 @@ export function registerKeyTools(server, client) {
|
|
|
36
13
|
};
|
|
37
14
|
}
|
|
38
15
|
});
|
|
39
|
-
server.tool("list_api_keys", "list all API keys for the account. requires
|
|
16
|
+
server.tool("list_api_keys", "list all API keys for the account. requires admin scope.", {
|
|
40
17
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
41
18
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
42
19
|
}, async ({ page, page_size }) => {
|
|
@@ -56,12 +33,12 @@ export function registerKeyTools(server, client) {
|
|
|
56
33
|
};
|
|
57
34
|
}
|
|
58
35
|
});
|
|
59
|
-
server.tool("create_api_key", "create a new API key. requires
|
|
36
|
+
server.tool("create_api_key", "create a new API key. requires admin scope.", {
|
|
60
37
|
name: z.string().min(1).max(100).describe("key name for identification"),
|
|
61
38
|
scopes: z
|
|
62
39
|
.array(z.enum(SCOPES))
|
|
63
40
|
.optional()
|
|
64
|
-
.describe('permission scopes (
|
|
41
|
+
.describe('permission scopes (any of: read, write, delete, generate, send, admin). defaults to all scopes'),
|
|
65
42
|
expires_at: z.string().optional().describe("expiration date in ISO 8601 format"),
|
|
66
43
|
}, async (args) => {
|
|
67
44
|
try {
|
|
@@ -75,7 +52,7 @@ export function registerKeyTools(server, client) {
|
|
|
75
52
|
};
|
|
76
53
|
}
|
|
77
54
|
});
|
|
78
|
-
server.tool("revoke_api_key", "revoke (delete) an API key. requires
|
|
55
|
+
server.tool("revoke_api_key", "revoke (delete) an API key. requires admin scope.", {
|
|
79
56
|
id: z.string().uuid().describe("API key ID to revoke"),
|
|
80
57
|
}, async ({ id }) => {
|
|
81
58
|
try {
|
package/dist/tools/messages.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerMessageTools(server, client) {
|
|
3
|
-
server.tool("list_pending_messages", "list messages awaiting approval before sending. requires
|
|
3
|
+
server.tool("list_pending_messages", "list messages awaiting approval before sending. requires read scope.", {
|
|
4
4
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
5
5
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
6
6
|
campaign_id: z.string().uuid().optional().describe("filter by campaign ID"),
|
|
@@ -23,7 +23,7 @@ export function registerMessageTools(server, client) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
|
-
server.tool("approve_message", "approve a pending message for sending. requires
|
|
26
|
+
server.tool("approve_message", "approve a pending message for sending. requires send scope.", {
|
|
27
27
|
id: z.string().uuid().describe("message ID"),
|
|
28
28
|
}, async ({ id }) => {
|
|
29
29
|
try {
|
|
@@ -37,7 +37,7 @@ export function registerMessageTools(server, client) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
|
-
server.tool("reject_message", "reject a pending message with an optional reason. requires
|
|
40
|
+
server.tool("reject_message", "reject a pending message with an optional reason. requires send scope.", {
|
|
41
41
|
id: z.string().uuid().describe("message ID"),
|
|
42
42
|
reason: z.string().max(500).optional().describe("reason for rejection"),
|
|
43
43
|
}, async ({ id, reason }) => {
|
|
@@ -52,7 +52,7 @@ export function registerMessageTools(server, client) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
-
server.tool("retry_message", "retry sending a failed message. requires
|
|
55
|
+
server.tool("retry_message", "retry sending a failed message. requires send scope.", {
|
|
56
56
|
id: z.string().uuid().describe("message ID"),
|
|
57
57
|
}, async ({ id }) => {
|
|
58
58
|
try {
|
|
@@ -66,7 +66,7 @@ export function registerMessageTools(server, client) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
|
-
server.tool("submit_message_feedback", "submit feedback on an AI-generated message to improve future generations. requires
|
|
69
|
+
server.tool("submit_message_feedback", "submit feedback on an AI-generated message to improve future generations. requires send scope.", {
|
|
70
70
|
id: z.string().uuid().describe("message ID"),
|
|
71
71
|
feedback: z.string().min(1).max(2000).describe("feedback text"),
|
|
72
72
|
}, async ({ id, feedback }) => {
|
|
@@ -81,7 +81,7 @@ export function registerMessageTools(server, client) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
|
-
server.tool("bulk_message_action", "approve or reject multiple messages at once. requires
|
|
84
|
+
server.tool("bulk_message_action", "approve or reject multiple messages at once. requires send scope.", {
|
|
85
85
|
action: z.enum(["approve", "reject"]).describe("action to perform"),
|
|
86
86
|
ids: z.array(z.string().uuid()).min(1).max(100).describe("message IDs to act on"),
|
|
87
87
|
}, async (args) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerNotificationTools(server, client) {
|
|
3
|
-
server.tool("list_notifications", "list user notifications. requires
|
|
3
|
+
server.tool("list_notifications", "list user notifications. requires read scope.", {
|
|
4
4
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
5
5
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
6
6
|
}, async ({ page, page_size }) => {
|
|
@@ -20,7 +20,7 @@ export function registerNotificationTools(server, client) {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
|
-
server.tool("update_notification", "update a notification (e.g., mark as read). requires
|
|
23
|
+
server.tool("update_notification", "update a notification (e.g., mark as read). requires write scope.", {
|
|
24
24
|
id: z.string().uuid().describe("notification ID"),
|
|
25
25
|
read: z.boolean().optional().describe("mark as read or unread"),
|
|
26
26
|
}, async ({ id, ...body }) => {
|
|
@@ -35,7 +35,7 @@ export function registerNotificationTools(server, client) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
-
server.tool("delete_notification", "delete a notification. requires
|
|
38
|
+
server.tool("delete_notification", "delete a notification. requires write scope.", {
|
|
39
39
|
id: z.string().uuid().describe("notification ID"),
|
|
40
40
|
}, async ({ id }) => {
|
|
41
41
|
try {
|
|
@@ -49,7 +49,7 @@ export function registerNotificationTools(server, client) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
server.tool("mark_all_notifications_read", "mark all notifications as read. requires
|
|
52
|
+
server.tool("mark_all_notifications_read", "mark all notifications as read. requires write scope.", {}, async () => {
|
|
53
53
|
try {
|
|
54
54
|
await client.post("/notifications/read-all");
|
|
55
55
|
return { content: [{ type: "text", text: "all notifications marked as read" }] };
|
package/dist/tools/webhooks.js
CHANGED
|
@@ -20,7 +20,7 @@ const WEBHOOK_EVENTS = [
|
|
|
20
20
|
"discovery.failed",
|
|
21
21
|
];
|
|
22
22
|
export function registerWebhookTools(server, client) {
|
|
23
|
-
server.tool("list_webhooks", "list all configured webhooks. requires
|
|
23
|
+
server.tool("list_webhooks", "list all configured webhooks. requires admin scope.", {
|
|
24
24
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
25
25
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
26
26
|
}, async ({ page, page_size }) => {
|
|
@@ -40,7 +40,7 @@ export function registerWebhookTools(server, client) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
-
server.tool("create_webhook", "create a new webhook endpoint. requires
|
|
43
|
+
server.tool("create_webhook", "create a new webhook endpoint. requires admin scope.", {
|
|
44
44
|
url: z
|
|
45
45
|
.string()
|
|
46
46
|
.url()
|
|
@@ -60,7 +60,7 @@ export function registerWebhookTools(server, client) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
|
-
server.tool("get_webhook", "get a webhook by ID. requires
|
|
63
|
+
server.tool("get_webhook", "get a webhook by ID. requires admin scope.", {
|
|
64
64
|
id: z.string().uuid().describe("webhook ID"),
|
|
65
65
|
}, async ({ id }) => {
|
|
66
66
|
try {
|
|
@@ -74,7 +74,7 @@ export function registerWebhookTools(server, client) {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
|
-
server.tool("update_webhook", "update a webhook's configuration. requires
|
|
77
|
+
server.tool("update_webhook", "update a webhook's configuration. requires admin scope.", {
|
|
78
78
|
id: z.string().uuid().describe("webhook ID"),
|
|
79
79
|
url: z
|
|
80
80
|
.string()
|
|
@@ -96,7 +96,7 @@ export function registerWebhookTools(server, client) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
|
-
server.tool("delete_webhook", "delete a webhook. requires
|
|
99
|
+
server.tool("delete_webhook", "delete a webhook. requires admin scope.", {
|
|
100
100
|
id: z.string().uuid().describe("webhook ID"),
|
|
101
101
|
}, async ({ id }) => {
|
|
102
102
|
try {
|
|
@@ -110,7 +110,7 @@ export function registerWebhookTools(server, client) {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
|
-
server.tool("test_webhook", "send a test event to a webhook endpoint to verify it's working. requires
|
|
113
|
+
server.tool("test_webhook", "send a test event to a webhook endpoint to verify it's working. requires admin scope.", {
|
|
114
114
|
id: z.string().uuid().describe("webhook ID"),
|
|
115
115
|
}, async ({ id }) => {
|
|
116
116
|
try {
|
|
@@ -124,7 +124,7 @@ export function registerWebhookTools(server, client) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
|
-
server.tool("rotate_webhook_secret", "rotate the signing secret for a webhook. the new secret is returned in the response. requires
|
|
127
|
+
server.tool("rotate_webhook_secret", "rotate the signing secret for a webhook. the new secret is returned in the response. requires admin scope.", {
|
|
128
128
|
id: z.string().uuid().describe("webhook ID"),
|
|
129
129
|
}, async ({ id }) => {
|
|
130
130
|
try {
|
|
@@ -138,7 +138,7 @@ export function registerWebhookTools(server, client) {
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
|
-
server.tool("list_webhook_deliveries", "list delivery history for a webhook. requires
|
|
141
|
+
server.tool("list_webhook_deliveries", "list delivery history for a webhook. requires admin scope.", {
|
|
142
142
|
id: z.string().uuid().describe("webhook ID"),
|
|
143
143
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
144
144
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|