@moda-ai/cli 1.0.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/.env.example ADDED
@@ -0,0 +1,8 @@
1
+ # Moda API Key (required)
2
+ # Get this from https://app.modaflows.com/settings
3
+ MODA_API_KEY=moda_sk_...
4
+
5
+ # Moda Backend URL (optional)
6
+ # Defaults to production: https://modaflows.com
7
+ # For local development, override with:
8
+ # MODA_BASE_URL=http://localhost:3001
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # moda-cli
2
+
3
+ CLI and MCP server for [Moda](https://modaflows.com) -- AI agent analytics and observability.
4
+
5
+ Query your conversation analytics from the terminal or connect AI assistants (Claude Code, Cursor, etc.) to your Moda data via the Model Context Protocol.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g moda-cli
11
+ ```
12
+
13
+ Or use without installing:
14
+
15
+ ```bash
16
+ npx moda-cli overview
17
+ ```
18
+
19
+ ## Setup
20
+
21
+ Get your API key from [modaflows.com/settings](https://modaflows.com/settings), then:
22
+
23
+ ```bash
24
+ export MODA_API_KEY="moda_sk_..."
25
+ ```
26
+
27
+ ## CLI Usage
28
+
29
+ ```bash
30
+ moda overview # Dashboard overview
31
+ moda overview --days-back=30 # Last 30 days
32
+
33
+ moda clusters # Browse topic clusters
34
+ moda clusters --parent-id=node-abc # Drill into a category
35
+
36
+ moda cluster-conversations <node_id> # Conversations in a cluster
37
+
38
+ moda conversations --search="error" # Search conversations
39
+ moda conversations --time-range=24h # Filter by time
40
+ moda conversations --environment=production # Filter by environment
41
+
42
+ moda context <conversation_id> # Get conversation context
43
+ moda context <conversation_id> --msg-index=5 # Center on message 5
44
+
45
+ moda frustrations # User frustration detections
46
+ moda frustrations --days-back=14 --limit=20
47
+
48
+ moda tool-failures # Tool failure overview
49
+ moda tool-failure-detail <tool_name> # Per-tool failure detail
50
+ ```
51
+
52
+ All commands output JSON. Pipe to `jq` for filtering:
53
+
54
+ ```bash
55
+ moda overview | jq '.frustrations'
56
+ moda frustrations | jq '.frustrations[].primary_cause'
57
+ ```
58
+
59
+ ## MCP Server
60
+
61
+ ### Claude Code
62
+
63
+ ```bash
64
+ claude mcp add moda -- npx moda-cli
65
+ ```
66
+
67
+ Or add manually to your MCP config:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "moda": {
73
+ "command": "npx",
74
+ "args": ["-y", "moda-cli"],
75
+ "env": {
76
+ "MODA_API_KEY": "moda_sk_your_key_here"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### Other MCP Clients (Cursor, etc.)
84
+
85
+ ```json
86
+ {
87
+ "command": "npx",
88
+ "args": ["-y", "moda-cli"],
89
+ "env": {
90
+ "MODA_API_KEY": "moda_sk_your_key_here"
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Available MCP Tools
96
+
97
+ | Tool | Description |
98
+ |------|-------------|
99
+ | `moda_get_overview` | Dashboard overview with KPIs, top clusters, recent activity |
100
+ | `moda_get_clusters` | Browse topic cluster hierarchy |
101
+ | `moda_get_cluster_conversations` | List conversations in a specific cluster |
102
+ | `moda_search_conversations` | Search and filter conversations |
103
+ | `moda_get_conversation_context` | Get windowed conversation context (max 5 messages) |
104
+ | `moda_get_frustrations` | User frustration detections with evidence |
105
+ | `moda_get_tool_failures` | Tool failure overview |
106
+ | `moda_get_tool_failure_detail` | Per-tool failure detail with examples |
107
+
108
+ ## Environment Variables
109
+
110
+ | Variable | Required | Default | Description |
111
+ |----------|----------|---------|-------------|
112
+ | `MODA_API_KEY` | Yes | -- | Your Moda API key (`moda_sk_...`) |
113
+ | `MODA_BASE_URL` | No | `https://modaflows.com` | Base URL for the Data API |
114
+
115
+ ## Documentation
116
+
117
+ - [CLI Reference](https://docs.modaflows.com/data-api/cli)
118
+ - [MCP Server Setup](https://docs.modaflows.com/data-api/mcp)
119
+ - [Data API Overview](https://docs.modaflows.com/data-api/overview)
120
+ - [Quickstart](https://docs.modaflows.com/quickstart)
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,7 @@
1
+ export declare class ApiError extends Error {
2
+ readonly statusCode: number;
3
+ readonly responseBody: string;
4
+ constructor(message: string, statusCode: number, responseBody: string);
5
+ }
6
+ export declare function callDataAPI(endpoint: string): Promise<unknown>;
7
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1,67 @@
1
+ // src/api-client.ts
2
+ import { MODA_API_KEY, MODA_BASE_URL } from './config.js';
3
+ const REQUEST_TIMEOUT_MS = 30_000;
4
+ const MAX_RETRIES = 3;
5
+ const INITIAL_BACKOFF_MS = 500;
6
+ export class ApiError extends Error {
7
+ statusCode;
8
+ responseBody;
9
+ constructor(message, statusCode, responseBody) {
10
+ super(message);
11
+ this.statusCode = statusCode;
12
+ this.responseBody = responseBody;
13
+ this.name = 'ApiError';
14
+ }
15
+ }
16
+ function isRetryable(status) {
17
+ return status === 429 || status >= 500;
18
+ }
19
+ export async function callDataAPI(endpoint) {
20
+ const url = `${MODA_BASE_URL}/api/v1/data${endpoint}`;
21
+ let lastError;
22
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
23
+ if (attempt > 0) {
24
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
25
+ await new Promise((resolve) => setTimeout(resolve, backoff));
26
+ }
27
+ const controller = new AbortController();
28
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
29
+ try {
30
+ const response = await fetch(url, {
31
+ headers: {
32
+ 'x-api-key': MODA_API_KEY,
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ signal: controller.signal,
36
+ });
37
+ if (!response.ok) {
38
+ const errorText = await response.text();
39
+ const error = new ApiError(`API error (${response.status}): ${errorText}`, response.status, errorText);
40
+ if (isRetryable(response.status) && attempt < MAX_RETRIES) {
41
+ lastError = error;
42
+ continue;
43
+ }
44
+ throw error;
45
+ }
46
+ return response.json();
47
+ }
48
+ catch (error) {
49
+ if (error instanceof ApiError)
50
+ throw error;
51
+ const isAbort = error instanceof DOMException && error.name === 'AbortError';
52
+ const wrapped = new Error(isAbort
53
+ ? `Request timed out after ${REQUEST_TIMEOUT_MS}ms: ${url}`
54
+ : `Network error: ${error instanceof Error ? error.message : String(error)}`);
55
+ if (attempt < MAX_RETRIES) {
56
+ lastError = wrapped;
57
+ continue;
58
+ }
59
+ throw wrapped;
60
+ }
61
+ finally {
62
+ clearTimeout(timeout);
63
+ }
64
+ }
65
+ throw lastError ?? new Error('Request failed after retries');
66
+ }
67
+ //# sourceMappingURL=api-client.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ command: string;
4
+ positional: string | undefined;
5
+ flags: Record<string, string>;
6
+ }
7
+ export declare function parseArgs(argv: string[]): ParsedArgs;
8
+ export declare function flagsToArgs(flags: Record<string, string>, positionalKey?: string, positionalValue?: string): Record<string, unknown>;
9
+ export {};
10
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ import { z } from 'zod';
3
+ import { validateConfig, VERSION } from './config.js';
4
+ import { callDataAPI, ApiError } from './api-client.js';
5
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from './schemas.js';
6
+ export function parseArgs(argv) {
7
+ const args = argv.slice(2);
8
+ const command = args[0] || '';
9
+ const flags = {};
10
+ let positional;
11
+ let i = 1;
12
+ while (i < args.length) {
13
+ const arg = args[i];
14
+ if (arg.startsWith('--')) {
15
+ const eqIndex = arg.indexOf('=');
16
+ if (eqIndex !== -1) {
17
+ // --key=value
18
+ const key = arg.slice(2, eqIndex);
19
+ flags[key] = arg.slice(eqIndex + 1);
20
+ }
21
+ else {
22
+ // --key value
23
+ const key = arg.slice(2);
24
+ const next = args[i + 1];
25
+ if (next !== undefined && !next.startsWith('--')) {
26
+ flags[key] = next;
27
+ i++;
28
+ }
29
+ else {
30
+ flags[key] = 'true';
31
+ }
32
+ }
33
+ }
34
+ else if (positional === undefined) {
35
+ positional = arg;
36
+ }
37
+ i++;
38
+ }
39
+ return { command, positional, flags };
40
+ }
41
+ // Convert kebab-case flags to snake_case for Zod schema compatibility
42
+ export function flagsToArgs(flags, positionalKey, positionalValue) {
43
+ const result = {};
44
+ if (positionalKey && positionalValue) {
45
+ result[positionalKey] = positionalValue;
46
+ }
47
+ for (const [key, value] of Object.entries(flags)) {
48
+ const snakeKey = key.replace(/-/g, '_');
49
+ // Try to parse as number for numeric fields
50
+ const num = Number(value);
51
+ result[snakeKey] = isNaN(num) || value === 'true' ? value : num;
52
+ }
53
+ return result;
54
+ }
55
+ // --- Help ---
56
+ function printHelp() {
57
+ console.error(`Usage: moda <command> [options]
58
+
59
+ Commands:
60
+ overview Dashboard overview with KPIs
61
+ clusters Browse topic cluster hierarchy
62
+ cluster-conversations <node_id> List conversations in a cluster
63
+ conversations Search and filter conversations
64
+ context <conversation_id> Get windowed conversation context
65
+ frustrations Get user frustration detections
66
+ tool-failures Get tool failure overview
67
+ tool-failure-detail <tool_name> Get per-tool failure detail
68
+
69
+ Options:
70
+ --help Show this help message
71
+ --version Show version number
72
+
73
+ Common flags:
74
+ --days-back=N Number of days to look back (1-90)
75
+ --limit=N Maximum results to return
76
+ --offset=N Number of results to skip
77
+ --time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
78
+
79
+ Examples:
80
+ moda overview
81
+ moda overview --days-back=30
82
+ moda clusters --time-range=7d
83
+ moda conversations --search="error" --limit=5
84
+ moda frustrations --days-back=14 --limit=20
85
+ moda context <conversation_id> --window=3`);
86
+ }
87
+ // --- Command Handlers ---
88
+ // Each handler replicates the URL construction from src/handlers.ts
89
+ // but outputs JSON directly instead of wrapping in MCP envelope.
90
+ async function runCommand(command, positional, flags) {
91
+ switch (command) {
92
+ case 'overview': {
93
+ const args = flagsToArgs(flags);
94
+ const params = OverviewSchema.parse(args);
95
+ const queryString = params.days_back ? `?days_back=${params.days_back}` : '';
96
+ const data = await callDataAPI(`/overview${queryString}`);
97
+ console.log(JSON.stringify(data, null, 2));
98
+ break;
99
+ }
100
+ case 'clusters': {
101
+ const args = flagsToArgs(flags);
102
+ const params = ClustersSchema.parse(args);
103
+ const query = new URLSearchParams();
104
+ if (params.parent_id)
105
+ query.set('parent_id', params.parent_id);
106
+ if (params.time_range)
107
+ query.set('time_range', params.time_range);
108
+ const queryString = query.toString() ? `?${query.toString()}` : '';
109
+ const data = await callDataAPI(`/clusters${queryString}`);
110
+ console.log(JSON.stringify(data, null, 2));
111
+ break;
112
+ }
113
+ case 'cluster-conversations': {
114
+ if (!positional) {
115
+ console.error('Error: <node_id> is required');
116
+ console.error('Usage: moda cluster-conversations <node_id> [--limit=N] [--offset=N]');
117
+ process.exit(1);
118
+ }
119
+ const args = flagsToArgs(flags, 'node_id', positional);
120
+ const params = ClusterConversationsSchema.parse(args);
121
+ const query = new URLSearchParams();
122
+ if (params.limit)
123
+ query.set('limit', params.limit.toString());
124
+ if (params.offset !== undefined)
125
+ query.set('offset', params.offset.toString());
126
+ const queryString = query.toString() ? `?${query.toString()}` : '';
127
+ const data = await callDataAPI(`/clusters/${params.node_id}/conversations${queryString}`);
128
+ console.log(JSON.stringify(data, null, 2));
129
+ break;
130
+ }
131
+ case 'conversations': {
132
+ const args = flagsToArgs(flags);
133
+ const params = ConversationsSchema.parse(args);
134
+ const query = new URLSearchParams();
135
+ if (params.search)
136
+ query.set('search', params.search);
137
+ if (params.cluster_id)
138
+ query.set('cluster_id', params.cluster_id);
139
+ if (params.user_id)
140
+ query.set('user_id', params.user_id);
141
+ if (params.time_range)
142
+ query.set('time_range', params.time_range);
143
+ if (params.environment)
144
+ query.set('environment', params.environment);
145
+ if (params.limit)
146
+ query.set('limit', params.limit.toString());
147
+ if (params.offset !== undefined)
148
+ query.set('offset', params.offset.toString());
149
+ const queryString = query.toString() ? `?${query.toString()}` : '';
150
+ const data = await callDataAPI(`/conversations${queryString}`);
151
+ console.log(JSON.stringify(data, null, 2));
152
+ break;
153
+ }
154
+ case 'context': {
155
+ if (!positional) {
156
+ console.error('Error: <conversation_id> is required');
157
+ console.error('Usage: moda context <conversation_id> [--msg-index=N] [--window=N]');
158
+ process.exit(1);
159
+ }
160
+ const args = flagsToArgs(flags, 'conversation_id', positional);
161
+ const params = ContextSchema.parse(args);
162
+ const query = new URLSearchParams();
163
+ if (params.msg_index !== undefined)
164
+ query.set('msg_index', params.msg_index.toString());
165
+ if (params.window)
166
+ query.set('window', params.window.toString());
167
+ const queryString = query.toString() ? `?${query.toString()}` : '';
168
+ const data = await callDataAPI(`/conversations/${params.conversation_id}/context${queryString}`);
169
+ console.log(JSON.stringify(data, null, 2));
170
+ break;
171
+ }
172
+ case 'frustrations': {
173
+ const args = flagsToArgs(flags);
174
+ const params = FrustrationsSchema.parse(args);
175
+ const query = new URLSearchParams();
176
+ if (params.days_back)
177
+ query.set('days_back', params.days_back.toString());
178
+ if (params.limit)
179
+ query.set('limit', params.limit.toString());
180
+ if (params.offset !== undefined)
181
+ query.set('offset', params.offset.toString());
182
+ const queryString = query.toString() ? `?${query.toString()}` : '';
183
+ const data = await callDataAPI(`/frustrations${queryString}`);
184
+ console.log(JSON.stringify(data, null, 2));
185
+ break;
186
+ }
187
+ case 'tool-failures': {
188
+ const args = flagsToArgs(flags);
189
+ const params = ToolFailuresSchema.parse(args);
190
+ const queryString = params.days_back ? `?days_back=${params.days_back}` : '';
191
+ const data = await callDataAPI(`/tool-failures${queryString}`);
192
+ console.log(JSON.stringify(data, null, 2));
193
+ break;
194
+ }
195
+ case 'tool-failure-detail': {
196
+ if (!positional) {
197
+ console.error('Error: <tool_name> is required');
198
+ console.error('Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N] [--offset=N]');
199
+ process.exit(1);
200
+ }
201
+ const args = flagsToArgs(flags, 'tool_name', positional);
202
+ const params = ToolFailureDetailSchema.parse(args);
203
+ const query = new URLSearchParams();
204
+ if (params.subtype)
205
+ query.set('subtype', params.subtype);
206
+ if (params.days_back)
207
+ query.set('days_back', params.days_back.toString());
208
+ if (params.limit)
209
+ query.set('limit', params.limit.toString());
210
+ if (params.offset !== undefined)
211
+ query.set('offset', params.offset.toString());
212
+ const queryString = query.toString() ? `?${query.toString()}` : '';
213
+ const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
214
+ console.log(JSON.stringify(data, null, 2));
215
+ break;
216
+ }
217
+ default:
218
+ console.error(`Error: Unknown command '${command}'`);
219
+ console.error('Run "moda --help" for usage information.');
220
+ process.exit(1);
221
+ }
222
+ }
223
+ // --- Main ---
224
+ async function main() {
225
+ const { command, positional, flags } = parseArgs(process.argv);
226
+ if (command === '--version' || command === '-V' || flags.version === 'true') {
227
+ console.log(`moda-cli ${VERSION}`);
228
+ process.exit(0);
229
+ }
230
+ if (!command || command === '--help' || command === '-h' || flags.help === 'true') {
231
+ printHelp();
232
+ process.exit(command ? 0 : 1);
233
+ }
234
+ validateConfig();
235
+ await runCommand(command, positional, flags);
236
+ }
237
+ // Only run main() when this file is executed directly (not when imported as a module).
238
+ // In ESM, import.meta.url is the file URL of the current module; process.argv[1] is
239
+ // the path of the entry-point script. Comparing them lets tests import parseArgs /
240
+ // flagsToArgs without triggering the CLI bootstrap.
241
+ import { fileURLToPath } from 'url';
242
+ const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
243
+ if (isMain) {
244
+ main().catch((error) => {
245
+ if (error instanceof z.ZodError) {
246
+ const issues = error.errors.map((e) => {
247
+ const field = e.path.length ? e.path.join('.') : 'input';
248
+ return ` ${field}: ${e.message}`;
249
+ });
250
+ console.error(`Validation error:\n${issues.join('\n')}`);
251
+ }
252
+ else if (error instanceof ApiError) {
253
+ console.error(`API error (HTTP ${error.statusCode}): ${error.responseBody || error.message}`);
254
+ }
255
+ else if (error instanceof Error) {
256
+ console.error(`Error: ${error.message}`);
257
+ }
258
+ else {
259
+ console.error('Error:', error);
260
+ }
261
+ process.exit(1);
262
+ });
263
+ }
264
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,6 @@
1
+ export declare const VERSION = "1.0.0";
2
+ export declare const MODA_API_KEY: string | undefined;
3
+ export declare const MODA_BASE_URL: string;
4
+ export declare const PORT: number;
5
+ export declare function validateConfig(): void;
6
+ //# sourceMappingURL=config.d.ts.map
package/dist/config.js ADDED
@@ -0,0 +1,13 @@
1
+ // src/config.ts
2
+ export const VERSION = '1.0.0';
3
+ export const MODA_API_KEY = process.env.MODA_API_KEY;
4
+ export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://modaflows.com';
5
+ export const PORT = parseInt(process.env.PORT || '3003', 10);
6
+ export function validateConfig() {
7
+ if (!MODA_API_KEY) {
8
+ console.error('Error: MODA_API_KEY environment variable is required');
9
+ console.error('Get your API key from https://modaflows.com/settings');
10
+ process.exit(1);
11
+ }
12
+ }
13
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ export declare function handleToolCall(name: string, args: unknown): Promise<{
2
+ content: {
3
+ type: "text";
4
+ text: string;
5
+ }[];
6
+ }>;
7
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1,117 @@
1
+ // src/handlers.ts
2
+ import { z } from 'zod';
3
+ import { callDataAPI } from './api-client.js';
4
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from './schemas.js';
5
+ export async function handleToolCall(name, args) {
6
+ try {
7
+ switch (name) {
8
+ case 'moda_get_overview': {
9
+ const params = OverviewSchema.parse(args);
10
+ const queryString = params.days_back ? `?days_back=${params.days_back}` : '';
11
+ const data = await callDataAPI(`/overview${queryString}`);
12
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
13
+ }
14
+ case 'moda_get_clusters': {
15
+ const params = ClustersSchema.parse(args);
16
+ const query = new URLSearchParams();
17
+ if (params.parent_id)
18
+ query.set('parent_id', params.parent_id);
19
+ if (params.time_range)
20
+ query.set('time_range', params.time_range);
21
+ const queryString = query.toString() ? `?${query.toString()}` : '';
22
+ const data = await callDataAPI(`/clusters${queryString}`);
23
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
24
+ }
25
+ case 'moda_get_cluster_conversations': {
26
+ const params = ClusterConversationsSchema.parse(args);
27
+ const query = new URLSearchParams();
28
+ if (params.limit)
29
+ query.set('limit', params.limit.toString());
30
+ // FIX: use !== undefined instead of falsy check so offset=0 is included
31
+ if (params.offset !== undefined)
32
+ query.set('offset', params.offset.toString());
33
+ const queryString = query.toString() ? `?${query.toString()}` : '';
34
+ const data = await callDataAPI(`/clusters/${params.node_id}/conversations${queryString}`);
35
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
36
+ }
37
+ case 'moda_search_conversations': {
38
+ const params = ConversationsSchema.parse(args);
39
+ const query = new URLSearchParams();
40
+ if (params.search)
41
+ query.set('search', params.search);
42
+ if (params.cluster_id)
43
+ query.set('cluster_id', params.cluster_id);
44
+ if (params.user_id)
45
+ query.set('user_id', params.user_id);
46
+ if (params.time_range)
47
+ query.set('time_range', params.time_range);
48
+ if (params.environment)
49
+ query.set('environment', params.environment);
50
+ if (params.limit)
51
+ query.set('limit', params.limit.toString());
52
+ // FIX: use !== undefined instead of falsy check
53
+ if (params.offset !== undefined)
54
+ query.set('offset', params.offset.toString());
55
+ const queryString = query.toString() ? `?${query.toString()}` : '';
56
+ const data = await callDataAPI(`/conversations${queryString}`);
57
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
58
+ }
59
+ case 'moda_get_conversation_context': {
60
+ const params = ContextSchema.parse(args);
61
+ const query = new URLSearchParams();
62
+ if (params.msg_index !== undefined)
63
+ query.set('msg_index', params.msg_index.toString());
64
+ if (params.window)
65
+ query.set('window', params.window.toString());
66
+ const queryString = query.toString() ? `?${query.toString()}` : '';
67
+ const data = await callDataAPI(`/conversations/${params.conversation_id}/context${queryString}`);
68
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
69
+ }
70
+ case 'moda_get_frustrations': {
71
+ const params = FrustrationsSchema.parse(args);
72
+ const query = new URLSearchParams();
73
+ if (params.days_back)
74
+ query.set('days_back', params.days_back.toString());
75
+ if (params.limit)
76
+ query.set('limit', params.limit.toString());
77
+ // FIX: use !== undefined instead of falsy check
78
+ if (params.offset !== undefined)
79
+ query.set('offset', params.offset.toString());
80
+ const queryString = query.toString() ? `?${query.toString()}` : '';
81
+ const data = await callDataAPI(`/frustrations${queryString}`);
82
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
83
+ }
84
+ case 'moda_get_tool_failures': {
85
+ const params = ToolFailuresSchema.parse(args);
86
+ const queryString = params.days_back ? `?days_back=${params.days_back}` : '';
87
+ const data = await callDataAPI(`/tool-failures${queryString}`);
88
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
89
+ }
90
+ case 'moda_get_tool_failure_detail': {
91
+ const params = ToolFailureDetailSchema.parse(args);
92
+ const query = new URLSearchParams();
93
+ if (params.subtype)
94
+ query.set('subtype', params.subtype);
95
+ if (params.days_back)
96
+ query.set('days_back', params.days_back.toString());
97
+ if (params.limit)
98
+ query.set('limit', params.limit.toString());
99
+ // FIX: use !== undefined instead of falsy check
100
+ if (params.offset !== undefined)
101
+ query.set('offset', params.offset.toString());
102
+ const queryString = query.toString() ? `?${query.toString()}` : '';
103
+ const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
104
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
105
+ }
106
+ default:
107
+ throw new Error(`Unknown tool: ${name}`);
108
+ }
109
+ }
110
+ catch (error) {
111
+ if (error instanceof z.ZodError) {
112
+ throw new Error(`Invalid arguments: ${JSON.stringify(error.errors)}`);
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=http-server.d.ts.map