@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 +8 -0
- package/README.md +124 -0
- package/dist/api-client.d.ts +7 -0
- package/dist/api-client.js +67 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +264 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +13 -0
- package/dist/handlers.d.ts +7 -0
- package/dist/handlers.js +117 -0
- package/dist/http-server.d.ts +3 -0
- package/dist/http-server.js +74 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +26 -0
- package/dist/schemas.d.ts +109 -0
- package/dist/schemas.js +48 -0
- package/dist/tools.d.ts +260 -0
- package/dist/tools.js +133 -0
- package/dist/worker.d.ts +9 -0
- package/dist/worker.js +24 -0
- package/mcp-config.example.json +12 -0
- package/package.json +69 -0
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
|
package/dist/config.d.ts
ADDED
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
|
package/dist/handlers.js
ADDED
|
@@ -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
|