@moda-ai/cli 1.0.4 → 1.1.0

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 (45) hide show
  1. package/.env.example +2 -2
  2. package/README.md +52 -50
  3. package/dist/auth.js +39 -67
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +140 -14
  6. package/dist/config.d.ts +1 -2
  7. package/dist/config.js +3 -4
  8. package/dist/init/detect.d.ts +1 -1
  9. package/dist/init/detect.js +28 -5
  10. package/dist/init/index.js +26 -68
  11. package/dist/init/mcp.d.ts +1 -7
  12. package/dist/init/mcp.js +5 -88
  13. package/dist/init/rules.d.ts +1 -1
  14. package/dist/init/rules.js +151 -58
  15. package/dist/init/tenant.js +4 -42
  16. package/dist/schemas.d.ts +71 -0
  17. package/dist/schemas.js +44 -0
  18. package/dist/transforms.d.ts +25 -0
  19. package/dist/transforms.js +102 -0
  20. package/package.json +8 -19
  21. package/skills/integrate-moda/SKILL.md +63 -0
  22. package/skills/moda-cli/SKILL.md +323 -0
  23. package/dist/__tests__/api-client.test.d.ts +0 -2
  24. package/dist/__tests__/api-client.test.js +0 -73
  25. package/dist/__tests__/cli.test.d.ts +0 -2
  26. package/dist/__tests__/cli.test.js +0 -109
  27. package/dist/__tests__/e2e.test.d.ts +0 -2
  28. package/dist/__tests__/e2e.test.js +0 -239
  29. package/dist/__tests__/handlers.test.d.ts +0 -2
  30. package/dist/__tests__/handlers.test.js +0 -71
  31. package/dist/__tests__/integration.test.d.ts +0 -2
  32. package/dist/__tests__/integration.test.js +0 -114
  33. package/dist/__tests__/schemas.test.d.ts +0 -2
  34. package/dist/__tests__/schemas.test.js +0 -81
  35. package/dist/handlers.d.ts +0 -7
  36. package/dist/handlers.js +0 -117
  37. package/dist/http-server.d.ts +0 -3
  38. package/dist/http-server.js +0 -74
  39. package/dist/index.d.ts +0 -3
  40. package/dist/index.js +0 -26
  41. package/dist/tools.d.ts +0 -260
  42. package/dist/tools.js +0 -133
  43. package/dist/worker.d.ts +0 -9
  44. package/dist/worker.js +0 -24
  45. package/mcp-config.example.json +0 -12
package/.env.example CHANGED
@@ -1,8 +1,8 @@
1
1
  # Moda API Key (required)
2
- # Get this from https://app.modaflows.com/settings
2
+ # Get this from https://moda.dev/settings
3
3
  MODA_API_KEY=moda_sk_...
4
4
 
5
5
  # Moda Backend URL (optional)
6
- # Defaults to production: https://modaflows.com
6
+ # Defaults to production: https://moda.dev
7
7
  # For local development, override with:
8
8
  # MODA_BASE_URL=http://localhost:3001
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://moda.dev) -- 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 [moda.dev/settings](https://moda.dev/settings).
34
+
35
+ ## Usage
28
36
 
29
37
  ```bash
30
38
  moda overview # Dashboard overview
@@ -44,9 +52,20 @@ moda context <conversation_id> --msg-index=5 # Center on message 5
44
52
 
45
53
  moda frustrations # User frustration detections
46
54
  moda frustrations --days-back=14 --limit=20
55
+ moda frustrations --include-window --window=1 # Attach 3-message slice per row
47
56
 
48
57
  moda tool-failures # Tool failure overview
49
58
  moda tool-failure-detail <tool_name> # Per-tool failure detail
59
+ moda tool-failure-detail <tool_name> --include-window --window=1
60
+
61
+ # Agent runs runtime (workflows = recurring class, run = one execution)
62
+ moda workflows --time-range=7d # Recurring workflow catalog with health
63
+ moda runs # Last 7d of workflow executions
64
+ moda runs --time-range=24h --outcome=partial # Last 24h, partial completions only
65
+ moda runs --class-id=cron:helpscout-triage-hourly # Scoped to one workflow class
66
+ moda run <run_id> # One execution + its side-effect receipts
67
+ moda loops # Currently-active loop detections
68
+ moda loop <detection_id> # One loop detail (signature + blast radius)
50
69
  ```
