@hailer/mcp 0.2.7 → 1.0.21

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.
Files changed (40) hide show
  1. package/.claude/skills/client-bot-architecture/skill.md +340 -0
  2. package/.claude/skills/publish-hailer-app/SKILL.md +11 -0
  3. package/dist/app.d.ts +1 -1
  4. package/dist/app.js +116 -84
  5. package/dist/bot/chat-bot.d.ts +31 -0
  6. package/dist/bot/chat-bot.js +356 -0
  7. package/dist/cli.d.ts +9 -1
  8. package/dist/cli.js +71 -2
  9. package/dist/config.d.ts +15 -2
  10. package/dist/config.js +53 -3
  11. package/dist/lib/logger.js +11 -11
  12. package/dist/mcp/hailer-clients.js +12 -11
  13. package/dist/mcp/tool-registry.d.ts +4 -0
  14. package/dist/mcp/tool-registry.js +78 -1
  15. package/dist/mcp/tools/activity.js +47 -0
  16. package/dist/mcp/tools/discussion.js +44 -1
  17. package/dist/mcp/tools/metrics.d.ts +13 -0
  18. package/dist/mcp/tools/metrics.js +546 -0
  19. package/dist/mcp/tools/user.d.ts +1 -0
  20. package/dist/mcp/tools/user.js +94 -1
  21. package/dist/mcp/tools/workflow.js +109 -40
  22. package/dist/mcp/webhook-handler.js +7 -4
  23. package/dist/mcp-server.js +22 -6
  24. package/dist/stdio-server.d.ts +14 -0
  25. package/dist/stdio-server.js +101 -0
  26. package/package.json +6 -6
  27. package/scripts/test-hal-tools.ts +154 -0
  28. package/test-billing-server.js +136 -0
  29. package/dist/lib/discussion-lock.d.ts +0 -42
  30. package/dist/lib/discussion-lock.js +0 -110
  31. package/dist/mcp/tools/bot-config/constants.d.ts +0 -23
  32. package/dist/mcp/tools/bot-config/constants.js +0 -94
  33. package/dist/mcp/tools/bot-config/core.d.ts +0 -253
  34. package/dist/mcp/tools/bot-config/core.js +0 -2456
  35. package/dist/mcp/tools/bot-config/index.d.ts +0 -10
  36. package/dist/mcp/tools/bot-config/index.js +0 -59
  37. package/dist/mcp/tools/bot-config/tools.d.ts +0 -7
  38. package/dist/mcp/tools/bot-config/tools.js +0 -15
  39. package/dist/mcp/tools/bot-config/types.d.ts +0 -50
  40. package/dist/mcp/tools/bot-config/types.js +0 -6
