@moda-ai/cli 1.0.4 → 1.0.5

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 (41) hide show
  1. package/README.md +24 -54
  2. package/dist/auth.js +39 -67
  3. package/dist/cli.js +93 -12
  4. package/dist/config.d.ts +1 -2
  5. package/dist/config.js +1 -2
  6. package/dist/init/detect.d.ts +1 -1
  7. package/dist/init/detect.js +28 -5
  8. package/dist/init/index.js +26 -68
  9. package/dist/init/mcp.d.ts +1 -7
  10. package/dist/init/mcp.js +5 -88
  11. package/dist/init/rules.d.ts +1 -1
  12. package/dist/init/rules.js +151 -58
  13. package/dist/init/tenant.js +3 -41
  14. package/dist/schemas.d.ts +59 -0
  15. package/dist/schemas.js +35 -0
  16. package/package.json +6 -17
  17. package/skills/integrate-moda/SKILL.md +63 -0
  18. package/skills/moda-cli/SKILL.md +291 -0
  19. package/dist/__tests__/api-client.test.d.ts +0 -2
  20. package/dist/__tests__/api-client.test.js +0 -73
  21. package/dist/__tests__/cli.test.d.ts +0 -2
  22. package/dist/__tests__/cli.test.js +0 -109
  23. package/dist/__tests__/e2e.test.d.ts +0 -2
  24. package/dist/__tests__/e2e.test.js +0 -239
  25. package/dist/__tests__/handlers.test.d.ts +0 -2
  26. package/dist/__tests__/handlers.test.js +0 -71
  27. package/dist/__tests__/integration.test.d.ts +0 -2
  28. package/dist/__tests__/integration.test.js +0 -114
  29. package/dist/__tests__/schemas.test.d.ts +0 -2
  30. package/dist/__tests__/schemas.test.js +0 -81
  31. package/dist/handlers.d.ts +0 -7
  32. package/dist/handlers.js +0 -117
  33. package/dist/http-server.d.ts +0 -3
  34. package/dist/http-server.js +0 -74
  35. package/dist/index.d.ts +0 -3
  36. package/dist/index.js +0 -26
  37. package/dist/tools.d.ts +0 -260
  38. package/dist/tools.js +0 -133
  39. package/dist/worker.d.ts +0 -9
  40. package/dist/worker.js +0 -24
  41. package/mcp-config.example.json +0 -12
