@moda-ai/cli 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +24 -54
  2. package/dist/auth.js +39 -67
  3. package/dist/cli.js +93 -12
  4. package/dist/config.d.ts +1 -2
  5. package/dist/config.js +1 -2
  6. package/dist/init/detect.d.ts +1 -1
  7. package/dist/init/detect.js +28 -5
  8. package/dist/init/index.js +26 -68
  9. package/dist/init/mcp.d.ts +1 -7
  10. package/dist/init/mcp.js +5 -88
  11. package/dist/init/rules.d.ts +1 -1
  12. package/dist/init/rules.js +151 -58
  13. package/dist/init/tenant.js +3 -41
  14. package/dist/schemas.d.ts +59 -0
  15. package/dist/schemas.js +35 -0
  16. package/package.json +7 -17
  17. package/skills/integrate-moda/SKILL.md +63 -0
  18. package/skills/moda-cli/SKILL.md +291 -0
  19. package/dist/__tests__/api-client.test.d.ts +0 -2
  20. package/dist/__tests__/api-client.test.js +0 -73
  21. package/dist/__tests__/cli.test.d.ts +0 -2
  22. package/dist/__tests__/cli.test.js +0 -109
  23. package/dist/__tests__/e2e.test.d.ts +0 -2
  24. package/dist/__tests__/e2e.test.js +0 -239
  25. package/dist/__tests__/handlers.test.d.ts +0 -2
  26. package/dist/__tests__/handlers.test.js +0 -71
  27. package/dist/__tests__/integration.test.d.ts +0 -2
  28. package/dist/__tests__/integration.test.js +0 -114
  29. package/dist/__tests__/schemas.test.d.ts +0 -2
  30. package/dist/__tests__/schemas.test.js +0 -81
  31. package/dist/handlers.d.ts +0 -7
  32. package/dist/handlers.js +0 -117
  33. package/dist/http-server.d.ts +0 -3
  34. package/dist/http-server.js +0 -74
  35. package/dist/index.d.ts +0 -3
  36. package/dist/index.js +0 -26
  37. package/dist/tools.d.ts +0 -260
  38. package/dist/tools.js +0 -133
  39. package/dist/worker.d.ts +0 -9
  40. package/dist/worker.js +0 -24
  41. package/mcp-config.example.json +0 -12
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # moda-cli
2
2
 