@@ -0,0 +1,546 @@
1
+ "use strict";
2
+ /**
3
+ * Victoria Metrics Tools - Query and List Metrics
4
+ *
5
+ * Tools for querying Hailer metrics from Victoria Metrics:
6
+ * - Query metrics with PromQL (READ)
7
+ * - List available metrics (READ)
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.searchUserForMetricsTool = exports.searchWorkspaceForMetricsTool = exports.listMetricsTool = exports.queryMetricTool = void 0;
14
+ const zod_1 = require("zod");
15
+ const tool_registry_1 = require("../tool-registry");
16
+ const index_1 = require("../utils/index");
17
+ const axios_1 = __importDefault(require("axios"));
18
+ const hailer_clients_1 = require("../hailer-clients");
19
+ const logger = (0, index_1.createLogger)({ component: 'metrics-tools' });
20
+ // ============================================================================
21
+ // CONFIGURATION
22
+ // ============================================================================
23
+ const METRICS_CONFIG = {
24
+ host: process.env.METRICS_QUERY_HOST || 'observer.hailer.com',
25
+ port: Number(process.env.METRICS_QUERY_PORT) || 443,
26
+ secure: process.env.METRICS_QUERY_SECURE !== 'false',
27
+ user: process.env.METRICS_QUERY_USER || 'metrics',
28
+ password: process.env.METRICS_QUERY_PASSWORD || 'verysecuremetricspassword42',
29
+ };
30
+ // Metrics Admin API credentials (for workspace/user search - requires whitelisted user)
31
+ const METRICS_ADMIN_CONFIG = {
32
+ email: process.env.METRICS_ADMIN_EMAIL || '',
33
+ password: process.env.METRICS_ADMIN_PASSWORD || '',
34
+ apiBaseUrl: process.env.METRICS_ADMIN_API_URL || 'https://api.hailer.com',
35
+ };
36
+ logger.info('Metrics admin config loaded', {
37
+ email: METRICS_ADMIN_CONFIG.email || '(not set)',
38
+ apiBaseUrl: METRICS_ADMIN_CONFIG.apiBaseUrl,
39
+ hasPassword: !!METRICS_ADMIN_CONFIG.password,
40
+ });
41
+ // ============================================================================
42
+ // HELPER FUNCTIONS
43
+ // ============================================================================
44
+ // Cached admin client for metrics operations
45
+ let metricsAdminClient = null;
46
+ let metricsAdminManager = null;
47
+ /**
48
+ * Get or create metrics admin client for privileged operations
49
+ */
50
+ async function getMetricsAdminClient() {
51
+ if (!METRICS_ADMIN_CONFIG.email || !METRICS_ADMIN_CONFIG.password) {
52
+ throw new Error('METRICS_ADMIN_EMAIL and METRICS_ADMIN_PASSWORD must be set for workspace/user search');
53
+ }
54
+ // Return cached client if connected
55
+ if (metricsAdminClient && metricsAdminManager?.isConnected()) {
56
+ return metricsAdminClient;
57
+ }
58
+ // Clean up stale connection
59
+ if (metricsAdminManager) {
60
+ metricsAdminManager.disconnect();
61
+ }
62
+ logger.info('Creating metrics admin connection', {
63
+ email: METRICS_ADMIN_CONFIG.email,
64
+ apiBaseUrl: METRICS_ADMIN_CONFIG.apiBaseUrl,
65
+ });
66
+ // Create new connection using WebSocket (same as regular clients)
67
+ metricsAdminManager = new hailer_clients_1.HailerClientManager(METRICS_ADMIN_CONFIG.apiBaseUrl, METRICS_ADMIN_CONFIG.email, METRICS_ADMIN_CONFIG.password);
68
+ metricsAdminClient = await metricsAdminManager.connect();
69
+ logger.info('Metrics admin connected successfully');
70
+ return metricsAdminClient;
71
+ }
72
+ /**
73
+ * Make admin API request with special credentials via WebSocket
74
+ */
75
+ async function metricsAdminRequest(method, params) {
76
+ const client = await getMetricsAdminClient();
77
+ return client.socket.request(method, params);
78
+ }
79
+ /**
80
+ * Get Victoria Metrics base URL
81
+ */
82
+ function getMetricsBaseUrl() {
83
+ const protocol = METRICS_CONFIG.secure ? 'https' : 'http';
84
+ const port = METRICS_CONFIG.port !== (METRICS_CONFIG.secure ? 443 : 80)
85
+ ? `:${METRICS_CONFIG.port}`
86
+ : '';
87
+ return `${protocol}://${METRICS_CONFIG.host}${port}`;
88
+ }
89
+ /**
90
+ * Get Basic Auth credentials
91
+ */
92
+ function getAuthHeader() {
93
+ return {
94
+ username: METRICS_CONFIG.user,
95
+ password: METRICS_CONFIG.password,
96
+ };
97
+ }
98
+ /**
99
+ * Parse time range string to PromQL duration
100
+ */
101
+ function parseTimeRange(timeRange) {
102
+ const validRanges = ['1h', '24h', '3d', '7d', '30d'];
103
+ if (!validRanges.includes(timeRange)) {
104
+ throw new Error(`Invalid time range "${timeRange}". Valid options: ${validRanges.join(', ')}`);
105
+ }
106
+ return timeRange;
107
+ }
108
+ /**
109
+ * Build PromQL query from parameters
110
+ */
111
+ function buildPromQLQuery(metric, timeRange, aggregation, groupBy, filters) {
112
+ // Build label filters
113
+ let labelFilters = '';
114
+ if (filters && Object.keys(filters).length > 0) {
115
+ const filterPairs = Object.entries(filters).map(([key, value]) => `${key}="${value}"`);
116
+ labelFilters = filterPairs.join(',');
117
+ }
118
+ // Build metric selector
119
+ const metricSelector = labelFilters
120
+ ? `${metric}{${labelFilters}}`
121
+ : metric;
122
+ // Build aggregation with groupBy
123
+ const groupByClause = groupBy && groupBy.length > 0
124
+ ? ` by (${groupBy.join(', ')})`
125
+ : '';
126
+ // Build final query
127
+ const query = `${aggregation}${groupByClause} (increase(${metricSelector}[${timeRange}]))`;
128
+ return query;
129
+ }
130
+ /**
131
+ * Format query results for display
132
+ */
133
+ function formatQueryResults(data) {
134
+ if (!data || !data.result || data.result.length === 0) {
135
+ return {
136
+ formatted: 'No data returned for this query.',
137
+ raw: data,
138
+ };
139
+ }
140
+ // Single value result
141
+ if (data.result.length === 1 && !data.result[0].metric) {
142
+ const value = data.result[0].value[1];
143
+ return {
144
+ formatted: `Result: ${value}`,
145
+ raw: { value: Number(value) },
146
+ };
147
+ }
148
+ // Multiple values or grouped results
149
+ const results = data.result.map((item) => {
150
+ const labels = item.metric || {};
151
+ const value = item.value[1];
152
+ return {
153
+ labels,
154
+ value: Number(value),
155
+ };
156
+ });
157
+ // Format as table
158
+ let formatted = `Found ${results.length} result(s):\n\n`;
159
+ for (const result of results) {
160
+ const labelStr = Object.entries(result.labels)
161
+ .map(([k, v]) => `${k}="${v}"`)
162
+ .join(', ');
163
+ formatted += `- ${labelStr ? `{${labelStr}}` : 'total'}: ${result.value}\n`;
164
+ }
165
+ return {
166
+ formatted,
167
+ raw: results,
168
+ };
169
+ }
170
+ // ============================================================================
171
+ // TOOL 1: QUERY METRIC
172
+ // ============================================================================
173
+ /**
174
+ * Generate ASCII bar chart from data
175
+ */
176
+ function generateAsciiChart(data, options = {}) {
177
+ const { maxWidth = 40, showPercent = true } = options;
178
+ if (data.length === 0)
179
+ return 'No data to display';
180
+ const maxValue = Math.max(...data.map(d => d.value));
181
+ const total = data.reduce((sum, d) => sum + d.value, 0);
182
+ // Find max label length for alignment
183
+ const maxLabelLen = Math.max(...data.map(d => d.label.length), 10);
184
+ const lines = data.map(item => {
185
+ const barLength = maxValue > 0 ? Math.round((item.value / maxValue) * maxWidth) : 0;
186
+ const bar = '█'.repeat(barLength);
187
+ const label = item.label.padEnd(maxLabelLen);
188
+ const valueStr = item.value.toLocaleString();
189
+ const percent = showPercent && total > 0
190
+ ? ` (${Math.round(item.value / total * 100)}%)`
191
+ : '';
192
+ return `${label} ${bar} ${valueStr}${percent}`;
193
+ });
194
+ return lines.join('\n');
195
+ }
196
+ /**
197
+ * Format grouped results as ASCII chart
198
+ */
199
+ function formatAsChart(data, groupByLabel) {
200
+ if (!data || !data.result || data.result.length === 0) {
201
+ return 'No data to display';
202
+ }
203
+ const chartData = data.result
204
+ .map((item) => ({
205
+ label: item.metric?.[groupByLabel] || 'unknown',
206
+ value: Math.round(parseFloat(item.value?.[1] || 0)),
207
+ }))
208
+ .filter((item) => item.value > 0)
209
+ .sort((a, b) => b.value - a.value)
210
+ .slice(0, 10); // Top 10
211
+ const total = chartData.reduce((sum, d) => sum + d.value, 0);
212
+ let output = generateAsciiChart(chartData);
213
+ output += `\n\nTotal: ${total.toLocaleString()}`;
214
+ return output;
215
+ }
216
+ // ============================================================================
217
+ // TOOL 1: QUERY METRIC
218
+ // ============================================================================
219
+ exports.queryMetricTool = {
220
+ name: 'query_metric',
221
+ group: tool_registry_1.ToolGroup.READ,
222
+ description: `Query metrics from Victoria Metrics using PromQL. Available metrics:
223
+ - hailer.activity.create - Activities created (labels: workspace, process, team, userRole)
224
+ - hailer.message.create - Messages sent (labels: workspace, discussion, userRole)
225
+ - hailer.file.upload - Files uploaded (labels: workspace)
226
+ - hailer.telemetry - UI events (labels: workspace, user, element)
227
+
228
+ Supports ASCII chart visualization for grouped data (format: "chart")`,
229
+ schema: zod_1.z.object({
230
+ metric: zod_1.z.string().describe('Metric name (e.g., "hailer.activity.create")'),
231
+ timeRange: zod_1.z.enum(['1h', '24h', '3d', '7d', '30d']).optional().default('24h').describe('Time range: 1h, 24h, 3d, 7d, 30d'),
232
+ aggregation: zod_1.z.enum(['sum', 'avg', 'max', 'min', 'count']).optional().default('sum').describe('Aggregation function'),
233
+ groupBy: zod_1.z.preprocess((val) => {
234
+ if (typeof val === 'string') {
235
+ try {
236
+ return JSON.parse(val);
237
+ }
238
+ catch {
239
+ return val.split(',').map(s => s.trim()).filter(Boolean);
240
+ }
241
+ }
242
+ return val;
243
+ }, zod_1.z.array(zod_1.z.string()).optional()).describe('Group by labels like ["workspace", "process"] (optional)'),
244
+ filters: zod_1.z.preprocess((val) => typeof val === 'string' ? JSON.parse(val) : val, zod_1.z.record(zod_1.z.string()).optional()).describe('Label filters like { "workspace": "abc123", "userRole": "admin" } (optional)'),
245
+ format: zod_1.z.enum(["json", "chart"]).optional().default("json").describe("Output format: 'json' for raw data, 'chart' for ASCII bar chart"),
246
+ }),
247
+ async execute(args, _context) {
248
+ try {
249
+ logger.info('Querying metric', {
250
+ metric: args.metric,
251
+ timeRange: args.timeRange,
252
+ aggregation: args.aggregation,
253
+ groupBy: args.groupBy,
254
+ filters: args.filters,
255
+ });
256
+ // Parse and validate time range
257
+ const timeRange = parseTimeRange(args.timeRange || '24h');
258
+ // Build PromQL query
259
+ const promqlQuery = buildPromQLQuery(args.metric, timeRange, args.aggregation || 'sum', args.groupBy, args.filters);
260
+ logger.debug('Built PromQL query', { query: promqlQuery });
261
+ // Execute query
262
+ const baseUrl = getMetricsBaseUrl();
263
+ const url = `${baseUrl}/api/v1/query`;
264
+ const response = await axios_1.default.get(url, {
265
+ params: { query: promqlQuery },
266
+ auth: getAuthHeader(),
267
+ timeout: 10000,
268
+ });
269
+ if (response.data.status !== 'success') {
270
+ throw new Error(`Query failed: ${response.data.error || 'Unknown error'}`);
271
+ }
272
+ // Format results
273
+ const { formatted, raw } = formatQueryResults(response.data.data);
274
+ // Format output based on requested format
275
+ const format = args.format || 'json';
276
+ let responseText;
277
+ if (format === 'chart' && args.groupBy && args.groupBy.length > 0) {
278
+ // ASCII chart for grouped data
279
+ const chartTitle = `📊 ${args.metric} by ${args.groupBy[0]} (${args.timeRange})\n${'─'.repeat(50)}\n`;
280
+ responseText = chartTitle + formatAsChart(response.data.data, args.groupBy[0]);
281
+ }
282
+ else {
283
+ // JSON format (default)
284
+ responseText = `📊 **Metric Query Results**
285
+
286
+ **Query:** \`${promqlQuery}\`
287
+ **Time Range:** ${args.timeRange}
288
+ **Aggregation:** ${args.aggregation}
289
+
290
+ ${formatted}
291
+
292
+ **Raw Data:**
293
+ \`\`\`json
294
+ ${JSON.stringify(raw, null, 2)}
295
+ \`\`\``;
296
+ }
297
+ return {
298
+ content: [
299
+ {
300
+ type: 'text',
301
+ text: responseText,
302
+ },
303
+ ],
304
+ };
305
+ }
306
+ catch (error) {
307
+ logger.error('Failed to query metric', error, {
308
+ metric: args.metric,
309
+ timeRange: args.timeRange,
310
+ });
311
+ const errorMessage = error instanceof Error
312
+ ? error.message
313
+ : String(error);
314
+ return {
315
+ content: [
316
+ {
317
+ type: 'text',
318
+ text: `❌ Failed to query metric: ${errorMessage}\n\n💡 **Troubleshooting:**\n- Verify the metric name is correct (use list_metrics to see available metrics)\n- Check time range is valid (1h, 24h, 3d, 7d, 30d)\n- Ensure filters match existing labels\n- Verify Victoria Metrics connection settings`,
319
+ },
320
+ ],
321
+ };
322
+ }
323
+ },
324
+ };
325
+ // ============================================================================
326
+ // TOOL 2: LIST METRICS
327
+ // ============================================================================
328
+ exports.listMetricsTool = {
329
+ name: 'list_metrics',
330
+ group: tool_registry_1.ToolGroup.READ,
331
+ description: 'List all available Hailer metrics from Victoria Metrics',
332
+ schema: zod_1.z.object({}),
333
+ async execute(_args, _context) {
334
+ try {
335
+ logger.info('Listing available metrics');
336
+ // Query Victoria Metrics for all metric names
337
+ const baseUrl = getMetricsBaseUrl();
338
+ const url = `${baseUrl}/api/v1/label/__name__/values`;
339
+ const response = await axios_1.default.get(url, {
340
+ auth: getAuthHeader(),
341
+ timeout: 10000,
342
+ });
343
+ if (response.data.status !== 'success') {
344
+ throw new Error(`Query failed: ${response.data.error || 'Unknown error'}`);
345
+ }
346
+ const metrics = response.data.data || [];
347
+ // Filter for Hailer metrics only
348
+ const hailerMetrics = metrics.filter((m) => m.startsWith('hailer.'));
349
+ // Group metrics by category
350
+ const categorized = {
351
+ activity: [],
352
+ message: [],
353
+ file: [],
354
+ telemetry: [],
355
+ other: [],
356
+ };
357
+ for (const metric of hailerMetrics) {
358
+ if (metric.includes('activity')) {
359
+ categorized.activity.push(metric);
360
+ }
361
+ else if (metric.includes('message')) {
362
+ categorized.message.push(metric);
363
+ }
364
+ else if (metric.includes('file')) {
365
+ categorized.file.push(metric);
366
+ }
367
+ else if (metric.includes('telemetry')) {
368
+ categorized.telemetry.push(metric);
369
+ }
370
+ else {
371
+ categorized.other.push(metric);
372
+ }
373
+ }
374
+ // Format output
375
+ let formatted = `📊 **Available Hailer Metrics** (${hailerMetrics.length} total)\n\n`;
376
+ if (categorized.activity.length > 0) {
377
+ formatted += `**Activity Metrics:**\n`;
378
+ for (const metric of categorized.activity) {
379
+ formatted += `- \`${metric}\` - Activities created/updated\n`;
380
+ }
381
+ formatted += '\n';
382
+ }
383
+ if (categorized.message.length > 0) {
384
+ formatted += `**Message Metrics:**\n`;
385
+ for (const metric of categorized.message) {
386
+ formatted += `- \`${metric}\` - Messages and discussions\n`;
387
+ }
388
+ formatted += '\n';
389
+ }
390
+ if (categorized.file.length > 0) {
391
+ formatted += `**File Metrics:**\n`;
392
+ for (const metric of categorized.file) {
393
+ formatted += `- \`${metric}\` - File uploads and operations\n`;
394
+ }
395
+ formatted += '\n';
396
+ }
397
+ if (categorized.telemetry.length > 0) {
398
+ formatted += `**Telemetry Metrics:**\n`;
399
+ for (const metric of categorized.telemetry) {
400
+ formatted += `- \`${metric}\` - UI events and user interactions\n`;
401
+ }
402
+ formatted += '\n';
403
+ }
404
+ if (categorized.other.length > 0) {
405
+ formatted += `**Other Metrics:**\n`;
406
+ for (const metric of categorized.other) {
407
+ formatted += `- \`${metric}\`\n`;
408
+ }
409
+ formatted += '\n';
410
+ }
411
+ formatted += `\n💡 **Usage:**\nUse \`query_metric\` with any of these metric names to query data.\n\n`;
412
+ formatted += `**Common Labels:**\n- workspace: Workspace ID\n- process: Workflow ID\n- team: Team ID\n- userRole: User role (admin, member, etc.)\n- discussion: Discussion ID\n- user: User ID\n- element: UI element name`;
413
+ return {
414
+ content: [
415
+ {
416
+ type: 'text',
417
+ text: formatted,
418
+ },
419
+ ],
420
+ };
421
+ }
422
+ catch (error) {
423
+ logger.error('Failed to list metrics', error);
424
+ const errorMessage = error instanceof Error
425
+ ? error.message
426
+ : String(error);
427
+ return {
428
+ content: [
429
+ {
430
+ type: 'text',
431
+ text: `❌ Failed to list metrics: ${errorMessage}\n\n💡 **Troubleshooting:**\n- Verify Victoria Metrics connection settings\n- Check network connectivity to ${METRICS_CONFIG.host}\n- Ensure credentials are correct`,
432
+ },
433
+ ],
434
+ };
435
+ }
436
+ },
437
+ };
438
+ // ============================================================================
439
+ // TOOL 3: SEARCH WORKSPACE FOR METRICS
440
+ // ============================================================================
441
+ exports.searchWorkspaceForMetricsTool = {
442
+ name: 'search_workspace_for_metrics',
443
+ group: tool_registry_1.ToolGroup.READ,
444
+ description: `Search for workspaces by name to get their IDs for metric queries.
445
+ Use this tool when user mentions a workspace by name (e.g. "Sales Team") to find its ID for filtering metrics.`,
446
+ schema: zod_1.z.object({
447
+ name: zod_1.z.string().min(3).describe("Workspace name to search (min 3 characters)"),
448
+ limit: zod_1.z.number().min(1).max(100).optional().default(20).describe("Maximum results to return (default 20, max 100)"),
449
+ }),
450
+ async execute(args, _context) {
451
+ try {
452
+ logger.info('Searching workspace for metrics', { name: args.name });
453
+ // Use admin credentials for this privileged operation
454
+ const result = await metricsAdminRequest('v3.metrics.workspaceSearch', [{ name: args.name }]);
455
+ if (!result || result.length === 0) {
456
+ return {
457
+ content: [{
458
+ type: 'text',
459
+ text: `No workspaces found matching "${args.name}"`,
460
+ }],
461
+ };
462
+ }
463
+ const totalCount = result.length;
464
+ const limitedResults = result.slice(0, args.limit || 20);
465
+ const workspaceList = limitedResults
466
+ .map((ws) => `- ${ws.name} (ID: ${ws._id})`)
467
+ .join('\n');
468
+ const truncationNote = totalCount > limitedResults.length
469
+ ? `\n\n⚠️ Showing ${limitedResults.length} of ${totalCount} results. Use a more specific search term or increase limit.`
470
+ : '';
471
+ return {
472
+ content: [{
473
+ type: 'text',
474
+ text: `Found ${totalCount} workspace(s)${totalCount > limitedResults.length ? ` (showing first ${limitedResults.length})` : ''}:\n${workspaceList}${truncationNote}\n\n💡 Use the ID in metric queries with filters: { "workspace": "ID" }`,
475
+ }],
476
+ };
477
+ }
478
+ catch (error) {
479
+ logger.error('Failed to search workspaces for metrics', error);
480
+ const errorMessage = error instanceof Error ? error.message : String(error);
481
+ return {
482
+ content: [{
483
+ type: 'text',
484
+ text: `❌ Error searching workspaces: ${errorMessage}`,
485
+ }],
486
+ };
487
+ }
488
+ },
489
+ };
490
+ // ============================================================================
491
+ // TOOL 4: SEARCH USER FOR METRICS
492
+ // ============================================================================
493
+ exports.searchUserForMetricsTool = {
494
+ name: 'search_user_for_metrics',
495
+ group: tool_registry_1.ToolGroup.READ,
496
+ description: `Look up user details by ID for metric analysis.
497
+ Use this after getting user IDs from grouped metric results to see who they are.`,
498
+ schema: zod_1.z.object({
499
+ userIds: zod_1.z.preprocess((val) => {
500
+ if (typeof val === 'string') {
501
+ try {
502
+ return JSON.parse(val);
503
+ }
504
+ catch {
505
+ return val.split(',').map(s => s.trim()).filter(Boolean);
506
+ }
507
+ }
508
+ return val;
509
+ }, zod_1.z.array(zod_1.z.string())).describe("Array of user IDs to look up"),
510
+ }),
511
+ async execute(args, _context) {
512
+ try {
513
+ logger.info('Looking up users for metrics', { userIds: args.userIds });
514
+ // Use admin credentials for this privileged operation
515
+ const result = await metricsAdminRequest('v3.metrics.user.list', [{ userIds: args.userIds }]);
516
+ if (!result || result.length === 0) {
517
+ return {
518
+ content: [{
519
+ type: 'text',
520
+ text: `No users found for provided IDs`,
521
+ }],
522
+ };
523
+ }
524
+ const userList = result
525
+ .map((user) => `- ${user.name || 'No name'} (${user.email || 'no email'}) - ID: ${user._id}`)
526
+ .join('\n');
527
+ return {
528
+ content: [{
529
+ type: 'text',
530
+ text: `Found ${result.length} user(s):\n${userList}`,
531
+ }],
532
+ };
533
+ }
534
+ catch (error) {
535
+ logger.error('Failed to look up users for metrics', error);
536
+ const errorMessage = error instanceof Error ? error.message : String(error);
537
+ return {
538
+ content: [{
539
+ type: 'text',
540
+ text: `❌ Error looking up users: ${errorMessage}`,
541
+ }],
542
+ };
543
+ }
544
+ },
545
+ };
546
+ //# sourceMappingURL=metrics.js.map
@@ -7,4 +7,5 @@
7
7
  */