@@ -1,239 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { resolve, dirname } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
- const SERVER_PATH = resolve(__dirname, '../../dist/index.js');
7
- const MODA_API_KEY = process.env.MODA_API_KEY;
8
- // --- JSON-RPC Helper ---
9
- let nextId = 1;
10
- function sendJsonRpc(server, method, params) {
11
- const id = nextId++;
12
- return new Promise((resolve, reject) => {
13
- let buffer = '';
14
- const onData = (chunk) => {
15
- buffer += chunk.toString();
16
- const lines = buffer.split('\n');
17
- // Keep the last (potentially incomplete) line in the buffer
18
- buffer = lines.pop() || '';
19
- for (const line of lines) {
20
- if (!line.trim())
21
- continue;
22
- try {
23
- const parsed = JSON.parse(line);
24
- if (parsed.id === id) {
25
- server.stdout.removeListener('data', onData);
26
- resolve(parsed);
27
- }
28
- }
29
- catch {
30
- // Not valid JSON, skip
31
- }
32
- }
33
- };
34
- server.stdout.on('data', onData);
35
- const message = JSON.stringify({
36
- jsonrpc: '2.0',
37
- method,
38
- ...(params && { params }),
39
- id,
40
- });
41
- server.stdin.write(message + '\n');
42
- // Timeout after 30s
43
- setTimeout(() => {
44
- server.stdout.removeListener('data', onData);
45
- reject(new Error(`sendJsonRpc timed out waiting for response id=${id}`));
46
- }, 30_000);
47
- });
48
- }
49
- // --- Shared State for Chained Tests ---
50
- let capturedNodeId;
51
- let capturedConversationId;
52
- let capturedToolName;
53
- // --- Test Suite ---
54
- describe.skipIf(!MODA_API_KEY)('E2E: MCP Server against live backend', { timeout: 30_000 }, () => {
55
- let mcpServer;
56
- beforeAll(async () => {
57
- // Spawn the MCP server process
58
- mcpServer = spawn('node', [SERVER_PATH], {
59
- env: { ...process.env },
60
- stdio: ['pipe', 'pipe', 'pipe'],
61
- });
62
- // Wait for the server startup message on stderr
63
- await new Promise((resolve, reject) => {
64
- const timeout = setTimeout(() => reject(new Error('Server startup timeout')), 10_000);
65
- mcpServer.stderr.on('data', (data) => {
66
- if (data.toString().includes('running on stdio')) {
67
- clearTimeout(timeout);
68
- resolve();
69
- }
70
- });
71
- mcpServer.on('error', (err) => {
72
- clearTimeout(timeout);
73
- reject(err);
74
- });
75
- });
76
- // MCP protocol: send initialize request
77
- const initResponse = await sendJsonRpc(mcpServer, 'initialize', {
78
- protocolVersion: '2024-11-05',
79
- capabilities: {},
80
- clientInfo: { name: 'e2e-test', version: '1.0.0' },
81
- });
82
- expect(initResponse.result).toBeDefined();
83
- // MCP protocol: send initialized notification (no id, no response)
84
- mcpServer.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
85
- });
86
- afterAll(() => {
87
- mcpServer?.kill();
88
- });
89
- // --- Phase 1: Overview ---
90
- it('moda_get_overview returns valid overview data', async () => {
91
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
92
- name: 'moda_get_overview',
93
- arguments: { days_back: 7 },
94
- });
95
- expect(response.result).toBeDefined();
96
- expect(response.result.content).toHaveLength(1);
97
- expect(response.result.content[0].type).toBe('text');
98
- const data = JSON.parse(response.result.content[0].text);
99
- expect(data).toHaveProperty('conversations');
100
- expect(data).toHaveProperty('frustrations');
101
- expect(data).toHaveProperty('tool_failures');
102
- expect(data).toHaveProperty('top_clusters');
103
- expect(data).toHaveProperty('recent_activity');
104
- });
105
- // --- Phase 2: Independent Tools ---
106
- it('moda_get_clusters returns cluster hierarchy', async () => {
107
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
108
- name: 'moda_get_clusters',
109
- arguments: {},
110
- });
111
- expect(response.result).toBeDefined();
112
- const data = JSON.parse(response.result.content[0].text);
113
- expect(data).toHaveProperty('clusters');
114
- expect(Array.isArray(data.clusters)).toBe(true);
115
- if (data.clusters.length > 0) {
116
- const cluster = data.clusters[0];
117
- expect(cluster).toHaveProperty('node_id');
118
- expect(cluster).toHaveProperty('label');
119
- // Capture for Phase 3
120
- capturedNodeId = cluster.node_id;
121
- }
122
- });
123
- it('moda_search_conversations returns conversations list', async () => {
124
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
125
- name: 'moda_search_conversations',
126
- arguments: {},
127
- });
128
- expect(response.result).toBeDefined();
129
- const data = JSON.parse(response.result.content[0].text);
130
- expect(data).toHaveProperty('conversations');
131
- expect(Array.isArray(data.conversations)).toBe(true);
132
- expect(data).toHaveProperty('pagination');
133
- if (data.conversations.length > 0) {
134
- const convo = data.conversations[0];
135
- expect(convo).toHaveProperty('conversation_id');
136
- expect(convo).toHaveProperty('summary');
137
- // Capture for Phase 3
138
- capturedConversationId = convo.conversation_id;
139
- }
140
- });
141
- it('moda_get_frustrations returns frustration data', async () => {
142
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
143
- name: 'moda_get_frustrations',
144
- arguments: { days_back: 7 },
145
- });
146
- expect(response.result).toBeDefined();
147
- const data = JSON.parse(response.result.content[0].text);
148
- expect(data).toHaveProperty('summary');
149
- expect(data).toHaveProperty('frustrations');
150
- expect(Array.isArray(data.frustrations)).toBe(true);
151
- if (data.frustrations.length > 0) {
152
- expect(data.frustrations[0]).toHaveProperty('conversation_id');
153
- }
154
- });
155
- it('moda_get_tool_failures returns failure overview', async () => {
156
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
157
- name: 'moda_get_tool_failures',
158
- arguments: { days_back: 7 },
159
- });
160
- expect(response.result).toBeDefined();
161
- const data = JSON.parse(response.result.content[0].text);
162
- expect(data).toHaveProperty('summary');
163
- expect(data).toHaveProperty('tools');
164
- expect(Array.isArray(data.tools)).toBe(true);
165
- if (data.tools.length > 0) {
166
- const tool = data.tools[0];
167
- expect(tool).toHaveProperty('tool_name');
168
- expect(tool).toHaveProperty('failure_count');
169
- // Capture for Phase 3
170
- capturedToolName = tool.tool_name;
171
- }
172
- });
173
- // --- Phase 3: Chained Tools ---
174
- it('moda_get_cluster_conversations returns conversations for a cluster', async () => {
175
- if (!capturedNodeId) {
176
- console.warn('Skipping: no node_id captured from moda_get_clusters (empty data)');
177
- return;
178
- }
179
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
180
- name: 'moda_get_cluster_conversations',
181
- arguments: { node_id: capturedNodeId },
182
- });
183
- expect(response.result).toBeDefined();
184
- const data = JSON.parse(response.result.content[0].text);
185
- expect(data).toHaveProperty('cluster');
186
- expect(data).toHaveProperty('conversations');
187
- expect(Array.isArray(data.conversations)).toBe(true);
188
- expect(data).toHaveProperty('pagination');
189
- if (data.conversations.length > 0) {
190
- expect(data.conversations[0]).toHaveProperty('conversation_id');
191
- }
192
- });
193
- it('moda_get_conversation_context returns message context', async () => {
194
- if (!capturedConversationId) {
195
- console.warn('Skipping: no conversation_id captured from moda_search_conversations (empty data)');
196
- return;
197
- }
198
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
199
- name: 'moda_get_conversation_context',
200
- arguments: { conversation_id: capturedConversationId },
201
- });
202
- expect(response.result).toBeDefined();
203
- const data = JSON.parse(response.result.content[0].text);
204
- expect(data).toHaveProperty('conversation_id');
205
- expect(data).toHaveProperty('summary');
206
- expect(data).toHaveProperty('context');
207
- expect(data.context).toHaveProperty('messages');
208
- expect(Array.isArray(data.context.messages)).toBe(true);
209
- });
210
- it('moda_get_tool_failure_detail returns failure details', async () => {
211
- if (!capturedToolName) {
212
- console.warn('Skipping: no tool_name captured from moda_get_tool_failures (empty data)');
213
- return;
214
- }
215
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
216
- name: 'moda_get_tool_failure_detail',
217
- arguments: { tool_name: capturedToolName },
218
- });
219
- expect(response.result).toBeDefined();
220
- const data = JSON.parse(response.result.content[0].text);
221
- expect(data).toHaveProperty('tool_name');
222
- expect(typeof data.tool_name).toBe('string');
223
- expect(data).toHaveProperty('subtypes');
224
- expect(Array.isArray(data.subtypes)).toBe(true);
225
- expect(data).toHaveProperty('examples');
226
- expect(Array.isArray(data.examples)).toBe(true);
227
- });
228
- // --- Error Handling ---
229
- it('returns error for invalid arguments (Zod validation)', async () => {
230
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
231
- name: 'moda_get_overview',
232
- arguments: { days_back: 999 },
233
- });
234
- // The MCP SDK wraps handler errors into an error response
235
- expect(response.error).toBeDefined();
236
- expect(response.error.message).toContain('Invalid arguments');
237
- });
238
- });
239
- //# sourceMappingURL=e2e.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=handlers.test.d.ts.map
@@ -1,71 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // Mock api-client
3
- vi.mock('../api-client.js', () => ({
4
- callDataAPI: vi.fn().mockResolvedValue({ mock: true }),
5
- }));
6
- import { handleToolCall } from '../handlers.js';
7
- import { callDataAPI } from '../api-client.js';
8
- const mockCallDataAPI = vi.mocked(callDataAPI);
9
- describe('handleToolCall', () => {
10
- beforeEach(() => {
11
- vi.clearAllMocks();
12
- });
13
- it('moda_get_overview calls correct endpoint', async () => {
14
- await handleToolCall('moda_get_overview', { days_back: 14 });
15
- expect(mockCallDataAPI).toHaveBeenCalledWith('/overview?days_back=14');
16
- });
17
- it('moda_get_overview with no args omits query string', async () => {
18
- await handleToolCall('moda_get_overview', {});
19
- expect(mockCallDataAPI).toHaveBeenCalledWith('/overview');
20
- });
21
- it('moda_get_clusters passes parent_id and time_range', async () => {
22
- await handleToolCall('moda_get_clusters', { parent_id: 'node-1', time_range: '7d' });
23
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('parent_id=node-1'));
24
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('time_range=7d'));
25
- });
26
- it('moda_get_cluster_conversations includes offset=0', async () => {
27
- await handleToolCall('moda_get_cluster_conversations', { node_id: 'c-1', offset: 0 });
28
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
29
- expect(calledUrl).toContain('offset=0');
30
- });
31
- it('moda_search_conversations includes offset=0', async () => {
32
- await handleToolCall('moda_search_conversations', { offset: 0 });
33
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
34
- expect(calledUrl).toContain('offset=0');
35
- });
36
- it('moda_get_frustrations includes offset=0', async () => {
37
- await handleToolCall('moda_get_frustrations', { offset: 0 });
38
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
39
- expect(calledUrl).toContain('offset=0');
40
- });
41
- it('moda_get_tool_failure_detail includes offset=0', async () => {
42
- await handleToolCall('moda_get_tool_failure_detail', { tool_name: 'search', offset: 0 });
43
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
44
- expect(calledUrl).toContain('offset=0');
45
- });
46
- it('moda_get_conversation_context uses conversation_id in path', async () => {
47
- await handleToolCall('moda_get_conversation_context', { conversation_id: 'conv-abc' });
48
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('/conversations/conv-abc/context'));
49
- });
50
- it('moda_get_tool_failures calls correct endpoint', async () => {
51
- await handleToolCall('moda_get_tool_failures', { days_back: 30 });
52
- expect(mockCallDataAPI).toHaveBeenCalledWith('/tool-failures?days_back=30');
53
- });
54
- it('moda_get_tool_failure_detail uses tool_name in path', async () => {
55
- await handleToolCall('moda_get_tool_failure_detail', { tool_name: 'compose' });
56
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('/tool-failures/compose'));
57
- });
58
- it('throws on unknown tool', async () => {
59
- await expect(handleToolCall('unknown_tool', {})).rejects.toThrow('Unknown tool: unknown_tool');
60
- });
61
- it('throws on invalid arguments', async () => {
62
- await expect(handleToolCall('moda_get_overview', { days_back: 'invalid' })).rejects.toThrow('Invalid arguments');
63
- });
64
- it('returns content array with JSON text', async () => {
65
- const result = await handleToolCall('moda_get_overview', {});
66
- expect(result.content).toHaveLength(1);
67
- expect(result.content[0].type).toBe('text');
68
- expect(JSON.parse(result.content[0].text)).toEqual({ mock: true });
69
- });
70
- });
71
- //# sourceMappingURL=handlers.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=integration.test.d.ts.map
@@ -1,114 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { spawn } from 'child_process';
3
- import { resolve } from 'path';
4
- import { fileURLToPath } from 'url';
5
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
6
- const ROOT = resolve(__dirname, '../..');
7
- let mockServer;
8
- function sendJsonRpc(proc, method, params, id = 1) {
9
- return new Promise((resolve, reject) => {
10
- const timeout = setTimeout(() => reject(new Error('Timeout waiting for response')), 10000);
11
- let buffer = '';
12
- const onData = (data) => {
13
- buffer += data.toString();
14
- const lines = buffer.split('\n');
15
- for (const line of lines) {
16
- if (!line.trim())
17
- continue;
18
- try {
19
- const parsed = JSON.parse(line);
20
- if (parsed.id === id) {
21
- clearTimeout(timeout);
22
- proc.stdout.off('data', onData);
23
- resolve(parsed);
24
- }
25
- }
26
- catch { /* incomplete JSON */ }
27
- }
28
- };
29
- proc.stdout.on('data', onData);
30
- const request = JSON.stringify({ jsonrpc: '2.0', method, params, id });
31
- proc.stdin.write(request + '\n');
32
- });
33
- }
34
- beforeAll(async () => {
35
- // Start mock server
36
- mockServer = spawn('node', [resolve(ROOT, 'mock-server.js')], { stdio: ['pipe', 'pipe', 'pipe'] });
37
- await new Promise((resolve, reject) => {
38
- const timeout = setTimeout(() => reject(new Error('Mock server did not start in time')), 10000);
39
- // mock-server.js uses console.log which goes to stdout
40
- mockServer.stdout.on('data', (data) => {
41
- if (data.toString().includes('Mock Data API server running')) {
42
- clearTimeout(timeout);
43
- resolve();
44
- }
45
- });
46
- mockServer.stderr.on('data', (data) => {
47
- if (data.toString().includes('Mock Data API server running')) {
48
- clearTimeout(timeout);
49
- resolve();
50
- }
51
- });
52
- });
53
- }, 15000);
54
- afterAll(() => {
55
- mockServer?.kill();
56
- });
57
- describe('MCP Server Integration', () => {
58
- it('lists 8 tools', async () => {
59
- const mcpServer = spawn('node', [resolve(ROOT, 'dist/index.js')], {
60
- env: { ...process.env, MODA_API_KEY: 'moda_sk_test', MODA_BASE_URL: 'http://localhost:3002' },
61
- stdio: ['pipe', 'pipe', 'pipe'],
62
- });
63
- try {
64
- // Wait for server to start
65
- await new Promise((resolve, reject) => {
66
- const timeout = setTimeout(() => reject(new Error('MCP server did not start in time')), 10000);
67
- mcpServer.stderr.on('data', (data) => {
68
- if (data.toString().includes('running on stdio')) {
69
- clearTimeout(timeout);
70
- resolve();
71
- }
72
- });
73
- });
74
- const response = await sendJsonRpc(mcpServer, 'tools/list');
75
- expect(response.result.tools).toHaveLength(8);
76
- const toolNames = response.result.tools.map((t) => t.name);
77
- expect(toolNames).toContain('moda_get_overview');
78
- expect(toolNames).toContain('moda_get_frustrations');
79
- expect(toolNames).toContain('moda_get_tool_failures');
80
- }
81
- finally {
82
- mcpServer.kill();
83
- }
84
- }, 20000);
85
- it('calls moda_get_overview and returns data', async () => {
86
- const mcpServer = spawn('node', [resolve(ROOT, 'dist/index.js')], {
87
- env: { ...process.env, MODA_API_KEY: 'moda_sk_test', MODA_BASE_URL: 'http://localhost:3002' },
88
- stdio: ['pipe', 'pipe', 'pipe'],
89
- });
90
- try {
91
- await new Promise((resolve, reject) => {
92
- const timeout = setTimeout(() => reject(new Error('MCP server did not start in time')), 10000);
93
- mcpServer.stderr.on('data', (data) => {
94
- if (data.toString().includes('running on stdio')) {
95
- clearTimeout(timeout);
96
- resolve();
97
- }
98
- });
99
- });
100
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
101
- name: 'moda_get_overview',
102
- arguments: { days_back: 7 },
103
- });
104
- expect(response.result.content).toHaveLength(1);
105
- expect(response.result.content[0].type).toBe('text');
106
- const data = JSON.parse(response.result.content[0].text);
107
- expect(data.conversations.total).toBe(1011);
108
- }
109
- finally {
110
- mcpServer.kill();
111
- }
112
- }, 20000);
113
- });
114
- //# sourceMappingURL=integration.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=schemas.test.d.ts.map
@@ -1,81 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from '../schemas.js';
3
- describe('OverviewSchema', () => {
4
- it('accepts valid days_back', () => {
5
- expect(OverviewSchema.parse({ days_back: 30 })).toEqual({ days_back: 30 });
6
- });
7
- it('accepts empty object with defaults', () => {
8
- expect(OverviewSchema.parse({})).toEqual({});
9
- });
10
- it('rejects days_back > 90', () => {
11
- expect(() => OverviewSchema.parse({ days_back: 91 })).toThrow();
12
- });
13
- it('rejects days_back < 1', () => {
14
- expect(() => OverviewSchema.parse({ days_back: 0 })).toThrow();
15
- });
16
- });
17
- describe('ClustersSchema', () => {
18
- it('accepts all backend time_range values', () => {
19
- for (const val of ['all', '1h', '3d', '7d', '24h', '30d', '90d']) {
20
- expect(ClustersSchema.parse({ time_range: val }).time_range).toBe(val);
21
- }
22
- });
23
- it('rejects invalid time_range', () => {
24
- expect(() => ClustersSchema.parse({ time_range: '2d' })).toThrow();
25
- });
26
- });
27
- describe('ConversationsSchema', () => {
28
- it('accepts 3d time_range (previously missing)', () => {
29
- expect(ConversationsSchema.parse({ time_range: '3d' }).time_range).toBe('3d');
30
- });
31
- it('accepts all valid time_range values', () => {
32
- for (const val of ['all', '1h', '3d', '24h', '7d', '30d', '90d']) {
33
- expect(ConversationsSchema.parse({ time_range: val }).time_range).toBe(val);
34
- }
35
- });
36
- });
37
- describe('FrustrationsSchema', () => {
38
- it('accepts limit up to 20', () => {
39
- expect(FrustrationsSchema.parse({ limit: 20 }).limit).toBe(20);
40
- });
41
- it('rejects limit > 20 (previously allowed 100)', () => {
42
- expect(() => FrustrationsSchema.parse({ limit: 21 })).toThrow();
43
- });
44
- });
45
- describe('ToolFailureDetailSchema', () => {
46
- it('accepts limit up to 20', () => {
47
- expect(ToolFailureDetailSchema.parse({ tool_name: 'search', limit: 20 }).limit).toBe(20);
48
- });
49
- it('rejects limit > 20 (previously allowed 100)', () => {
50
- expect(() => ToolFailureDetailSchema.parse({ tool_name: 'search', limit: 21 })).toThrow();
51
- });
52
- it('requires tool_name', () => {
53
- expect(() => ToolFailureDetailSchema.parse({})).toThrow();
54
- });
55
- });
56
- describe('ClusterConversationsSchema', () => {
57
- it('requires node_id', () => {
58
- expect(() => ClusterConversationsSchema.parse({})).toThrow();
59
- });
60
- it('accepts valid input', () => {
61
- const result = ClusterConversationsSchema.parse({ node_id: 'cluster-1', limit: 10, offset: 0 });
62
- expect(result.node_id).toBe('cluster-1');
63
- });
64
- });
65
- describe('ContextSchema', () => {
66
- it('requires conversation_id', () => {
67
- expect(() => ContextSchema.parse({})).toThrow();
68
- });
69
- it('accepts msg_index of 0', () => {
70
- expect(ContextSchema.parse({ conversation_id: 'conv-1', msg_index: 0 }).msg_index).toBe(0);
71
- });
72
- it('rejects window > 5', () => {
73
- expect(() => ContextSchema.parse({ conversation_id: 'conv-1', window: 6 })).toThrow();
74
- });
75
- });
76
- describe('ToolFailuresSchema', () => {
77
- it('accepts valid days_back', () => {
78
- expect(ToolFailuresSchema.parse({ days_back: 14 }).days_back).toBe(14);
79
- });
80
- });
81
- //# sourceMappingURL=schemas.test.js.map
@@ -1,7 +0,0 @@
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
package/dist/handlers.js DELETED
@@ -1,117 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=http-server.d.ts.map