@moda-ai/cli 1.0.1 → 1.0.3
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/README.md +5 -5
- 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 +4 -0
- package/dist/auth.js +187 -0
- package/dist/cli.js +15 -0
- package/dist/index.js +0 -0
- package/dist/init/detect.d.ts +9 -0
- package/dist/init/detect.js +156 -0
- package/dist/init/index.d.ts +2 -0
- package/dist/init/index.js +94 -0
- package/dist/init/mcp.d.ts +8 -0
- package/dist/init/mcp.js +110 -0
- package/dist/init/rules.d.ts +2 -0
- package/dist/init/rules.js +72 -0
- package/dist/init/tenant.d.ts +9 -0
- package/dist/init/tenant.js +91 -0
- package/dist/schemas.d.ts +2 -2
- package/package.json +6 -5
|
@@ -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
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/auth.ts — Browser-based PKCE authentication for moda init
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { MODA_BASE_URL } from './config.js';
|
|
8
|
+
const AUTH0_DOMAIN = 'dev-6kukcoznniv30wau.us.auth0.com';
|
|
9
|
+
const AUTH0_CLIENT_ID = 'JuqWjslalOwI9TXLeU2KkF0NTZab3qgC';
|
|
10
|
+
const AUTH0_AUDIENCE = 'https://moda-api';
|
|
11
|
+
const CALLBACK_PORT = 8432;
|
|
12
|
+
const AUTH_TIMEOUT_MS = 120_000;
|
|
13
|
+
const MODA_DIR = join(homedir(), '.moda');
|
|
14
|
+
const TOKEN_PATH = join(MODA_DIR, 'auth.json');
|
|
15
|
+
// --- PKCE ---
|
|
16
|
+
function generateVerifier() {
|
|
17
|
+
return randomBytes(32).toString('base64url');
|
|
18
|
+
}
|
|
19
|
+
function generateChallenge(verifier) {
|
|
20
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
21
|
+
}
|
|
22
|
+
export function loadToken() {
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(TOKEN_PATH))
|
|
25
|
+
return null;
|
|
26
|
+
const data = JSON.parse(readFileSync(TOKEN_PATH, 'utf8'));
|
|
27
|
+
if (data.expires_at && Date.now() / 1000 > data.expires_at - 60) {
|
|
28
|
+
return null; // Expired or about to expire
|
|
29
|
+
}
|
|
30
|
+
return data.access_token || null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function saveToken(accessToken, expiresIn) {
|
|
37
|
+
mkdirSync(MODA_DIR, { recursive: true });
|
|
38
|
+
const data = {
|
|
39
|
+
access_token: accessToken,
|
|
40
|
+
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
41
|
+
};
|
|
42
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
43
|
+
try {
|
|
44
|
+
chmodSync(TOKEN_PATH, 0o600);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// chmod may fail on some systems, the mode flag above handles it
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// --- Login Flow ---
|
|
51
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
52
|
+
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fafafa">
|
|
53
|
+
<div style="text-align:center">
|
|
54
|
+
<h2>Authenticated</h2>
|
|
55
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
56
|
+
</div>
|
|
57
|
+
<script>setTimeout(()=>window.close(),2000)</script>
|
|
58
|
+
</body></html>`;
|
|
59
|
+
export async function login(screen = 'login') {
|
|
60
|
+
const verifier = generateVerifier();
|
|
61
|
+
const challenge = generateChallenge(verifier);
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let settled = false;
|
|
64
|
+
const server = createServer(async (req, res) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
|
|
68
|
+
if (url.pathname !== '/callback') {
|
|
69
|
+
res.writeHead(404);
|
|
70
|
+
res.end('Not found');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const code = url.searchParams.get('code');
|
|
74
|
+
const error = url.searchParams.get('error');
|
|
75
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
76
|
+
if (error) {
|
|
77
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
78
|
+
res.end(`<html><body><h2>Error</h2><p>${errorDescription || error}</p></body></html>`);
|
|
79
|
+
settled = true;
|
|
80
|
+
server.close();
|
|
81
|
+
reject(new Error(`Auth0 error: ${errorDescription || error}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!code) {
|
|
85
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
86
|
+
res.end('<html><body><h2>Error</h2><p>No authorization code received.</p></body></html>');
|
|
87
|
+
settled = true;
|
|
88
|
+
server.close();
|
|
89
|
+
reject(new Error('No authorization code received'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Exchange code for tokens
|
|
93
|
+
try {
|
|
94
|
+
const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
grant_type: 'authorization_code',
|
|
99
|
+
client_id: AUTH0_CLIENT_ID,
|
|
100
|
+
code,
|
|
101
|
+
redirect_uri: `http://localhost:${CALLBACK_PORT}/callback`,
|
|
102
|
+
code_verifier: verifier,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
if (!tokenResponse.ok) {
|
|
106
|
+
const errBody = await tokenResponse.text();
|
|
107
|
+
throw new Error(`Token exchange failed (${tokenResponse.status}): ${errBody}`);
|
|
108
|
+
}
|
|
109
|
+
const tokens = (await tokenResponse.json());
|
|
110
|
+
saveToken(tokens.access_token, tokens.expires_in);
|
|
111
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
112
|
+
res.end(SUCCESS_HTML);
|
|
113
|
+
settled = true;
|
|
114
|
+
server.close();
|
|
115
|
+
resolve(tokens.access_token);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
119
|
+
res.end('<html><body><h2>Error</h2><p>Token exchange failed.</p></body></html>');
|
|
120
|
+
settled = true;
|
|
121
|
+
server.close();
|
|
122
|
+
reject(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Timeout
|
|
126
|
+
const timeout = setTimeout(() => {
|
|
127
|
+
if (!settled) {
|
|
128
|
+
settled = true;
|
|
129
|
+
server.close();
|
|
130
|
+
reject(new Error('Authentication timed out. Please try again.'));
|
|
131
|
+
}
|
|
132
|
+
}, AUTH_TIMEOUT_MS);
|
|
133
|
+
server.on('close', () => clearTimeout(timeout));
|
|
134
|
+
server.on('error', (err) => {
|
|
135
|
+
if (err.code === 'EADDRINUSE') {
|
|
136
|
+
reject(new Error(`Port ${CALLBACK_PORT} is in use. Close the process using it and try again.`));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
reject(err);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
server.listen(CALLBACK_PORT, async () => {
|
|
143
|
+
const authUrl = new URL(`https://${AUTH0_DOMAIN}/authorize`);
|
|
144
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
145
|
+
authUrl.searchParams.set('client_id', AUTH0_CLIENT_ID);
|
|
146
|
+
authUrl.searchParams.set('redirect_uri', `http://localhost:${CALLBACK_PORT}/callback`);
|
|
147
|
+
authUrl.searchParams.set('audience', AUTH0_AUDIENCE);
|
|
148
|
+
authUrl.searchParams.set('scope', 'openid profile email');
|
|
149
|
+
authUrl.searchParams.set('code_challenge', challenge);
|
|
150
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
151
|
+
if (screen === 'signup') {
|
|
152
|
+
authUrl.searchParams.set('screen_hint', 'signup');
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const open = (await import('open')).default;
|
|
156
|
+
await open(authUrl.toString());
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
console.error(` Open this URL in your browser:\n ${authUrl.toString()}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Clean up on process exit
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
if (!settled) {
|
|
165
|
+
settled = true;
|
|
166
|
+
server.close();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
process.on('SIGINT', cleanup);
|
|
170
|
+
process.on('SIGTERM', cleanup);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// --- Authenticated Fetch ---
|
|
174
|
+
export async function authFetch(path, options = {}) {
|
|
175
|
+
const token = loadToken();
|
|
176
|
+
if (!token) {
|
|
177
|
+
throw new Error('Not authenticated. Run "moda init" to log in.');
|
|
178
|
+
}
|
|
179
|
+
const url = `${MODA_BASE_URL}${path}`;
|
|
180
|
+
const headers = new Headers(options.headers);
|
|
181
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
182
|
+
if (!headers.has('Content-Type') && options.body) {
|
|
183
|
+
headers.set('Content-Type', 'application/json');
|
|
184
|
+
}
|
|
185
|
+
return fetch(url, { ...options, headers });
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/cli.js
CHANGED
|
@@ -57,6 +57,7 @@ function printHelp() {
|
|
|
57
57
|
console.error(`Usage: moda <command> [options]
|
|
58
58
|
|
|
59
59
|
Commands:
|
|
60
|
+
init Set up Moda in your project
|
|
60
61
|
overview Dashboard overview with KPIs
|
|
61
62
|
clusters Browse topic cluster hierarchy
|
|
62
63
|
cluster-conversations <node_id> List conversations in a cluster
|
|
@@ -76,7 +77,14 @@ Common flags:
|
|
|
76
77
|
--offset=N Number of results to skip
|
|
77
78
|
--time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
|
|
78
79
|
|
|
80
|
+
Init flags:
|
|
81
|
+
--yes Accept all defaults (non-interactive)
|
|
82
|
+
--skip-sdk Skip SDK installation
|
|
83
|
+
--skip-mcp Skip MCP server configuration
|
|
84
|
+
|
|
79
85
|
Examples:
|
|
86
|
+
moda init
|
|
87
|
+
moda init --yes
|
|
80
88
|
moda overview
|
|
81
89
|
moda overview --days-back=30
|
|
82
90
|
moda clusters --time-range=7d
|
|
@@ -231,6 +239,13 @@ async function main() {
|
|
|
231
239
|
printHelp();
|
|
232
240
|
process.exit(command ? 0 : 1);
|
|
233
241
|
}
|
|
242
|
+
// init does NOT require MODA_API_KEY — that's what it creates
|
|
243
|
+
if (command === 'init') {
|
|
244
|
+
const { runInit } = await import('./init/index.js');
|
|
245
|
+
const initFlags = flagsToArgs(flags);
|
|
246
|
+
await runInit(initFlags);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
234
249
|
validateConfig();
|
|
235
250
|
await runCommand(command, positional, flags);
|
|
236
251
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ProjectInfo {
|
|
2
|
+
language: 'typescript' | 'python' | null;
|
|
3
|
+
packageManager: string | null;
|
|
4
|
+
llmProviders: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function detectProject(): Promise<ProjectInfo>;
|
|
7
|
+
export declare function installSDK(project: ProjectInfo): Promise<void>;
|
|
8
|
+
export declare function getInitSnippet(project: ProjectInfo): string;
|
|
9
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/init/detect.ts — Project detection and SDK installation
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { select } from '@inquirer/prompts';
|
|
6
|
+
const LOCKFILE_TO_PM = {
|
|
7
|
+
'package-lock.json': 'npm',
|
|
8
|
+
'yarn.lock': 'yarn',
|
|
9
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
10
|
+
'bun.lockb': 'bun',
|
|
11
|
+
'bun.lock': 'bun',
|
|
12
|
+
'poetry.lock': 'poetry',
|
|
13
|
+
'uv.lock': 'uv',
|
|
14
|
+
};
|
|
15
|
+
const INSTALL_COMMANDS = {
|
|
16
|
+
npm: 'npm install moda-ai',
|
|
17
|
+
yarn: 'yarn add moda-ai',
|
|
18
|
+
pnpm: 'pnpm add moda-ai',
|
|
19
|
+
bun: 'bun add moda-ai',
|
|
20
|
+
pip: 'pip install moda-ai',
|
|
21
|
+
poetry: 'poetry add moda-ai',
|
|
22
|
+
uv: 'uv add moda-ai',
|
|
23
|
+
};
|
|
24
|
+
const LLM_PATTERNS = [
|
|
25
|
+
{ pattern: /from openai|import openai|require\(['"]openai['"]\)/i, name: 'OpenAI' },
|
|
26
|
+
{ pattern: /from anthropic|import.*@anthropic-ai\/sdk|require\(['"]@anthropic-ai\/sdk['"]\)/i, name: 'Anthropic' },
|
|
27
|
+
{ pattern: /from ai |import.*@ai-sdk|require\(['"](ai|@ai-sdk)/i, name: 'Vercel AI SDK' },
|
|
28
|
+
{ pattern: /from langchain|import.*langchain/i, name: 'LangChain' },
|
|
29
|
+
{ pattern: /from claude_agent_sdk|import.*claude.agent/i, name: 'Claude Agent SDK' },
|
|
30
|
+
];
|
|
31
|
+
export async function detectProject() {
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const hasPackageJson = existsSync(join(cwd, 'package.json'));
|
|
34
|
+
const hasPython = existsSync(join(cwd, 'pyproject.toml')) ||
|
|
35
|
+
existsSync(join(cwd, 'requirements.txt')) ||
|
|
36
|
+
existsSync(join(cwd, 'Pipfile'));
|
|
37
|
+
let language = null;
|
|
38
|
+
if (hasPackageJson && hasPython) {
|
|
39
|
+
language = await select({
|
|
40
|
+
message: 'Both TypeScript and Python detected. Which to configure?',
|
|
41
|
+
choices: [
|
|
42
|
+
{ name: 'TypeScript / JavaScript', value: 'typescript' },
|
|
43
|
+
{ name: 'Python', value: 'python' },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else if (hasPackageJson) {
|
|
48
|
+
language = 'typescript';
|
|
49
|
+
}
|
|
50
|
+
else if (hasPython) {
|
|
51
|
+
language = 'python';
|
|
52
|
+
}
|
|
53
|
+
// Detect package manager
|
|
54
|
+
let packageManager = null;
|
|
55
|
+
if (language === 'typescript') {
|
|
56
|
+
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
57
|
+
if (existsSync(join(cwd, lockfile))) {
|
|
58
|
+
packageManager = pm;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!packageManager)
|
|
63
|
+
packageManager = 'npm';
|
|
64
|
+
}
|
|
65
|
+
else if (language === 'python') {
|
|
66
|
+
if (existsSync(join(cwd, 'poetry.lock'))) {
|
|
67
|
+
packageManager = 'poetry';
|
|
68
|
+
}
|
|
69
|
+
else if (existsSync(join(cwd, 'uv.lock'))) {
|
|
70
|
+
packageManager = 'uv';
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
packageManager = 'pip';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Detect LLM providers (shallow scan)
|
|
77
|
+
const llmProviders = detectLLMProviders(cwd, language);
|
|
78
|
+
return { language, packageManager, llmProviders };
|
|
79
|
+
}
|
|
80
|
+
function detectLLMProviders(cwd, language) {
|
|
81
|
+
if (!language)
|
|
82
|
+
return [];
|
|
83
|
+
const providers = new Set();
|
|
84
|
+
const extensions = language === 'typescript' ? ['.ts', '.tsx', '.js', '.jsx', '.mjs'] : ['.py'];
|
|
85
|
+
const dirs = ['src', 'app', 'lib', '.'];
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
const dirPath = join(cwd, dir);
|
|
88
|
+
if (!existsSync(dirPath))
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
const files = readdirSync(dirPath, { withFileTypes: true });
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
if (!file.isFile())
|
|
94
|
+
continue;
|
|
95
|
+
if (!extensions.some((ext) => file.name.endsWith(ext)))
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
const content = readFileSync(join(dirPath, file.name), 'utf8');
|
|
99
|
+
for (const { pattern, name } of LLM_PATTERNS) {
|
|
100
|
+
if (pattern.test(content)) {
|
|
101
|
+
providers.add(name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Skip unreadable files
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Skip unreadable dirs
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return Array.from(providers);
|
|
115
|
+
}
|
|
116
|
+
export async function installSDK(project) {
|
|
117
|
+
if (!project.packageManager)
|
|
118
|
+
return;
|
|
119
|
+
const command = INSTALL_COMMANDS[project.packageManager];
|
|
120
|
+
if (!command)
|
|
121
|
+
return;
|
|
122
|
+
console.log(` Installing moda-ai (${project.packageManager})...`);
|
|
123
|
+
try {
|
|
124
|
+
execSync(command, { cwd: process.cwd(), stdio: 'pipe' });
|
|
125
|
+
console.log(' Installed moda-ai');
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
129
|
+
console.error(` Warning: SDK install failed: ${message}`);
|
|
130
|
+
console.error(` Run manually: ${command}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function getInitSnippet(project) {
|
|
134
|
+
if (project.language === 'typescript') {
|
|
135
|
+
return `
|
|
136
|
+
import { Moda } from 'moda-ai';
|
|
137
|
+
|
|
138
|
+
await Moda.init(process.env.MODA_API_KEY!);
|
|
139
|
+
// Moda.conversationId = 'your-session-id';
|
|
140
|
+
// ... your LLM calls are auto-instrumented ...
|
|
141
|
+
await Moda.flush();
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
if (project.language === 'python') {
|
|
145
|
+
return `
|
|
146
|
+
import moda
|
|
147
|
+
|
|
148
|
+
moda.init() # reads MODA_API_KEY from env
|
|
149
|
+
# moda.conversation_id = 'your-session-id'
|
|
150
|
+
# ... your LLM calls are auto-instrumented ...
|
|
151
|
+
moda.flush()
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/init/index.ts — Full moda init wizard orchestrator
|
|
2
|
+
import { select } from '@inquirer/prompts';
|
|
3
|
+
import { login, loadToken } from '../auth.js';
|
|
4
|
+
import { selectTenantAndCreateKey } from './tenant.js';
|
|
5
|
+
import { detectProject, installSDK, getInitSnippet } from './detect.js';
|
|
6
|
+
import { detectAgents, configureMCP } from './mcp.js';
|
|
7
|
+
import { installRules } from './rules.js';
|
|
8
|
+
export async function runInit(flags) {
|
|
9
|
+
const yes = flags.yes === true || flags.y === true;
|
|
10
|
+
const skipSdk = flags.skip_sdk === true;
|
|
11
|
+
const skipMcp = flags.skip_mcp === true;
|
|
12
|
+
console.log('\n Moda — LLM Observability for Agents\n');
|
|
13
|
+
// Step 1: Authentication
|
|
14
|
+
console.log(' Step 1/5: Authentication');
|
|
15
|
+
let token = loadToken();
|
|
16
|
+
if (!token) {
|
|
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');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(' Already authenticated (cached token)');
|
|
30
|
+
}
|
|
31
|
+
// Step 2: Tenant + API Key
|
|
32
|
+
console.log('\n Step 2/5: Select Organization');
|
|
33
|
+
const { apiKey, tenantName } = await selectTenantAndCreateKey();
|
|
34
|
+
// Step 3: SDK
|
|
35
|
+
if (!skipSdk) {
|
|
36
|
+
console.log('\n Step 3/5: Install SDK');
|
|
37
|
+
const project = await detectProject();
|
|
38
|
+
if (project.language) {
|
|
39
|
+
const parts = [project.language];
|
|
40
|
+
if (project.packageManager)
|
|
41
|
+
parts.push(project.packageManager);
|
|
42
|
+
if (project.llmProviders.length > 0) {
|
|
43
|
+
parts.push(project.llmProviders.join(', '));
|
|
44
|
+
}
|
|
45
|
+
console.log(` Detected: ${parts.join(' / ')}`);
|
|
46
|
+
await installSDK(project);
|
|
47
|
+
const snippet = getInitSnippet(project);
|
|
48
|
+
if (snippet) {
|
|
49
|
+
console.log('\n Add this to your entry file:');
|
|
50
|
+
console.log(snippet);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(' No project detected, skipping SDK installation');
|
|
55
|
+
console.log(' Install manually: npm install moda-ai (or pip install moda-ai)');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log('\n Step 3/5: Install SDK (skipped)');
|
|
60
|
+
}
|
|
61
|
+
// Step 4: MCP
|
|
62
|
+
if (!skipMcp) {
|
|
63
|
+
console.log('\n Step 4/5: Configure MCP Server');
|
|
64
|
+
const agents = detectAgents();
|
|
65
|
+
if (agents.length > 0) {
|
|
66
|
+
console.log(` Detected: ${agents.map((a) => a.name).join(', ')}`);
|
|
67
|
+
const configured = await configureMCP(agents, apiKey, yes);
|
|
68
|
+
if (configured.length > 0) {
|
|
69
|
+
console.log(` Configured ${configured.length} agent(s): ${configured.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(' No coding agents detected');
|
|
74
|
+
console.log(' Manual setup: claude mcp add moda -- npx -y @moda-ai/cli');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log('\n Step 4/5: Configure MCP Server (skipped)');
|
|
79
|
+
}
|
|
80
|
+
// Step 5: Rules
|
|
81
|
+
console.log('\n Step 5/5: Agent Rules (optional)');
|
|
82
|
+
const agentNames = detectAgents().map((a) => a.name);
|
|
83
|
+
if (agentNames.length > 0) {
|
|
84
|
+
await installRules(agentNames, yes);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(' Skipped (no agents detected)');
|
|
88
|
+
}
|
|
89
|
+
// Done
|
|
90
|
+
console.log(`\n Setup complete for ${tenantName}!`);
|
|
91
|
+
console.log(' Dashboard: https://app.modaflows.com');
|
|
92
|
+
console.log('');
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface DetectedAgent {
|
|
2
|
+
name: string;
|
|
3
|
+
configPath: string;
|
|
4
|
+
configKey: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function detectAgents(): DetectedAgent[];
|
|
7
|
+
export declare function configureMCP(agents: DetectedAgent[], apiKey: string, autoYes?: boolean): Promise<string[]>;
|
|
8
|
+
//# sourceMappingURL=mcp.d.ts.map
|