@moda-ai/cli 1.0.1 → 1.0.2

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 CHANGED
@@ -7,13 +7,13 @@ Query your conversation analytics from the terminal or connect AI assistants (Cl
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install -g moda-cli
10
+ npm install -g @moda-ai/cli
11
11
  ```
12
12
 
13
13
  Or use without installing:
14
14
 
15
15
  ```bash
16
- npx moda-cli overview
16
+ npx -p @moda-ai/cli moda overview
17
17
  ```
18
18
 
19
19
  ## Setup
@@ -61,7 +61,7 @@ moda frustrations | jq '.frustrations[].primary_cause'
61
61
  ### Claude Code
62
62
 
63
63
  ```bash
64
- claude mcp add moda -- npx moda-cli
64
+ claude mcp add moda -- npx -y @moda-ai/cli
65
65
  ```
66
66
 
67
67
  Or add manually to your MCP config:
@@ -71,7 +71,7 @@ Or add manually to your MCP config:
71
71
  "mcpServers": {
72
72
  "moda": {
73
73
  "command": "npx",
74
- "args": ["-y", "moda-cli"],
74
+ "args": ["-y", "@moda-ai/cli"],
75
75
  "env": {
76
76
  "MODA_API_KEY": "moda_sk_your_key_here"
77
77
  }
@@ -85,7 +85,7 @@ Or add manually to your MCP config:
85
85
  ```json
86
86
  {
87
87
  "command": "npx",
88
- "args": ["-y", "moda-cli"],
88
+ "args": ["-y", "@moda-ai/cli"],
89
89
  "env": {
90
90
  "MODA_API_KEY": "moda_sk_your_key_here"
91
91
  }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function loadToken(): string | null;
2
+ export declare function login(): Promise<string>;
3
+ export declare function authFetch(path: string, options?: RequestInit): Promise<Response>;
4
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,184 @@
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() {
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
+ try {
152
+ const open = (await import('open')).default;
153
+ await open(authUrl.toString());
154
+ }
155
+ catch {
156
+ console.error(` Open this URL in your browser:\n ${authUrl.toString()}`);
157
+ }
158
+ });
159
+ // Clean up on process exit
160
+ const cleanup = () => {
161
+ if (!settled) {
162
+ settled = true;
163
+ server.close();
164
+ }
165
+ };
166
+ process.on('SIGINT', cleanup);
167
+ process.on('SIGTERM', cleanup);
168
+ });
169
+ }
170
+ // --- Authenticated Fetch ---
171
+ export async function authFetch(path, options = {}) {
172
+ const token = loadToken();
173
+ if (!token) {
174
+ throw new Error('Not authenticated. Run "moda init" to log in.');
175
+ }
176
+ const url = `${MODA_BASE_URL}${path}`;
177
+ const headers = new Headers(options.headers);
178
+ headers.set('Authorization', `Bearer ${token}`);
179
+ if (!headers.has('Content-Type') && options.body) {
180
+ headers.set('Content-Type', 'application/json');
181
+ }
182
+ return fetch(url, { ...options, headers });
183
+ }
184
+ //# 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
  }
