@moda-ai/cli 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -54
- package/dist/auth.js +39 -67
- package/dist/cli.js +93 -12
- package/dist/config.d.ts +1 -2
- package/dist/config.js +1 -2
- package/dist/init/detect.d.ts +1 -1
- package/dist/init/detect.js +28 -5
- package/dist/init/index.js +26 -68
- package/dist/init/mcp.d.ts +1 -7
- package/dist/init/mcp.js +5 -88
- package/dist/init/rules.d.ts +1 -1
- package/dist/init/rules.js +151 -58
- package/dist/init/tenant.js +3 -41
- package/dist/schemas.d.ts +59 -0
- package/dist/schemas.js +35 -0
- package/package.json +6 -17
- package/skills/integrate-moda/SKILL.md +63 -0
- package/skills/moda-cli/SKILL.md +291 -0
- package/dist/__tests__/api-client.test.d.ts +0 -2
- package/dist/__tests__/api-client.test.js +0 -73
- package/dist/__tests__/cli.test.d.ts +0 -2
- package/dist/__tests__/cli.test.js +0 -109
- package/dist/__tests__/e2e.test.d.ts +0 -2
- package/dist/__tests__/e2e.test.js +0 -239
- package/dist/__tests__/handlers.test.d.ts +0 -2
- package/dist/__tests__/handlers.test.js +0 -71
- package/dist/__tests__/integration.test.d.ts +0 -2
- package/dist/__tests__/integration.test.js +0 -114
- package/dist/__tests__/schemas.test.d.ts +0 -2
- package/dist/__tests__/schemas.test.js +0 -81
- package/dist/handlers.d.ts +0 -7
- package/dist/handlers.js +0 -117
- package/dist/http-server.d.ts +0 -3
- package/dist/http-server.js +0 -74
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -26
- package/dist/tools.d.ts +0 -260
- package/dist/tools.js +0 -133
- package/dist/worker.d.ts +0 -9
- package/dist/worker.js +0 -24
- package/mcp-config.example.json +0 -12
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# moda-cli
|
|
2
2
|
|
|
3
|
-
CLI
|
|
3
|
+
CLI for [Moda](https://modaflows.com) -- AI agent analytics and observability.
|
|
4
4
|
|
|
5
|
-
Query your conversation analytics from the terminal
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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>${
|
|
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(`
|
|
89
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
82
90
|
return;
|
|
83
91
|
}
|
|
84
|
-
if (!
|
|
92
|
+
if (!token) {
|
|
85
93
|
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
86
|
-
res.end('<html><body><h2>Error</h2><p>No
|
|
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
|
|
97
|
+
reject(new Error('No token received'));
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 =
|
|
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
|
|
128
|
+
await open(authUrl);
|
|
157
129
|
}
|
|
158
130
|
catch {
|
|
159
|
-
console.error(` Open this URL in your browser:\n ${authUrl
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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');
|
package/dist/init/detect.d.ts
CHANGED
|
@@ -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<
|
|
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
|
package/dist/init/detect.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
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) {
|
package/dist/init/index.js
CHANGED
|
@@ -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 {
|
|
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/
|
|
11
|
+
console.log(' Step 1/3: Authentication');
|
|
15
12
|
let token = loadToken();
|
|
16
13
|
if (!token) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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/
|
|
33
|
-
const {
|
|
34
|
-
// Step 3: SDK
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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('
|
|
42
|
+
console.log(' No coding agents detected, skipping');
|
|
88
43
|
}
|
|
89
44
|
// Done
|
|
90
|
-
console.log(`\n Setup complete for ${tenantName}
|
|
91
|
-
console.log('
|
|
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
|
package/dist/init/mcp.d.ts
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
export
|
|
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
|