3
- CLI and MCP server for [Moda](https://modaflows.com) -- AI agent analytics and observability.
3
+ CLI for [Moda](https://modaflows.com) -- AI agent analytics and observability.
4
4
 
5
- Query your conversation analytics from the terminal or connect AI assistants (Claude Code, Cursor, etc.) to your Moda data via the Model Context Protocol.
5
+ Query your conversation analytics from the terminal.
6
6
 
7
7
  ## Install
8
8
 
@@ -18,13 +18,21 @@ npx -p @moda-ai/cli moda overview
18
18
 
19
19
  ## Setup
20
20
 
21
- Get your API key from [modaflows.com/settings](https://modaflows.com/settings), then:
21
+ Run the wizard to authenticate, pick an org, and provision an API key:
22
+
23
+ ```bash
24
+ moda init
25
+ ```
26
+
27
+ Or set the key manually:
22
28
 
23
29
  ```bash
24
30
  export MODA_API_KEY="moda_sk_..."
25
31
  ```
26
32
 
27
- ## CLI Usage
33
+ Get your API key from [modaflows.com/settings](https://modaflows.com/settings).
34
+
35
+ ## Usage
28
36
 
29
37
  ```bash
30
38
  moda overview # Dashboard overview
@@ -47,6 +55,15 @@ moda frustrations --days-back=14 --limit=20
47
55
 
48
56
  moda tool-failures # Tool failure overview
49
57
  moda tool-failure-detail <tool_name> # Per-tool failure detail
58
+
59
+ # Agent runs runtime (workflows = recurring class, run = one execution)
60
+ moda workflows --time-range=7d # Recurring workflow catalog with health
61
+ moda runs # Last 7d of workflow executions
62
+ moda runs --time-range=24h --outcome=partial # Last 24h, partial completions only
63
+ moda runs --class-id=cron:helpscout-triage-hourly # Scoped to one workflow class
64
+ moda run <run_id> # One execution + its side-effect receipts
65
+ moda loops # Currently-active loop detections
66
+ moda loop <detection_id> # One loop detail (signature + blast radius)
50
67
  ```
51
68
 
52
69
  All commands output JSON. Pipe to `jq` for filtering:
@@ -54,57 +71,11 @@ All commands output JSON. Pipe to `jq` for filtering:
54
71
  ```bash
55
72
  moda overview | jq '.frustrations'
56
73
  moda frustrations | jq '.frustrations[].primary_cause'
74
+ moda runs --outcome=partial | jq '.runs[] | {job_id, intent_summary, outcome_reason}'
75
+ moda loops | jq '.loops | sort_by(-.cycle_visits) | .[0:5] | .[].loop_signature_human'
76
+ moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.health < 0.8)'
57
77
  ```
58
78
 
59
- ## MCP Server
60
-
61
- ### Claude Code
62
-
63
- ```bash
64
- claude mcp add moda -- npx -y @moda-ai/cli
65
- ```
66
-
67
- Or add manually to your MCP config:
68
-
69
- ```json
70
- {
71
- "mcpServers": {
72
- "moda": {
73
- "command": "npx",
74
- "args": ["-y", "@moda-ai/cli"],
75
- "env": {
76
- "MODA_API_KEY": "moda_sk_your_key_here"
77
- }
78
- }
79
- }
80
- }
81
- ```
82
-
83
- ### Other MCP Clients (Cursor, etc.)
84
-
85
- ```json
86
- {
87
- "command": "npx",
88
- "args": ["-y", "@moda-ai/cli"],
89
- "env": {
90
- "MODA_API_KEY": "moda_sk_your_key_here"
91
- }
92
- }
93
- ```
94
-
95
- ### Available MCP Tools
96
-
97
- | Tool | Description |
98
- |------|-------------|
99
- | `moda_get_overview` | Dashboard overview with KPIs, top clusters, recent activity |
100
- | `moda_get_clusters` | Browse topic cluster hierarchy |
101
- | `moda_get_cluster_conversations` | List conversations in a specific cluster |
102
- | `moda_search_conversations` | Search and filter conversations |
103
- | `moda_get_conversation_context` | Get windowed conversation context (max 5 messages) |
104
- | `moda_get_frustrations` | User frustration detections with evidence |
105
- | `moda_get_tool_failures` | Tool failure overview |
106
- | `moda_get_tool_failure_detail` | Per-tool failure detail with examples |
107
-
108
79
  ## Environment Variables
109
80
 
110
81
  | Variable | Required | Default | Description |
@@ -115,7 +86,6 @@ Or add manually to your MCP config:
115
86
  ## Documentation
116
87
 
117
88
  - [CLI Reference](https://docs.modaflows.com/data-api/cli)
118
- - [MCP Server Setup](https://docs.modaflows.com/data-api/mcp)
119
89
  - [Data API Overview](https://docs.modaflows.com/data-api/overview)
120
90
  - [Quickstart](https://docs.modaflows.com/quickstart)
121
91
 
package/dist/auth.js CHANGED
@@ -1,24 +1,13 @@
1
- // src/auth.ts — Browser-based PKCE authentication for moda init
2
- import { createHash, randomBytes } from 'node:crypto';
1
+ // src/auth.ts — Browser-based authentication for moda init via frontend
3
2
  import { createServer } from 'node:http';
4
3
  import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
5
4
  import { join } from 'node:path';
6
5
  import { homedir } from 'node:os';
7
6
  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
7
  const CALLBACK_PORT = 8432;
12
8
  const AUTH_TIMEOUT_MS = 120_000;
13
9
  const MODA_DIR = join(homedir(), '.moda');
14
10
  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
11
  export function loadToken() {
23
12
  try {
24
13
  if (!existsSync(TOKEN_PATH))
@@ -56,71 +45,64 @@ const SUCCESS_HTML = `<!DOCTYPE html>
56
45
  </div>
57
46
  <script>setTimeout(()=>window.close(),2000)</script>
58
47
  </body></html>`;
48
+ // Page served at /callback that extracts token from URL fragment and
49
+ // forwards it to /callback-token as query params (fragments never reach the server).
50
+ const FRAGMENT_EXTRACTOR_HTML = `<!DOCTYPE html>
51
+ <html><body>
52
+ <script>
53
+ var h = window.location.hash.substring(1);
54
+ if (h) {
55
+ window.location.replace('/callback-token?' + h);
56
+ } else {
57
+ document.body.innerText = 'No token received.';
58
+ }
59
+ </script>
60
+ </body></html>`;
59
61
  export async function login(screen = 'login') {
60
- const verifier = generateVerifier();
61
- const challenge = generateChallenge(verifier);
62
62
  return new Promise((resolve, reject) => {
63
63
  let settled = false;
64
64
  const server = createServer(async (req, res) => {
65
65
  if (settled)
66
66
  return;
67
67
  const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
68
- if (url.pathname !== '/callback') {
68
+ // Serve fragment extractor at /callback — the frontend redirects here
69
+ // with token in the URL fragment (#token=...) which never reaches the server.
70
+ // This page extracts it client-side and forwards to /callback-token as query params.
71
+ if (url.pathname === '/callback') {
72
+ res.writeHead(200, { 'Content-Type': 'text/html' });
73
+ res.end(FRAGMENT_EXTRACTOR_HTML);
74
+ return;
75
+ }
76
+ if (url.pathname !== '/callback-token') {
69
77
  res.writeHead(404);
70
78
  res.end('Not found');
71
79
  return;
72
80
  }
73
- const code = url.searchParams.get('code');
81
+ const token = url.searchParams.get('token');
82
+ const expiresIn = url.searchParams.get('expires_in');
74
83
  const error = url.searchParams.get('error');
75
- const errorDescription = url.searchParams.get('error_description');
76
84
  if (error) {
77
85
  res.writeHead(400, { 'Content-Type': 'text/html' });
78
- res.end(`<html><body><h2>Error</h2><p>${errorDescription || error}</p></body></html>`);
86
+ res.end(`<html><body><h2>Error</h2><p>${error}</p></body></html>`);
79
87
  settled = true;
80
88
  server.close();
81
- reject(new Error(`Auth0 error: ${errorDescription || error}`));
89
+ reject(new Error(`Authentication error: ${error}`));
82
90
  return;
83
91
  }
84
- if (!code) {
92
+ if (!token) {
85
93
  res.writeHead(400, { 'Content-Type': 'text/html' });
86
- res.end('<html><body><h2>Error</h2><p>No authorization code received.</p></body></html>');
94
+ res.end('<html><body><h2>Error</h2><p>No token received.</p></body></html>');
87
95
  settled = true;
88
96
  server.close();
89
- reject(new Error('No authorization code received'));
97
+ reject(new Error('No token received'));
90
98
  return;
91
99
  }
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
- }
100
+ saveToken(token, parseInt(expiresIn || '86400', 10));
101
+ res.writeHead(200, { 'Content-Type': 'text/html' });
102
+ res.end(SUCCESS_HTML);
103
+ settled = true;
104
+ server.close();
105
+ resolve(token);
124
106
  });
125
107
  // Timeout
126
108
  const timeout = setTimeout(() => {
@@ -140,23 +122,13 @@ export async function login(screen = 'login') {
140
122
  }
141
123
  });
142
124
  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
- }
125
+ const authUrl = `${MODA_BASE_URL}/auth/cli?port=${CALLBACK_PORT}&screen=${screen}`;
154
126
  try {
155
127
  const open = (await import('open')).default;
156
- await open(authUrl.toString());
128
+ await open(authUrl);
157
129
  }
158
130
  catch {
159
- console.error(` Open this URL in your browser:\n ${authUrl.toString()}`);
131
+ console.error(` Open this URL in your browser:\n ${authUrl}`);
160
132
  }
161
133
  });
162
134
  // Clean up on process exit
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { validateConfig, VERSION } from './config.js';
4
4
  import { callDataAPI, ApiError } from './api-client.js';
5
- import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from './schemas.js';
5
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
6
6
  export function parseArgs(argv) {
7
7
  const args = argv.slice(2);
8
8
  const command = args[0] || '';
@@ -67,6 +67,13 @@ Commands:
67
67
  tool-failures Get tool failure overview
68
68
  tool-failure-detail <tool_name> Get per-tool failure detail
69
69
 
70
+ Agent runs runtime:
71
+ workflows Recurring workflow catalog (per-class 30d health)
72
+ runs List workflow executions with classified outcomes
73
+ run <run_id> Get single execution + side-effect receipts
74
+ loops Currently-active loop detections
75
+ loop <detection_id> Single loop detail
76
+
70
77
  Options:
71
78
  --help Show this help message
72
79
  --version Show version number
@@ -77,24 +84,29 @@ Common flags:
77
84
  --offset=N Number of results to skip
78
85
  --time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
79
86
 
87
+ Runs flags:
88
+ --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
89
+ --class-id=ID Filter to a single workflow class (e.g. cron:helpscout-triage-hourly)
90
+ --user-id=ID Filter to a single user
91
+
80
92
  Init flags:
81
93
  --yes Accept all defaults (non-interactive)
82
- --skip-sdk Skip SDK installation
83
- --skip-mcp Skip MCP server configuration
84
94
 
85
95
  Examples:
86
96
  moda init
87
- moda init --yes
88
- moda overview
89
97
  moda overview --days-back=30
90
98
  moda clusters --time-range=7d
91
99
  moda conversations --search="error" --limit=5
92
100
  moda frustrations --days-back=14 --limit=20
93
- moda context <conversation_id> --window=3`);
101
+ moda context <conversation_id> --window=3
102
+ moda workflows --time-range=7d
103
+ moda runs --time-range=24h --outcome=partial
104
+ moda runs --class-id=cron:helpscout-triage-hourly --limit=20
105
+ moda run 12345678-1234-1234-1234-1234567890ab
106
+ moda loops --limit=10
107
+ moda loop 12345678-1234-1234-1234-1234567890ab`);
94
108
  }
95
109
  // --- Command Handlers ---
96
- // Each handler replicates the URL construction from src/handlers.ts
97
- // but outputs JSON directly instead of wrapping in MCP envelope.
98
110
  async function runCommand(command, positional, flags) {
99
111
  switch (command) {
100
112
  case 'overview': {
@@ -222,6 +234,76 @@ async function runCommand(command, positional, flags) {
222
234
  console.log(JSON.stringify(data, null, 2));
223
235
  break;
224
236
  }
237
+ // ─── Workflows + Runs ──────────────────────────────────────
238
+ case 'workflows': {
239
+ const args = flagsToArgs(flags);
240
+ const params = WorkflowsSchema.parse(args);
241
+ const query = new URLSearchParams();
242
+ if (params.time_range)
243
+ query.set('time_range', params.time_range);
244
+ if (params.user_id)
245
+ query.set('user_id', params.user_id);
246
+ const queryString = query.toString() ? `?${query.toString()}` : '';
247
+ const data = await callDataAPI(`/workflows${queryString}`);
248
+ console.log(JSON.stringify(data, null, 2));
249
+ break;
250
+ }
251
+ case 'runs': {
252
+ const args = flagsToArgs(flags);
253
+ const params = RunsSchema.parse(args);
254
+ const query = new URLSearchParams();
255
+ if (params.time_range)
256
+ query.set('time_range', params.time_range);
257
+ if (params.outcome)
258
+ query.set('outcome', params.outcome);
259
+ if (params.class_id)
260
+ query.set('class_id', params.class_id);
261
+ if (params.user_id)
262
+ query.set('user_id', params.user_id);
263
+ if (params.limit)
264
+ query.set('limit', params.limit.toString());
265
+ if (params.offset !== undefined)
266
+ query.set('offset', params.offset.toString());
267
+ const queryString = query.toString() ? `?${query.toString()}` : '';
268
+ const data = await callDataAPI(`/runs${queryString}`);
269
+ console.log(JSON.stringify(data, null, 2));
270
+ break;
271
+ }
272
+ case 'run': {
273
+ if (!positional) {
274
+ console.error('Error: <run_id> is required');
275
+ console.error('Usage: moda run <run_id>');
276
+ process.exit(1);
277
+ }
278
+ const args = flagsToArgs(flags, 'run_id', positional);
279
+ const params = RunDetailSchema.parse(args);
280
+ const data = await callDataAPI(`/runs/${params.run_id}`);
281
+ console.log(JSON.stringify(data, null, 2));
282
+ break;
283
+ }
284
+ case 'loops': {
285
+ const args = flagsToArgs(flags);
286
+ const params = LoopsSchema.parse(args);
287
+ const query = new URLSearchParams();
288
+ if (params.limit)
289
+ query.set('limit', params.limit.toString());
290
+ const queryString = query.toString() ? `?${query.toString()}` : '';
291
+ const data = await callDataAPI(`/loops/active${queryString}`);
292
+ console.log(JSON.stringify(data, null, 2));
293
+ break;
294
+ }
295
+ case 'loop': {
296
+ if (!positional) {
297
+ console.error('Error: <detection_id> is required');
298
+ console.error('Usage: moda loop <detection_id>');
299
+ process.exit(1);
300
+ }
301
+ const args = flagsToArgs(flags, 'detection_id', positional);
302
+ const params = LoopDetailSchema.parse(args);
303
+ const data = await callDataAPI(`/loops/${params.detection_id}`);
304
+ console.log(JSON.stringify(data, null, 2));
305
+ break;
306
+ }
225
307
  default:
226
308
  console.error(`Error: Unknown command '${command}'`);
227
309
  console.error('Run "moda --help" for usage information.');
@@ -250,11 +332,10 @@ async function main() {
250
332
  await runCommand(command, positional, flags);
251
333
  }
252
334
  // Only run main() when this file is executed directly (not when imported as a module).
253
- // In ESM, import.meta.url is the file URL of the current module; process.argv[1] is
254
- // the path of the entry-point script. Comparing them lets tests import parseArgs /
255
- // flagsToArgs without triggering the CLI bootstrap.
256
335
  import { fileURLToPath } from 'url';
257
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
336
+ import { realpathSync } from 'fs';
337
+ const isMain = process.argv[1] &&
338
+ fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
258
339
  if (isMain) {
259
340
  main().catch((error) => {
260
341
  if (error instanceof z.ZodError) {
package/dist/config.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- export declare const VERSION = "1.0.0";
1
+ export declare const VERSION = "1.0.4";
2
2
  export declare const MODA_API_KEY: string | undefined;
3
3
  export declare const MODA_BASE_URL: string;
4
- export declare const PORT: number;
5
4
  export declare function validateConfig(): void;
6
5
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -1,8 +1,7 @@
1
1
  // src/config.ts
2
- export const VERSION = '1.0.0';
2
+ export const VERSION = '1.0.4';
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
- export const PORT = parseInt(process.env.PORT || '3003', 10);
6
5
  export function validateConfig() {
7
6
  if (!MODA_API_KEY) {
8
7
  console.error('Error: MODA_API_KEY environment variable is required');
@@ -4,6 +4,6 @@ export interface ProjectInfo {
4
4
  llmProviders: string[];
5
5
  }
6
6
  export declare function detectProject(): Promise<ProjectInfo>;
7
- export declare function installSDK(project: ProjectInfo): Promise<void>;
7
+ export declare function installSDK(project: ProjectInfo): Promise<boolean>;
8
8
  export declare function getInitSnippet(project: ProjectInfo): string;
9
9
  //# sourceMappingURL=detect.d.ts.map
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { execSync } from 'node:child_process';
5
- import { select } from '@inquirer/prompts';
5
+ import { select, confirm } from '@inquirer/prompts';
6
6
  const LOCKFILE_TO_PM = {
7
7
  'package-lock.json': 'npm',
8
8
  'yarn.lock': 'yarn',
@@ -59,8 +59,21 @@ export async function detectProject() {
59
59
  break;
60
60
  }
61
61
  }
62
- if (!packageManager)
63
- packageManager = 'npm';
62
+ // No lockfile — detect which package manager is actually installed
63
+ if (!packageManager) {
64
+ for (const pm of ['bun', 'pnpm', 'yarn', 'npm']) {
65
+ try {
66
+ execSync(`${pm} --version`, { stdio: 'pipe' });
67
+ packageManager = pm;
68
+ break;
69
+ }
70
+ catch {
71
+ // not installed
72
+ }
73
+ }
74
+ if (!packageManager)
75
+ packageManager = 'npm';
76
+ }
64
77
  }
65
78
  else if (language === 'python') {
66
79
  if (existsSync(join(cwd, 'poetry.lock'))) {
@@ -115,19 +128,29 @@ function detectLLMProviders(cwd, language) {
115
128
  }
116
129
  export async function installSDK(project) {
117
130
  if (!project.packageManager)
118
- return;
131
+ return false;
119
132
  const command = INSTALL_COMMANDS[project.packageManager];
120
133
  if (!command)
121
- return;
134
+ return false;
135
+ const shouldInstall = await confirm({
136
+ message: `Install moda-ai SDK? (${command})`,
137
+ default: true,
138
+ });
139
+ if (!shouldInstall) {
140
+ console.log(` Skipped. Run manually: ${command}`);
141
+ return false;
142
+ }
122
143
  console.log(` Installing moda-ai (${project.packageManager})...`);
123
144
  try {
124
145
  execSync(command, { cwd: process.cwd(), stdio: 'pipe' });
125
146
  console.log(' Installed moda-ai');
147
+ return true;
126
148
  }
127
149
  catch (err) {
128
150
  const message = err instanceof Error ? err.message : String(err);
129
151
  console.error(` Warning: SDK install failed: ${message}`);
130
152
  console.error(` Run manually: ${command}`);
153
+ return false;
131
154
  }
132
155
  }
133
156
  export function getInitSnippet(project) {
@@ -2,25 +2,25 @@
2
2
  import { select } from '@inquirer/prompts';
3
3
  import { login, loadToken } from '../auth.js';
4
4
  import { selectTenantAndCreateKey } from './tenant.js';
5
- import { detectProject, installSDK, getInitSnippet } from './detect.js';
6
- import { detectAgents, configureMCP } from './mcp.js';
5
+ import { detectAgents } from './mcp.js';
7
6
  import { installRules } from './rules.js';
8
7
  export async function runInit(flags) {
9
8
  const yes = flags.yes === true || flags.y === true;
10
- const skipSdk = flags.skip_sdk === true;
11
- const skipMcp = flags.skip_mcp === true;
12
9
  console.log('\n Moda — LLM Observability for Agents\n');
13
10
  // Step 1: Authentication
14
- console.log(' Step 1/5: Authentication');
11
+ console.log(' Step 1/3: Authentication');
15
12
  let token = loadToken();
16
13
  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
- });
14
+ let action = 'login';
15
+ if (!yes) {
16
+ action = await select({
17
+ message: 'Get started',
18
+ choices: [
19
+ { name: 'Sign up', value: 'signup' },
20
+ { name: 'Login', value: 'login' },
21
+ ],
22
+ });
23
+ }
24
24
  console.log(` Opening browser to ${action === 'signup' ? 'sign up' : 'log in'}...`);
25
25
  token = await login(action);
26
26
  console.log(' Authentication successful');
@@ -29,66 +29,24 @@ export async function runInit(flags) {
29
29
  console.log(' Already authenticated (cached token)');
30
30
  }
31
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);
32
+ console.log('\n Step 2/3: Select Organization');
33
+ const { tenantName } = await selectTenantAndCreateKey();
34
+ // Step 3: Install SDK integration guide for coding agents
35
+ console.log('\n Step 3/3: Install SDK Guide');
36
+ const agents = detectAgents();
37
+ if (agents.length > 0) {
38
+ console.log(` Detected: ${agents.join(', ')}`);
39
+ await installRules(agents);
85
40
  }
86
41
  else {
87
- console.log(' Skipped (no agents detected)');
42
+ console.log(' No coding agents detected, skipping');
88
43
  }
89
44
  // Done
90
- console.log(`\n Setup complete for ${tenantName}!`);
91
- console.log(' Dashboard: https://app.modaflows.com');
45
+ console.log(`\n Setup complete for ${tenantName}!\n`);
46
+ console.log(' Now paste this into your coding agent to integrate Moda:\n');
47
+ console.log(' Integrate the Moda SDK into this project. Follow the Moda SDK');
48
+ console.log(' integration guide in the project rules.\n');
49
+ console.log(' Dashboard: https://modaflows.com');
92
50
  console.log('');
93
51
  }
94
52
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,2 @@
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[]>;
1
+ export declare function detectAgents(): string[];
8
2
  //# sourceMappingURL=mcp.d.ts.map