@@ -239,7 +254,9 @@ async function main() {
239
254
  // the path of the entry-point script. Comparing them lets tests import parseArgs /
240
255
  // flagsToArgs without triggering the CLI bootstrap.
241
256
  import { fileURLToPath } from 'url';
242
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
257
+ import { realpathSync } from 'fs';
258
+ const isMain = process.argv[1] &&
259
+ fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
243
260
  if (isMain) {
244
261
  main().catch((error) => {
245
262
  if (error instanceof z.ZodError) {
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "1.0.0";
1
+ export declare const VERSION = "1.0.2";
2
2
  export declare const MODA_API_KEY: string | undefined;
3
3
  export declare const MODA_BASE_URL: string;
4
4
  export declare const PORT: number;
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/config.ts
2
- export const VERSION = '1.0.0';
2
+ export const VERSION = '1.0.2';
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);
@@ -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,2 @@
1
+ export declare function runInit(flags: Record<string, unknown>): Promise<void>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,86 @@
1
+ // src/init/index.ts — Full moda init wizard orchestrator
2
+ import { login, loadToken } from '../auth.js';
3
+ import { selectTenantAndCreateKey } from './tenant.js';
4
+ import { detectProject, installSDK, getInitSnippet } from './detect.js';
5
+ import { detectAgents, configureMCP } from './mcp.js';
6
+ import { installRules } from './rules.js';
7
+ export async function runInit(flags) {
8
+ const yes = flags.yes === true || flags.y === true;
9
+ const skipSdk = flags.skip_sdk === true;
10
+ const skipMcp = flags.skip_mcp === true;
11
+ console.log('\n Moda — LLM Observability for Agents\n');
12
+ // Step 1: Authentication
13
+ console.log(' Step 1/5: Authentication');
14
+ let token = loadToken();
15
+ if (!token) {
16
+ console.log(' Opening browser for login...');
17
+ token = await login();
18
+ console.log(' Login successful');
19
+ }
20
+ else {
21
+ console.log(' Already authenticated (cached token)');
22
+ }
23
+ // Step 2: Tenant + API Key
24
+ console.log('\n Step 2/5: Select Organization');
25
+ const { apiKey, tenantName } = await selectTenantAndCreateKey();
26
+ // Step 3: SDK
27
+ if (!skipSdk) {
28
+ console.log('\n Step 3/5: Install SDK');
29
+ const project = await detectProject();
30
+ if (project.language) {
31
+ const parts = [project.language];
32
+ if (project.packageManager)
33
+ parts.push(project.packageManager);
34
+ if (project.llmProviders.length > 0) {
35
+ parts.push(project.llmProviders.join(', '));
36
+ }
37
+ console.log(` Detected: ${parts.join(' / ')}`);
38
+ await installSDK(project);
39
+ const snippet = getInitSnippet(project);
40
+ if (snippet) {
41
+ console.log('\n Add this to your entry file:');
42
+ console.log(snippet);
43
+ }
44
+ }
45
+ else {
46
+ console.log(' No project detected, skipping SDK installation');
47
+ console.log(' Install manually: npm install moda-ai (or pip install moda-ai)');
48
+ }
49
+ }
50
+ else {
51
+ console.log('\n Step 3/5: Install SDK (skipped)');
52
+ }
53
+ // Step 4: MCP
54
+ if (!skipMcp) {
55
+ console.log('\n Step 4/5: Configure MCP Server');
56
+ const agents = detectAgents();
57
+ if (agents.length > 0) {
58
+ console.log(` Detected: ${agents.map((a) => a.name).join(', ')}`);
59
+ const configured = await configureMCP(agents, apiKey, yes);
60
+ if (configured.length > 0) {
61
+ console.log(` Configured ${configured.length} agent(s): ${configured.join(', ')}`);
62
+ }
63
+ }
64
+ else {
65
+ console.log(' No coding agents detected');
66
+ console.log(' Manual setup: claude mcp add moda -- npx -y @moda-ai/cli');
67
+ }
68
+ }
69
+ else {
70
+ console.log('\n Step 4/5: Configure MCP Server (skipped)');
71
+ }
72
+ // Step 5: Rules
73
+ console.log('\n Step 5/5: Agent Rules (optional)');
74
+ const agentNames = detectAgents().map((a) => a.name);
75
+ if (agentNames.length > 0) {
76
+ await installRules(agentNames, yes);
77
+ }
78
+ else {
79
+ console.log(' Skipped (no agents detected)');
80
+ }
81
+ // Done
82
+ console.log(`\n Setup complete for ${tenantName}!`);
83
+ console.log(' Dashboard: https://app.modaflows.com');
84
+ console.log('');
85
+ }
86
+ //# 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
@@ -0,0 +1,110 @@
1
+ // src/init/mcp.ts — MCP server auto-configuration for coding agents
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ import { checkbox } from '@inquirer/prompts';
6
+ function isOnPath(cmd) {
7
+ try {
8
+ execSync(`which ${cmd}`, { stdio: 'pipe' });
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export function detectAgents() {
16
+ const cwd = process.cwd();
17
+ const agents = [];
18
+ // Claude Code
19
+ if (existsSync(join(cwd, '.claude')) || isOnPath('claude')) {
20
+ agents.push({
21
+ name: 'Claude Code',
22
+ configPath: join(cwd, '.claude', 'settings.local.json'),
23
+ configKey: 'mcpServers',
24
+ });
25
+ }
26
+ // Cursor
27
+ if (existsSync(join(cwd, '.cursor'))) {
28
+ agents.push({
29
+ name: 'Cursor',
30
+ configPath: join(cwd, '.cursor', 'mcp.json'),
31
+ configKey: 'mcpServers',
32
+ });
33
+ }
34
+ // VS Code
35
+ if (existsSync(join(cwd, '.vscode'))) {
36
+ agents.push({
37
+ name: 'VS Code',
38
+ configPath: join(cwd, '.vscode', 'mcp.json'),
39
+ configKey: 'servers',
40
+ });
41
+ }
42
+ return agents;
43
+ }
44
+ export async function configureMCP(agents, apiKey, autoYes = false) {
45
+ if (agents.length === 0)
46
+ return [];
47
+ let selected;
48
+ if (autoYes) {
49
+ selected = agents;
50
+ }
51
+ else {
52
+ const choices = agents.map((a) => ({
53
+ name: a.name,
54
+ value: a,
55
+ checked: true,
56
+ }));
57
+ selected = await checkbox({
58
+ message: 'Configure MCP server for:',
59
+ choices,
60
+ });
61
+ }
62
+ const configured = [];
63
+ for (const agent of selected) {
64
+ try {
65
+ writeAgentConfig(agent, apiKey);
66
+ configured.push(agent.name);
67
+ }
68
+ catch (err) {
69
+ const message = err instanceof Error ? err.message : String(err);
70
+ console.error(` Warning: Failed to configure ${agent.name}: ${message}`);
71
+ }
72
+ }
73
+ // Show manual instructions for skipped agents
74
+ const skipped = agents.filter((a) => !selected.includes(a));
75
+ if (skipped.length > 0) {
76
+ console.log('\n Manual setup for skipped agents:');
77
+ for (const agent of skipped) {
78
+ console.log(` ${agent.name}: Add moda server to ${agent.configPath}`);
79
+ }
80
+ }
81
+ return configured;
82
+ }
83
+ function writeAgentConfig(agent, apiKey) {
84
+ // Ensure directory exists
85
+ const dir = dirname(agent.configPath);
86
+ mkdirSync(dir, { recursive: true });
87
+ // Read existing config
88
+ let config = {};
89
+ if (existsSync(agent.configPath)) {
90
+ try {
91
+ config = JSON.parse(readFileSync(agent.configPath, 'utf8'));
92
+ }
93
+ catch {
94
+ config = {};
95
+ }
96
+ }
97
+ // Merge moda server entry
98
+ const servers = config[agent.configKey] || {};
99
+ servers['moda'] = {
100
+ command: 'npx',
101
+ args: ['-y', '@moda-ai/cli'],
102
+ env: {
103
+ MODA_API_KEY: apiKey,
104
+ },
105
+ };
106
+ config[agent.configKey] = servers;
107
+ // Write back
108
+ writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + '\n');
109
+ }
110
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,2 @@
1
+ export declare function installRules(detectedAgents: string[], autoYes?: boolean): Promise<void>;
2
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1,72 @@
1
+ // src/init/rules.ts — Agent rules file installation
2
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { confirm } from '@inquirer/prompts';
5
+ const MODA_RULES_CONTENT = `
6
+
7
+ ## Moda SDK
8
+
9
+ Moda is integrated in this project for LLM observability. The \`moda-ai\` SDK
10
+ auto-instruments OpenAI and Anthropic calls via OpenTelemetry.
11
+
12
+ ### Initialization
13
+
14
+ - TypeScript: \`import { Moda } from 'moda-ai'; await Moda.init(process.env.MODA_API_KEY!);\`
15
+ - Python: \`import moda; moda.init()\` (reads MODA_API_KEY from env)
16
+
17
+ ### Conversation Tracking
18
+
19
+ Set conversation ID before LLM calls to group multi-turn conversations:
20
+ - TypeScript: \`Moda.conversationId = 'session-id';\`
21
+ - Python: \`moda.conversation_id = 'session-id'\`
22
+
23
+ ### Before Exit
24
+
25
+ Flush pending traces before the process exits:
26
+ - TypeScript: \`await Moda.flush();\`
27
+ - Python: \`moda.flush()\`
28
+ `;
29
+ const AGENT_RULES_FILES = {
30
+ 'Claude Code': 'CLAUDE.md',
31
+ 'Cursor': '.cursor/rules/moda.mdc',
32
+ };
33
+ export async function installRules(detectedAgents, autoYes = false) {
34
+ const applicable = detectedAgents.filter((a) => a in AGENT_RULES_FILES);
35
+ if (applicable.length === 0) {
36
+ console.log(' No supported agents detected for rules files');
37
+ return;
38
+ }
39
+ let shouldInstall = autoYes;
40
+ if (!shouldInstall) {
41
+ shouldInstall = await confirm({
42
+ message: 'Install agent rules for coding assistants?',
43
+ default: true,
44
+ });
45
+ }
46
+ if (!shouldInstall)
47
+ return;
48
+ for (const agent of applicable) {
49
+ const filePath = join(process.cwd(), AGENT_RULES_FILES[agent]);
50
+ try {
51
+ const dir = dirname(filePath);
52
+ mkdirSync(dir, { recursive: true });
53
+ if (existsSync(filePath)) {
54
+ const content = readFileSync(filePath, 'utf8');
55
+ if (content.includes('## Moda SDK')) {
56
+ console.log(` ${agent}: Moda SDK section already exists in ${AGENT_RULES_FILES[agent]}`);
57
+ continue;
58
+ }
59
+ appendFileSync(filePath, MODA_RULES_CONTENT);
60
+ }
61
+ else {
62
+ writeFileSync(filePath, MODA_RULES_CONTENT.trimStart());
63
+ }
64
+ console.log(` Added Moda SDK reference to ${AGENT_RULES_FILES[agent]}`);
65
+ }
66
+ catch (err) {
67
+ const message = err instanceof Error ? err.message : String(err);
68
+ console.error(` Warning: Failed to write ${AGENT_RULES_FILES[agent]}: ${message}`);
69
+ }
70
+ }
71
+ }
72
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1,9 @@
1
+ interface BootstrapResult {
2
+ apiKey: string;
3
+ tenantId: string;
4
+ tenantName: string;
5
+ tenantSlug: string;
6
+ }
7
+ export declare function selectTenantAndCreateKey(): Promise<BootstrapResult>;
8
+ export {};
9
+ //# sourceMappingURL=tenant.d.ts.map
@@ -0,0 +1,91 @@
1
+ // src/init/tenant.ts — Tenant selection and API key provisioning
2
+ import { select } from '@inquirer/prompts';
3
+ import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { authFetch } from '../auth.js';
6
+ export async function selectTenantAndCreateKey() {
7
+ // 1. Fetch CLI context
8
+ const contextRes = await authFetch('/api/cli/context');
9
+ if (!contextRes.ok) {
10
+ const body = await contextRes.text();
11
+ throw new Error(`Failed to fetch context (${contextRes.status}): ${body}`);
12
+ }
13
+ const ctx = (await contextRes.json());
14
+ console.log(` Authenticated as ${ctx.user.email}`);
15
+ // 2. Select tenant
16
+ if (ctx.tenants.length === 0) {
17
+ console.error(' No organizations found. Create one at https://app.modaflows.com');
18
+ process.exit(1);
19
+ }
20
+ let tenantId;
21
+ if (ctx.tenants.length === 1) {
22
+ tenantId = ctx.tenants[0].id;
23
+ console.log(` Organization: ${ctx.tenants[0].name}`);
24
+ }
25
+ else {
26
+ tenantId = await select({
27
+ message: 'Select organization',
28
+ choices: ctx.tenants.map((t) => ({
29
+ name: `${t.name} (${t.role})`,
30
+ value: t.id,
31
+ })),
32
+ });
33
+ }
34
+ // 3. Create API key via bootstrap endpoint
35
+ const bootstrapRes = await authFetch('/api/cli/bootstrap', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ tenantId }),
39
+ });
40
+ if (bootstrapRes.status === 403) {
41
+ console.error(' Error: You need admin or owner access to create API keys.');
42
+ console.error(' Ask your organization owner to upgrade your role.');
43
+ process.exit(1);
44
+ }
45
+ if (!bootstrapRes.ok) {
46
+ const body = await bootstrapRes.text();
47
+ throw new Error(`Failed to create API key (${bootstrapRes.status}): ${body}`);
48
+ }
49
+ const result = (await bootstrapRes.json());
50
+ // 4. Write .env
51
+ writeEnvFile(result.apiKey);
52
+ // 5. Update .gitignore
53
+ ensureGitignore();
54
+ console.log(' API key created and saved to .env');
55
+ return result;
56
+ }
57
+ function writeEnvFile(apiKey) {
58
+ const envPath = join(process.cwd(), '.env');
59
+ const line = `MODA_API_KEY=${apiKey}`;
60
+ if (existsSync(envPath)) {
61
+ const content = readFileSync(envPath, 'utf8');
62
+ if (content.includes('MODA_API_KEY=')) {
63
+ // Replace existing line
64
+ const updated = content.replace(/^MODA_API_KEY=.*$/m, line);
65
+ writeFileSync(envPath, updated);
66
+ }
67
+ else {
68
+ // Append
69
+ const separator = content.endsWith('\n') ? '' : '\n';
70
+ appendFileSync(envPath, `${separator}${line}\n`);
71
+ }
72
+ }
73
+ else {
74
+ writeFileSync(envPath, `${line}\n`);
75
+ }
76
+ }
77
+ function ensureGitignore() {
78
+ const gitignorePath = join(process.cwd(), '.gitignore');
79
+ if (existsSync(gitignorePath)) {
80
+ const content = readFileSync(gitignorePath, 'utf8');
81
+ const lines = content.split('\n').map((l) => l.trim());
82
+ if (!lines.includes('.env')) {
83
+ const separator = content.endsWith('\n') ? '' : '\n';
84
+ appendFileSync(gitignorePath, `${separator}.env\n`);
85
+ }
86
+ }
87
+ else {
88
+ writeFileSync(gitignorePath, '.env\n');
89
+ }
90
+ }
91
+ //# sourceMappingURL=tenant.js.map
package/dist/schemas.d.ts CHANGED
@@ -38,18 +38,18 @@ export declare const ConversationsSchema: z.ZodObject<{
38
38
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
39
39
  offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
40
40
  }, "strip", z.ZodTypeAny, {
41
+ search?: string | undefined;
41
42
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
42
43
  limit?: number | undefined;
43
44
  offset?: number | undefined;
44
- search?: string | undefined;
45
45
  cluster_id?: string | undefined;
46
46
  user_id?: string | undefined;
47
47
  environment?: "all" | "development" | "staging" | "production" | undefined;
48
48
  }, {
49
+ search?: string | undefined;
49
50
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
50
51
  limit?: number | undefined;
51
52
  offset?: number | undefined;
52
- search?: string | undefined;
53
53
  cluster_id?: string | undefined;
54
54
  user_id?: string | undefined;
55
55
  environment?: "all" | "development" | "staging" | "production" | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "CLI and MCP server for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,9 +52,11 @@
52
52
  "url": "https://github.com/ModaLabs/moda-cli/issues"
53
53
  },
54
54
  "dependencies": {
55
+ "@inquirer/prompts": "^8.4.1",
55
56
  "@modelcontextprotocol/sdk": "^1.0.4",
56
57
  "cors": "^2.8.5",
57
58
  "express": "^4.21.2",
59
+ "open": "^11.0.0",
58
60
  "zod": "^3.24.1"
59
61
  },
60
62
  "devDependencies": {