51
70
 
52
71
  All commands output JSON. Pipe to `jq` for filtering:
@@ -54,70 +73,53 @@ All commands output JSON. Pipe to `jq` for filtering:
54
73
  ```bash
55
74
  moda overview | jq '.frustrations'
56
75
  moda frustrations | jq '.frustrations[].primary_cause'
76
+ moda runs --outcome=partial | jq '.runs[] | {job_id, intent_summary, outcome_reason}'
77
+ moda loops | jq '.loops | sort_by(-.cycle_visits) | .[0:5] | .[].loop_signature_human'
78
+ moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.health < 0.8)'
57
79
  ```
58
80
 
59
- ## MCP Server
60
-
61
- ### Claude Code
81
+ ## Anchors and message windows
62
82
 
63
- ```bash
64
- claude mcp add moda -- npx -y @moda-ai/cli
65
- ```
66
-
67
- Or add manually to your MCP config:
83
+ Rows returned by `moda frustrations` and `moda tool-failure-detail` carry
84
+ a top-level `anchor` block naming the exact turn the event fired on:
68
85
 
69
86
  ```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
- }
87
+ // frustrations
88
+ { "kind": "frustration", "conversation_id": "...", "msg_index": 12,
89
+ "signal": "exasperation", "quote_preview": "...", "all_turns": [...],
90
+ "no_anchor": false }
91
+
92
+ // tool-failure-detail examples
93
+ { "kind": "tool_failure", "conversation_id": "...", "msg_index": 7,
94
+ "tool_name": "search", "tool_use_id": "...",
95
+ "error_subtype": "SEARCH_NO_RESULTS", "no_anchor": false }
81
96
  ```
82
97
 
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
- ```
98
+ `msg_index` is the 0-indexed turn where the breakage happened. For
99
+ frustrations it's derived from `key_turns[0]` (the same turn the API centers
100
+ the embedded window on), falling back to `user_quotes[0].turn` when
101
+ `key_turns` is empty. `no_anchor: true` means no anchor could be derived for
102
+ that row.
94
103
 
95
- ### Available MCP Tools
104
+ Both commands accept two flags to inline a message window per row:
96
105
 
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 |
106
+ | Flag | Default | Description |
107
+ |------|---------|-------------|
108
+ | `--include-window` | off | Attach a `window` field with the message slice centered on the anchor |
109
+ | `--window=N` | `1` | Half-width (1–5). `--window=1` ⇒ 3 messages total (anchor ± 1) |
107
110
 
108
111
  ## Environment Variables
109
112
 
110
113
  | Variable | Required | Default | Description |
111
114
  |----------|----------|---------|-------------|
112
115
  | `MODA_API_KEY` | Yes | -- | Your Moda API key (`moda_sk_...`) |
113
- | `MODA_BASE_URL` | No | `https://modaflows.com` | Base URL for the Data API |
116
+ | `MODA_BASE_URL` | No | `https://moda.dev` | Base URL for the Data API |
114
117
 
115
118
  ## Documentation
116
119
 
