@moda-ai/cli 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/api-client.test.d.ts +2 -0
- package/dist/__tests__/api-client.test.js +73 -0
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.js +109 -0
- package/dist/__tests__/e2e.test.d.ts +2 -0
- package/dist/__tests__/e2e.test.js +239 -0
- package/dist/__tests__/handlers.test.d.ts +2 -0
- package/dist/__tests__/handlers.test.js +71 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.js +114 -0
- package/dist/__tests__/schemas.test.d.ts +2 -0
- package/dist/__tests__/schemas.test.js +81 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +4 -1
- package/dist/cli.js +1 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/index.js +0 -0
- package/dist/init/index.js +11 -3
- package/package.json +5 -5
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// Mock config before importing api-client
|
|
3
|
+
vi.mock('../config.js', () => ({
|
|
4
|
+
MODA_API_KEY: 'moda_sk_test_key',
|
|
5
|
+
MODA_BASE_URL: 'https://test.modaflows.com',
|
|
6
|
+
}));
|
|
7
|
+
import { callDataAPI, ApiError } from '../api-client.js';
|
|
8
|
+
describe('callDataAPI', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it('constructs correct URL', async () => {
|
|
13
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
14
|
+
ok: true,
|
|
15
|
+
json: () => Promise.resolve({ data: 'test' }),
|
|
16
|
+
});
|
|
17
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
18
|
+
await callDataAPI('/overview?days_back=7');
|
|
19
|
+
expect(mockFetch).toHaveBeenCalledWith('https://test.modaflows.com/api/v1/data/overview?days_back=7', expect.objectContaining({
|
|
20
|
+
headers: expect.objectContaining({
|
|
21
|
+
'x-api-key': 'moda_sk_test_key',
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
it('throws ApiError on non-ok response', async () => {
|
|
26
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
27
|
+
ok: false,
|
|
28
|
+
status: 401,
|
|
29
|
+
text: () => Promise.resolve('Unauthorized'),
|
|
30
|
+
}));
|
|
31
|
+
await expect(callDataAPI('/overview')).rejects.toThrow('API error (401): Unauthorized');
|
|
32
|
+
await vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
33
|
+
ok: false,
|
|
34
|
+
status: 401,
|
|
35
|
+
text: () => Promise.resolve('Unauthorized'),
|
|
36
|
+
}));
|
|
37
|
+
try {
|
|
38
|
+
await callDataAPI('/overview');
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
expect(e).toBeInstanceOf(ApiError);
|
|
42
|
+
expect(e.statusCode).toBe(401);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('retries on 500 errors', async () => {
|
|
46
|
+
const mockFetch = vi.fn()
|
|
47
|
+
.mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve('Internal Server Error') })
|
|
48
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: 'ok' }) });
|
|
49
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
50
|
+
const result = await callDataAPI('/overview');
|
|
51
|
+
expect(result).toEqual({ data: 'ok' });
|
|
52
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
53
|
+
});
|
|
54
|
+
it('does not retry on 4xx errors (except 429)', async () => {
|
|
55
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
56
|
+
ok: false,
|
|
57
|
+
status: 403,
|
|
58
|
+
text: () => Promise.resolve('Forbidden'),
|
|
59
|
+
});
|
|
60
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
61
|
+
await expect(callDataAPI('/overview')).rejects.toThrow('API error (403)');
|
|
62
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
it('returns parsed JSON on success', async () => {
|
|
65
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
66
|
+
ok: true,
|
|
67
|
+
json: () => Promise.resolve({ conversations: { total: 100 } }),
|
|
68
|
+
}));
|
|
69
|
+
const result = await callDataAPI('/overview');
|
|
70
|
+
expect(result).toEqual({ conversations: { total: 100 } });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=api-client.test.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// Mock api-client (same pattern as handlers.test.ts)
|
|
3
|
+
vi.mock('../api-client.js', () => ({
|
|
4
|
+
callDataAPI: vi.fn().mockResolvedValue({ mock: true }),
|
|
5
|
+
}));
|
|
6
|
+
// Mock config to prevent process.exit
|
|
7
|
+
vi.mock('../config.js', () => ({
|
|
8
|
+
validateConfig: vi.fn(),
|
|
9
|
+
MODA_API_KEY: 'test-key',
|
|
10
|
+
MODA_BASE_URL: 'https://test.example.com',
|
|
11
|
+
PORT: 3003,
|
|
12
|
+
}));
|
|
13
|
+
// Import after mocks
|
|
14
|
+
import { parseArgs, flagsToArgs } from '../cli.js';
|
|
15
|
+
import { callDataAPI } from '../api-client.js';
|
|
16
|
+
const mockCallDataAPI = vi.mocked(callDataAPI);
|
|
17
|
+
describe('parseArgs', () => {
|
|
18
|
+
it('extracts command from argv', () => {
|
|
19
|
+
const result = parseArgs(['node', 'cli.js', 'overview']);
|
|
20
|
+
expect(result.command).toBe('overview');
|
|
21
|
+
expect(result.positional).toBeUndefined();
|
|
22
|
+
expect(result.flags).toEqual({});
|
|
23
|
+
});
|
|
24
|
+
it('extracts positional argument', () => {
|
|
25
|
+
const result = parseArgs(['node', 'cli.js', 'cluster-conversations', 'node-123']);
|
|
26
|
+
expect(result.command).toBe('cluster-conversations');
|
|
27
|
+
expect(result.positional).toBe('node-123');
|
|
28
|
+
});
|
|
29
|
+
it('parses --key=value flags', () => {
|
|
30
|
+
const result = parseArgs(['node', 'cli.js', 'overview', '--days-back=30']);
|
|
31
|
+
expect(result.flags).toEqual({ 'days-back': '30' });
|
|
32
|
+
});
|
|
33
|
+
it('parses --key value flags', () => {
|
|
34
|
+
const result = parseArgs(['node', 'cli.js', 'overview', '--days-back', '30']);
|
|
35
|
+
expect(result.flags).toEqual({ 'days-back': '30' });
|
|
36
|
+
});
|
|
37
|
+
it('parses mixed flag formats', () => {
|
|
38
|
+
const result = parseArgs(['node', 'cli.js', 'conversations', '--search=error', '--limit', '5']);
|
|
39
|
+
expect(result.flags).toEqual({ search: 'error', limit: '5' });
|
|
40
|
+
});
|
|
41
|
+
it('handles boolean flags (no value)', () => {
|
|
42
|
+
const result = parseArgs(['node', 'cli.js', '--help']);
|
|
43
|
+
expect(result.command).toBe('--help');
|
|
44
|
+
});
|
|
45
|
+
it('returns empty command for no args', () => {
|
|
46
|
+
const result = parseArgs(['node', 'cli.js']);
|
|
47
|
+
expect(result.command).toBe('');
|
|
48
|
+
});
|
|
49
|
+
it('handles positional before flags', () => {
|
|
50
|
+
const result = parseArgs(['node', 'cli.js', 'context', 'conv-abc', '--window=3']);
|
|
51
|
+
expect(result.command).toBe('context');
|
|
52
|
+
expect(result.positional).toBe('conv-abc');
|
|
53
|
+
expect(result.flags).toEqual({ window: '3' });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('flagsToArgs', () => {
|
|
57
|
+
it('converts kebab-case to snake_case', () => {
|
|
58
|
+
const result = flagsToArgs({ 'days-back': '30', 'time-range': '7d' });
|
|
59
|
+
expect(result).toEqual({ days_back: 30, time_range: '7d' });
|
|
60
|
+
});
|
|
61
|
+
it('parses numeric values', () => {
|
|
62
|
+
const result = flagsToArgs({ limit: '10', offset: '0' });
|
|
63
|
+
expect(result).toEqual({ limit: 10, offset: 0 });
|
|
64
|
+
});
|
|
65
|
+
it('keeps string values as strings', () => {
|
|
66
|
+
const result = flagsToArgs({ search: 'error message' });
|
|
67
|
+
expect(result).toEqual({ search: 'error message' });
|
|
68
|
+
});
|
|
69
|
+
it('includes positional key/value', () => {
|
|
70
|
+
const result = flagsToArgs({ limit: '5' }, 'node_id', 'node-123');
|
|
71
|
+
expect(result).toEqual({ node_id: 'node-123', limit: 5 });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('CLI command dispatch', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
// We test the commands by dynamically importing and calling runCommand.
|
|
79
|
+
// Since runCommand is not exported, we test indirectly via the module's behavior.
|
|
80
|
+
// The key thing to test is that each command constructs the correct API URL.
|
|
81
|
+
// This is tested at the integration level via handlers.test.ts already.
|
|
82
|
+
// Here we focus on the arg parsing + flag conversion that is unique to the CLI.
|
|
83
|
+
it('overview command builds correct args from flags', () => {
|
|
84
|
+
const args = flagsToArgs({ 'days-back': '14' });
|
|
85
|
+
expect(args).toEqual({ days_back: 14 });
|
|
86
|
+
});
|
|
87
|
+
it('cluster-conversations builds args with positional node_id', () => {
|
|
88
|
+
const args = flagsToArgs({ limit: '5', offset: '0' }, 'node_id', 'cluster-abc');
|
|
89
|
+
expect(args).toEqual({ node_id: 'cluster-abc', limit: 5, offset: 0 });
|
|
90
|
+
});
|
|
91
|
+
it('conversations builds args with multiple flags', () => {
|
|
92
|
+
const args = flagsToArgs({ search: 'error', 'user-id': 'user-1', 'time-range': '7d', limit: '10', offset: '0' });
|
|
93
|
+
expect(args).toEqual({ search: 'error', user_id: 'user-1', time_range: '7d', limit: 10, offset: 0 });
|
|
94
|
+
});
|
|
95
|
+
it('context builds args with positional conversation_id', () => {
|
|
96
|
+
const args = flagsToArgs({ 'msg-index': '5', window: '3' }, 'conversation_id', 'conv-xyz');
|
|
97
|
+
expect(args).toEqual({ conversation_id: 'conv-xyz', msg_index: 5, window: 3 });
|
|
98
|
+
});
|
|
99
|
+
it('tool-failure-detail builds args with positional tool_name', () => {
|
|
100
|
+
const args = flagsToArgs({ subtype: 'timeout', 'days-back': '14' }, 'tool_name', 'search');
|
|
101
|
+
expect(args).toEqual({ tool_name: 'search', subtype: 'timeout', days_back: 14 });
|
|
102
|
+
});
|
|
103
|
+
it('offset=0 is preserved (not dropped as falsy)', () => {
|
|
104
|
+
const args = flagsToArgs({ offset: '0' });
|
|
105
|
+
expect(args.offset).toBe(0);
|
|
106
|
+
expect(args.offset).not.toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=cli.test.js.map
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare function loadToken(): string | null;
|
|
2
|
-
export declare function login(): Promise<string>;
|
|
2
|
+
export declare function login(screen?: 'login' | 'signup'): Promise<string>;
|
|
3
3
|
export declare function authFetch(path: string, options?: RequestInit): Promise<Response>;
|
|
4
4
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.js
CHANGED
|
@@ -56,7 +56,7 @@ const SUCCESS_HTML = `<!DOCTYPE html>
|
|
|
56
56
|
</div>
|
|
57
57
|
<script>setTimeout(()=>window.close(),2000)</script>
|
|
58
58
|
</body></html>`;
|
|
59
|
-
export async function login() {
|
|
59
|
+
export async function login(screen = 'login') {
|
|
60
60
|
const verifier = generateVerifier();
|
|
61
61
|
const challenge = generateChallenge(verifier);
|
|
62
62
|
return new Promise((resolve, reject) => {
|
|
@@ -148,6 +148,9 @@ export async function login() {
|
|
|
148
148
|
authUrl.searchParams.set('scope', 'openid profile email');
|
|
149
149
|
authUrl.searchParams.set('code_challenge', challenge);
|
|
150
150
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
151
|
+
if (screen === 'signup') {
|
|
152
|
+
authUrl.searchParams.set('screen_hint', 'signup');
|
|
153
|
+
}
|
|
151
154
|
try {
|
|
152
155
|
const open = (await import('open')).default;
|
|
153
156
|
await open(authUrl.toString());
|
package/dist/cli.js
CHANGED
|
@@ -254,9 +254,7 @@ async function main() {
|
|
|
254
254
|
// the path of the entry-point script. Comparing them lets tests import parseArgs /
|
|
255
255
|
// flagsToArgs without triggering the CLI bootstrap.
|
|
256
256
|
import { fileURLToPath } from 'url';
|
|
257
|
-
|
|
258
|
-
const isMain = process.argv[1] &&
|
|
259
|
-
fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
|
|
257
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
260
258
|
if (isMain) {
|
|
261
259
|
main().catch((error) => {
|
|
262
260
|
if (error instanceof z.ZodError) {
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
|
-
export const VERSION = '1.0.
|
|
2
|
+
export const VERSION = '1.0.0';
|
|
3
3
|
export const MODA_API_KEY = process.env.MODA_API_KEY;
|
|
4
4
|
export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://modaflows.com';
|
|
5
5
|
export const PORT = parseInt(process.env.PORT || '3003', 10);
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/init/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/init/index.ts — Full moda init wizard orchestrator
|
|
2
|
+
import { select } from '@inquirer/prompts';
|
|
2
3
|
import { login, loadToken } from '../auth.js';
|
|
3
4
|
import { selectTenantAndCreateKey } from './tenant.js';
|
|
4
5
|
import { detectProject, installSDK, getInitSnippet } from './detect.js';
|
|
@@ -13,9 +14,16 @@ export async function runInit(flags) {
|
|
|
13
14
|
console.log(' Step 1/5: Authentication');
|
|
14
15
|
let token = loadToken();
|
|
15
16
|
if (!token) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const action = await select({
|
|
18
|
+
message: 'Get started',
|
|
19
|
+
choices: [
|
|
20
|
+
{ name: 'Sign up', value: 'signup' },
|
|
21
|
+
{ name: 'Login', value: 'login' },
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
console.log(` Opening browser to ${action === 'signup' ? 'sign up' : 'log in'}...`);
|
|
25
|
+
token = await login(action);
|
|
26
|
+
console.log(' Authentication successful');
|
|
19
27
|
}
|
|
20
28
|
else {
|
|
21
29
|
console.log(' Already authenticated (cached token)');
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moda-ai/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "CLI and MCP server for Moda - AI agent analytics and observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"cli": "./dist/
|
|
9
|
-
"moda
|
|
10
|
-
"moda": "./dist/
|
|
8
|
+
"cli": "./dist/cli.js",
|
|
9
|
+
"moda": "./dist/cli.js",
|
|
10
|
+
"moda-mcp": "./dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist/**/*.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc",
|
|
25
|
-
"prepublishOnly": "
|
|
25
|
+
"prepublishOnly": "tsc",
|
|
26
26
|
"dev": "tsc --watch",
|
|
27
27
|
"start": "node dist/index.js",
|
|
28
28
|
"start:http": "node dist/http-server.js",
|