@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 +5 -5
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +184 -0
- package/dist/cli.js +18 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- 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 +86 -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 +3 -1
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
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
|
-
|
|
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
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
|
-
export const VERSION = '1.0.
|
|
2
|
+
export const VERSION = '1.0.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,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
|
package/dist/init/mcp.js
ADDED
|
@@ -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,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,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.
|
|
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": {
|