@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.
- package/.env.example +2 -2
- package/README.md +52 -50
- package/dist/auth.js +39 -67
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +140 -14
- package/dist/config.d.ts +1 -2
- package/dist/config.js +3 -4
- 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 +4 -42
- package/dist/schemas.d.ts +71 -0
- package/dist/schemas.js +44 -0
- package/dist/transforms.d.ts +25 -0
- package/dist/transforms.js +102 -0
- package/package.json +8 -19
- package/skills/integrate-moda/SKILL.md +63 -0
- package/skills/moda-cli/SKILL.md +323 -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/.env.example
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Moda API Key (required)
|
|
2
|
-
# Get this from https://
|
|
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://
|
|
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
|
|
3
|
+
CLI for [Moda](https://moda.dev) -- 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 [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
|
-
##
|
|
60
|
-
|
|
61
|
-
### Claude Code
|
|
81
|
+
## Anchors and message windows
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
104
|
+
Both commands accept two flags to inline a message window per row:
|
|
96
105
|
|
|
97
|
-
|
|
|
98
|
-
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
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://
|
|
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.
|
|
118
|
-
- [
|
|
119
|
-
- [
|
|
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
|
|
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.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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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://
|
|
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://
|
|
8
|
+
console.error('Get your API key from https://moda.dev/settings');
|
|
10
9
|
process.exit(1);
|
|
11
10
|
}
|
|
12
11
|
}
|
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) {
|