117
- - [CLI Reference](https://docs.modaflows.com/data-api/cli)
118
- - [MCP Server Setup](https://docs.modaflows.com/data-api/mcp)
119
- - [Data API Overview](https://docs.modaflows.com/data-api/overview)
120
- - [Quickstart](https://docs.modaflows.com/quickstart)
120
+ - [CLI Reference](https://docs.moda.dev/data-api/cli)
121
+ - [Data API Overview](https://docs.moda.dev/data-api/overview)
122
+ - [Quickstart](https://docs.moda.dev/quickstart)
121
123
 
122
124
  ## License
123
125
 
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.d.ts CHANGED
@@ -6,5 +6,6 @@ interface ParsedArgs {
6
6
  }
7
7
  export declare function parseArgs(argv: string[]): ParsedArgs;
8
8
  export declare function flagsToArgs(flags: Record<string, string>, positionalKey?: string, positionalValue?: string): Record<string, unknown>;
9
+ export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>): Promise<void>;
9
10
  export {};
10
11
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
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
+ import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
6
7
  export function parseArgs(argv) {
7
8
  const args = argv.slice(2);
8
9
  const command = args[0] || '';
@@ -67,6 +68,13 @@ Commands:
67
68
  tool-failures Get tool failure overview
68
69
  tool-failure-detail <tool_name> Get per-tool failure detail
69
70
 
71
+ Agent runs runtime:
72
+ workflows Recurring workflow catalog (per-class 30d health)
73
+ runs List workflow executions with classified outcomes
74
+ run <run_id> Get single execution + side-effect receipts
75
+ loops Currently-active loop detections
76
+ loop <detection_id> Single loop detail
77
+
70
78
  Options:
71
79
  --help Show this help message
72
80
  --version Show version number
@@ -77,25 +85,74 @@ Common flags:
77
85
  --offset=N Number of results to skip
78
86
  --time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
79
87
 
88
+ Runs flags:
89
+ --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
90
+ --class-id=ID Filter to a single workflow class (e.g. cron:helpscout-triage-hourly)
91
+ --user-id=ID Filter to a single user
92
+
80
93
  Init flags:
81
94
  --yes Accept all defaults (non-interactive)
82
- --skip-sdk Skip SDK installation
83
- --skip-mcp Skip MCP server configuration
84
95
 
85
96
  Examples:
86
97
  moda init
87
- moda init --yes
88
- moda overview
89
98
  moda overview --days-back=30
90
99
  moda clusters --time-range=7d
91
100
  moda conversations --search="error" --limit=5
92
101
  moda frustrations --days-back=14 --limit=20
93
- moda context <conversation_id> --window=3`);
102
+ moda context <conversation_id> --window=3
103
+ moda workflows --time-range=7d
104
+ moda runs --time-range=24h --outcome=partial
105
+ moda runs --class-id=cron:helpscout-triage-hourly --limit=20
106
+ moda run 12345678-1234-1234-1234-1234567890ab
107
+ moda loops --limit=10
108
+ moda loop 12345678-1234-1234-1234-1234567890ab`);
94
109
  }
95
110
  // --- 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
- async function runCommand(command, positional, flags) {
111
+ async function attachWindows(rows, windowSize) {
112
+ return Promise.all(rows.map(async (row) => {
113
+ const anchor = asRecord(row.anchor);
114
+ if (!anchor || anchor.no_anchor === true) {
115
+ return { ...row, window: null };
116
+ }
117
+ const msg_index = asNumber(anchor.msg_index);
118
+ if (msg_index === null) {
119
+ return { ...row, window: null };
120
+ }
121
+ const conv_id = asString(anchor.conversation_id) ?? '';
122
+ if (!conv_id) {
123
+ return { ...row, window: null };
124
+ }
125
+ const conversation = asRecord(row.conversation);
126
+ // embedded window may be wider than requested (backend default = 2); reuse anything ≥ requested
127
+ const embedded = conversation ? embeddedWindowSize(conversation) : null;
128
+ if (conversation && embedded !== null && embedded >= windowSize) {
129
+ return { ...row, window: conversation };
130
+ }
131
+ try {
132
+ const query = new URLSearchParams();
133
+ query.set('msg_index', msg_index.toString());
134
+ query.set('window', windowSize.toString());
135
+ const data = await callDataAPI(`/conversations/${conv_id}/context?${query.toString()}`);
136
+ return { ...row, window: data };
137
+ }
138
+ catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ return { ...row, window: { error: message } };
141
+ }
142
+ }));
143
+ }
144
+ async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize) {
145
+ if (!includeWindow) {
146
+ console.log(JSON.stringify(enriched, null, 2));
147
+ return;
148
+ }
149
+ const list = Array.isArray(enriched[listKey])
150
+ ? enriched[listKey]
151
+ : [];
152
+ const withWindows = await attachWindows(list, windowSize);
153
+ console.log(JSON.stringify({ ...enriched, [listKey]: withWindows }, null, 2));
154
+ }
155
+ export async function runCommand(command, positional, flags) {
99
156
  switch (command) {
100
157
  case 'overview': {
101
158
  const args = flagsToArgs(flags);
@@ -189,7 +246,7 @@ async function runCommand(command, positional, flags) {
189
246
  query.set('offset', params.offset.toString());
190
247
  const queryString = query.toString() ? `?${query.toString()}` : '';
191
248
  const data = await callDataAPI(`/frustrations${queryString}`);
192
- console.log(JSON.stringify(data, null, 2));
249
+ await printWithOptionalWindows(withFrustrationAnchors(data), 'frustrations', params.include_window === true, params.window ?? 1);
193
250
  break;
194
251
  }
195
252
  case 'tool-failures': {
@@ -219,6 +276,76 @@ async function runCommand(command, positional, flags) {
219
276
  query.set('offset', params.offset.toString());
220
277
  const queryString = query.toString() ? `?${query.toString()}` : '';
221
278
  const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
279
+ await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1);
280
+ break;
281
+ }
282
+ // ─── Workflows + Runs ──────────────────────────────────────
283
+ case 'workflows': {
284
+ const args = flagsToArgs(flags);
285
+ const params = WorkflowsSchema.parse(args);
286
+ const query = new URLSearchParams();
287
+ if (params.time_range)
288
+ query.set('time_range', params.time_range);
289
+ if (params.user_id)
290
+ query.set('user_id', params.user_id);
291
+ const queryString = query.toString() ? `?${query.toString()}` : '';
292
+ const data = await callDataAPI(`/workflows${queryString}`);
293
+ console.log(JSON.stringify(data, null, 2));
294
+ break;
295
+ }
296
+ case 'runs': {
297
+ const args = flagsToArgs(flags);
298
+ const params = RunsSchema.parse(args);
299
+ const query = new URLSearchParams();
300
+ if (params.time_range)
301
+ query.set('time_range', params.time_range);
302
+ if (params.outcome)
303
+ query.set('outcome', params.outcome);
304
+ if (params.class_id)
305
+ query.set('class_id', params.class_id);
306
+ if (params.user_id)
307
+ query.set('user_id', params.user_id);
308
+ if (params.limit)
309
+ query.set('limit', params.limit.toString());
310
+ if (params.offset !== undefined)
311
+ query.set('offset', params.offset.toString());
312
+ const queryString = query.toString() ? `?${query.toString()}` : '';
313
+ const data = await callDataAPI(`/runs${queryString}`);
314
+ console.log(JSON.stringify(data, null, 2));
315
+ break;
316
+ }
317
+ case 'run': {
318
+ if (!positional) {
319
+ console.error('Error: <run_id> is required');
320
+ console.error('Usage: moda run <run_id>');
321
+ process.exit(1);
322
+ }
323
+ const args = flagsToArgs(flags, 'run_id', positional);
324
+ const params = RunDetailSchema.parse(args);
325
+ const data = await callDataAPI(`/runs/${params.run_id}`);
326
+ console.log(JSON.stringify(data, null, 2));
327
+ break;
328
+ }
329
+ case 'loops': {
330
+ const args = flagsToArgs(flags);
331
+ const params = LoopsSchema.parse(args);
332
+ const query = new URLSearchParams();
333
+ if (params.limit)
334
+ query.set('limit', params.limit.toString());
335
+ const queryString = query.toString() ? `?${query.toString()}` : '';
336
+ const data = await callDataAPI(`/loops/active${queryString}`);
337
+ console.log(JSON.stringify(data, null, 2));
338
+ break;
339
+ }
340
+ case 'loop': {
341
+ if (!positional) {
342
+ console.error('Error: <detection_id> is required');
343
+ console.error('Usage: moda loop <detection_id>');
344
+ process.exit(1);
345
+ }
346
+ const args = flagsToArgs(flags, 'detection_id', positional);
347
+ const params = LoopDetailSchema.parse(args);
348
+ const data = await callDataAPI(`/loops/${params.detection_id}`);
222
349
  console.log(JSON.stringify(data, null, 2));
223
350
  break;
224
351
  }
@@ -250,11 +377,10 @@ async function main() {
250
377
  await runCommand(command, positional, flags);
251
378
  }
252
379
  // 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
380
  import { fileURLToPath } from 'url';
257
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
381
+ import { realpathSync } from 'fs';
382
+ const isMain = process.argv[1] &&
383
+ fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
258
384
  if (isMain) {
259
385
  main().catch((error) => {
260
386
  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,12 +1,11 @@
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
- export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://modaflows.com';
5
- export const PORT = parseInt(process.env.PORT || '3003', 10);
4
+ export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://moda.dev';
6
5
  export function validateConfig() {
7
6
  if (!MODA_API_KEY) {
8
7
  console.error('Error: MODA_API_KEY environment variable is required');
9
- console.error('Get your API key from https://modaflows.com/settings');
8
+ console.error('Get your API key from https://moda.dev/settings');
10
9
  process.exit(1);
11
10
  }
12
11
  }
@@ -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) {