8
8
  import { Tool } from '../tool-registry';
9
9
  export declare const searchWorkspaceUsersTool: Tool;
10
+ export declare const getWorkspaceBalanceTool: Tool;
10
11
  //# sourceMappingURL=user.d.ts.map
@@ -7,7 +7,7 @@
7
7
  * - Get initialization data (READ) - currently disabled
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.searchWorkspaceUsersTool = void 0;
10
+ exports.getWorkspaceBalanceTool = exports.searchWorkspaceUsersTool = void 0;
11
11
  const zod_1 = require("zod");
12
12
  const tool_registry_1 = require("../tool-registry");
13
13
  const index_1 = require("../utils/index");
@@ -80,4 +80,97 @@ exports.searchWorkspaceUsersTool = {
80
80
  }
81
81
  },
82
82
  };
83
+ // ============================================================================
84
+ // TOOL 2: GET WORKSPACE BALANCE
85
+ // ============================================================================
86
+ const getWorkspaceBalanceDescription = `Check the current token balance for the workspace. Use this when users ask about their balance, credits, or usage limits.`;
87
+ exports.getWorkspaceBalanceTool = {
88
+ name: 'get_workspace_balance',
89
+ group: tool_registry_1.ToolGroup.READ,
90
+ description: getWorkspaceBalanceDescription,
91
+ schema: zod_1.z.object({
92
+ workspaceId: zod_1.z
93
+ .string()
94
+ .optional()
95
+ .describe("The workspace ID to check balance for. If not provided, uses the current workspace context."),
96
+ }),
97
+ async execute(args, context) {
98
+ logger.debug('Checking workspace balance', {
99
+ workspaceId: args.workspaceId,
100
+ apiKey: context.apiKey.substring(0, 8) + '...'
101
+ });
102
+ try {
103
+ // Get workspace ID from args or default to current workspace
104
+ const workspaceId = (0, tool_helpers_1.getResolvedWorkspaceId)(args, context);
105
+ if (!workspaceId) {
106
+ return (0, tool_helpers_1.missingWorkspaceCacheResponse)();
107
+ }
108
+ // Call the Hailer API to get token balance
109
+ const result = await context.hailer.request('v3.mcp.getTokenBalance', [workspaceId]);
110
+ // Parse the balance from response
111
+ const balance = typeof result?.balance === "number" ? result.balance :
112
+ typeof result === "number" ? result : 0;
113
+ // Determine status
114
+ let status;
115
+ let hasBalance;
116
+ if (balance < 0) {
117
+ status = 'NEGATIVE';
118
+ hasBalance = false;
119
+ }
120
+ else if (balance === 0) {
121
+ status = 'EMPTY';
122
+ hasBalance = false;
123
+ }
124
+ else if (balance < 100) {
125
+ status = 'LOW';
126
+ hasBalance = true;
127
+ }
128
+ else {
129
+ status = 'OK';
130
+ hasBalance = true;
131
+ }
132
+ // Format the balance with commas for readability
133
+ const formattedBalance = balance.toLocaleString();
134
+ // Build response message
135
+ let message;
136
+ if (status === 'OK') {
137
+ message = `Workspace has ${formattedBalance} tokens available`;
138
+ }
139
+ else if (status === 'LOW') {
140
+ message = `Workspace balance is low: ${formattedBalance} tokens remaining`;
141
+ }
142
+ else if (status === 'EMPTY') {
143
+ message = `Workspace has no tokens available`;
144
+ }
145
+ else {
146
+ message = `Workspace balance is negative: ${formattedBalance} tokens`;
147
+ }
148
+ const response = {
149
+ balance,
150
+ status,
151
+ hasBalance,
152
+ message,
153
+ workspaceId,
154
+ };
155
+ logger.debug('Workspace balance retrieved', {
156
+ workspaceId,
157
+ balance,
158
+ status
159
+ });
160
+ // Return formatted response
161
+ const statusEmoji = status === 'OK' ? '✅' :
162
+ status === 'LOW' ? '⚠️' :
163
+ '❌';
164
+ return (0, index_1.textResponse)(`${statusEmoji} **Workspace Balance**\n\n` +
165
+ `- Balance: **${formattedBalance}** tokens\n` +
166
+ `- Status: ${status}\n` +
167
+ `- Has sufficient balance: ${hasBalance ? 'Yes' : 'No'}\n\n` +
168
+ `${message}`);
169
+ }
170
+ catch (error) {
171
+ logger.error("Error checking workspace balance", error);
172
+ return (0, index_1.errorResponse)(`Error checking workspace balance: ${(0, index_1.getErrorMessage)(error)}`);
173
+ }
174
+ },
175
+ };
83
176
  //# sourceMappingURL=user.js.map