@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/dist/schemas.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/schemas.ts
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
// CLI flags arrive as strings; this coerces 'true'/'false' to booleans
|
|
4
|
+
// at parse time so individual schemas can declare boolean fields cleanly.
|
|
5
|
+
const boolFlag = () => z
|
|
6
|
+
.preprocess((val) => (val === 'true' ? true : val === 'false' ? false : val), z.boolean())
|
|
7
|
+
.optional();
|
|
3
8
|
export const OverviewSchema = z.object({
|
|
4
9
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
5
10
|
});
|
|
@@ -33,6 +38,8 @@ export const FrustrationsSchema = z.object({
|
|
|
33
38
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
34
39
|
limit: z.number().min(1).max(20).default(10).optional(),
|
|
35
40
|
offset: z.number().min(0).default(0).optional(),
|
|
41
|
+
include_window: boolFlag(),
|
|
42
|
+
window: z.number().min(1).max(5).default(1).optional(),
|
|
36
43
|
});
|
|
37
44
|
export const ToolFailuresSchema = z.object({
|
|
38
45
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
@@ -44,5 +51,42 @@ export const ToolFailureDetailSchema = z.object({
|
|
|
44
51
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
45
52
|
limit: z.number().min(1).max(20).default(5).optional(),
|
|
46
53
|
offset: z.number().min(0).default(0).optional(),
|
|
54
|
+
include_window: boolFlag(),
|
|
55
|
+
window: z.number().min(1).max(5).default(1).optional(),
|
|
56
|
+
});
|
|
57
|
+
// ─── Workflows + Runs (agent runs runtime) ──────────────────
|
|
58
|
+
//
|
|
59
|
+
// Naming model:
|
|
60
|
+
// workflow = the recurring class definition (e.g. cron:helpscout-triage-hourly)
|
|
61
|
+
// run = one execution of a workflow
|
|
62
|
+
// `moda workflows` lists the catalog; `moda runs` lists executions.
|
|
63
|
+
const RUN_OUTCOMES = ['running', 'succeeded', 'partial', 'failed', 'loop', 'abandoned', 'unknown'];
|
|
64
|
+
const TIME_RANGES = ['all', '1h', '3d', '7d', '24h', '30d', '90d'];
|
|
65
|
+
/** moda workflows — recurring workflow catalog (read from agent_job_classes). */
|
|
66
|
+
export const WorkflowsSchema = z.object({
|
|
67
|
+
time_range: z.enum(TIME_RANGES).default('all').optional(),
|
|
68
|
+
user_id: z.string().optional(),
|
|
69
|
+
});
|
|
70
|
+
/** moda runs — list workflow executions with classified outcomes. */
|
|
71
|
+
export const RunsSchema = z.object({
|
|
72
|
+
time_range: z.enum(TIME_RANGES).default('7d').optional(),
|
|
73
|
+
outcome: z.enum(RUN_OUTCOMES).optional(),
|
|
74
|
+
/** Filter to one workflow class id (e.g. cron:helpscout-triage-hourly). */
|
|
75
|
+
class_id: z.string().optional(),
|
|
76
|
+
user_id: z.string().optional(),
|
|
77
|
+
limit: z.number().min(1).max(100).default(50).optional(),
|
|
78
|
+
offset: z.number().min(0).default(0).optional(),
|
|
79
|
+
});
|
|
80
|
+
/** moda run <runId> — single execution detail with side-effect receipts. */
|
|
81
|
+
export const RunDetailSchema = z.object({
|
|
82
|
+
run_id: z.string().uuid({ message: 'run_id must be a UUID' }),
|
|
83
|
+
});
|
|
84
|
+
/** moda loops — currently active loop detections. */
|
|
85
|
+
export const LoopsSchema = z.object({
|
|
86
|
+
limit: z.number().min(1).max(200).default(25).optional(),
|
|
87
|
+
});
|
|
88
|
+
/** moda loop <detectionId> — single loop detection detail. */
|
|
89
|
+
export const LoopDetailSchema = z.object({
|
|
90
|
+
detection_id: z.string().uuid({ message: 'detection_id must be a UUID' }),
|
|
47
91
|
});
|
|
48
92
|
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FrustrationAnchor {
|
|
2
|
+
kind: 'frustration';
|
|
3
|
+
conversation_id: string;
|
|
4
|
+
msg_index: number | null;
|
|
5
|
+
signal: string | null;
|
|
6
|
+
quote_preview: string | null;
|
|
7
|
+
all_turns: number[];
|
|
8
|
+
no_anchor: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolFailureAnchor {
|
|
11
|
+
kind: 'tool_failure';
|
|
12
|
+
conversation_id: string;
|
|
13
|
+
msg_index: number | null;
|
|
14
|
+
tool_name: string;
|
|
15
|
+
tool_use_id: string | null;
|
|
16
|
+
error_subtype: string | null;
|
|
17
|
+
no_anchor: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
20
|
+
export declare function asString(value: unknown): string | null;
|
|
21
|
+
export declare function asNumber(value: unknown): number | null;
|
|
22
|
+
export declare function withFrustrationAnchors(response: Record<string, unknown>): Record<string, unknown>;
|
|
23
|
+
export declare function withToolFailureAnchors(response: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
export declare function embeddedWindowSize(conversation: Record<string, unknown> | null | undefined): number | null;
|
|
25
|
+
//# sourceMappingURL=transforms.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export function asRecord(value) {
|
|
2
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
export function asString(value) {
|
|
8
|
+
return typeof value === 'string' ? value : null;
|
|
9
|
+
}
|
|
10
|
+
export function asNumber(value) {
|
|
11
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
12
|
+
}
|
|
13
|
+
function buildFrustrationAnchor(row) {
|
|
14
|
+
const conversation_id = asString(row.conversation_id) ?? '';
|
|
15
|
+
const userQuotes = Array.isArray(row.user_quotes) ? row.user_quotes : [];
|
|
16
|
+
const firstQuote = asRecord(userQuotes[0]);
|
|
17
|
+
const keyTurnsRaw = Array.isArray(row.key_turns) ? row.key_turns : [];
|
|
18
|
+
const all_turns = keyTurnsRaw.filter((t) => typeof t === 'number');
|
|
19
|
+
// Prefer key_turns[0] because the backend Data API centers the embedded
|
|
20
|
+
// windowed conversation on it; anchor must match that center so
|
|
21
|
+
// --include-window stays consistent.
|
|
22
|
+
let msg_index = null;
|
|
23
|
+
if (all_turns.length > 0) {
|
|
24
|
+
msg_index = all_turns[0];
|
|
25
|
+
}
|
|
26
|
+
if (msg_index === null && firstQuote) {
|
|
27
|
+
const turn = asNumber(firstQuote.turn);
|
|
28
|
+
if (turn !== null)
|
|
29
|
+
msg_index = turn;
|
|
30
|
+
}
|
|
31
|
+
let signal = null;
|
|
32
|
+
let quote_preview = null;
|
|
33
|
+
if (firstQuote) {
|
|
34
|
+
signal = asString(firstQuote.signal);
|
|
35
|
+
const quote = asString(firstQuote.quote);
|
|
36
|
+
if (quote !== null)
|
|
37
|
+
quote_preview = quote.slice(0, 200);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
kind: 'frustration',
|
|
41
|
+
conversation_id,
|
|
42
|
+
msg_index,
|
|
43
|
+
signal,
|
|
44
|
+
quote_preview,
|
|
45
|
+
all_turns,
|
|
46
|
+
no_anchor: msg_index === null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function buildToolFailureAnchor(row, toolName) {
|
|
50
|
+
const conversation_id = asString(row.conversation_id) ?? '';
|
|
51
|
+
const msg_index = asNumber(row.msg_index);
|
|
52
|
+
const tool_use_id = asString(row.tool_use_id);
|
|
53
|
+
const error_subtype = asString(row.error_subtype);
|
|
54
|
+
return {
|
|
55
|
+
kind: 'tool_failure',
|
|
56
|
+
conversation_id,
|
|
57
|
+
msg_index,
|
|
58
|
+
tool_name: toolName,
|
|
59
|
+
tool_use_id,
|
|
60
|
+
error_subtype,
|
|
61
|
+
no_anchor: msg_index === null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function withFrustrationAnchors(response) {
|
|
65
|
+
if (!Array.isArray(response.frustrations))
|
|
66
|
+
return response;
|
|
67
|
+
const frustrations = response.frustrations.map((item) => {
|
|
68
|
+
const row = asRecord(item);
|
|
69
|
+
if (!row)
|
|
70
|
+
return item;
|
|
71
|
+
const anchor = buildFrustrationAnchor(row);
|
|
72
|
+
return { ...row, anchor };
|
|
73
|
+
});
|
|
74
|
+
return { ...response, frustrations };
|
|
75
|
+
}
|
|
76
|
+
export function withToolFailureAnchors(response) {
|
|
77
|
+
if (!Array.isArray(response.examples))
|
|
78
|
+
return response;
|
|
79
|
+
const toolName = asString(response.tool_name) ?? '';
|
|
80
|
+
const examples = response.examples.map((item) => {
|
|
81
|
+
const row = asRecord(item);
|
|
82
|
+
if (!row)
|
|
83
|
+
return item;
|
|
84
|
+
const anchor = buildToolFailureAnchor(row, toolName);
|
|
85
|
+
return { ...row, anchor };
|
|
86
|
+
});
|
|
87
|
+
return { ...response, examples };
|
|
88
|
+
}
|
|
89
|
+
export function embeddedWindowSize(conversation) {
|
|
90
|
+
if (!conversation)
|
|
91
|
+
return null;
|
|
92
|
+
const ctx = asRecord(conversation.context);
|
|
93
|
+
if (!ctx)
|
|
94
|
+
return null;
|
|
95
|
+
const center = asNumber(ctx.center_index);
|
|
96
|
+
const from = asNumber(ctx.from_index);
|
|
97
|
+
const to = asNumber(ctx.to_index);
|
|
98
|
+
if (center === null || from === null || to === null)
|
|
99
|
+
return null;
|
|
100
|
+
return Math.min(center - from, to - center);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=transforms.js.map
|
package/package.json
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moda-ai/cli",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "CLI for Moda - AI agent analytics and observability",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
6
|
"bin": {
|
|
8
7
|
"cli": "./dist/cli.js",
|
|
9
|
-
"moda": "./dist/cli.js"
|
|
10
|
-
"moda-mcp": "./dist/index.js"
|
|
8
|
+
"moda": "./dist/cli.js"
|
|
11
9
|
},
|
|
12
10
|
"files": [
|
|
13
11
|
"dist/**/*.js",
|
|
14
12
|
"dist/**/*.d.ts",
|
|
15
13
|
"!dist/__tests__",
|
|
14
|
+
"skills/**/SKILL.md",
|
|
16
15
|
"README.md",
|
|
17
|
-
".env.example"
|
|
18
|
-
"mcp-config.example.json"
|
|
16
|
+
".env.example"
|
|
19
17
|
],
|
|
20
18
|
"publishConfig": {
|
|
21
19
|
"access": "public"
|
|
@@ -24,13 +22,10 @@
|
|
|
24
22
|
"build": "tsc",
|
|
25
23
|
"prepublishOnly": "tsc",
|
|
26
24
|
"dev": "tsc --watch",
|
|
27
|
-
"start": "node dist/index.js",
|
|
28
|
-
"start:http": "node dist/http-server.js",
|
|
29
25
|
"typecheck": "tsc --noEmit",
|
|
30
26
|
"test": "vitest run",
|
|
31
27
|
"test:ci": "vitest run",
|
|
32
|
-
"test:watch": "vitest"
|
|
33
|
-
"test:e2e": "npm run build && vitest run e2e"
|
|
28
|
+
"test:watch": "vitest"
|
|
34
29
|
},
|
|
35
30
|
"repository": {
|
|
36
31
|
"type": "git",
|
|
@@ -40,28 +35,22 @@
|
|
|
40
35
|
"moda",
|
|
41
36
|
"llm",
|
|
42
37
|
"observability",
|
|
43
|
-
"mcp",
|
|
44
38
|
"cli",
|
|
45
39
|
"analytics",
|
|
46
40
|
"ai-agents"
|
|
47
41
|
],
|
|
48
|
-
"author": "Moda <hello@
|
|
42
|
+
"author": "Moda <hello@moda.dev>",
|
|
49
43
|
"license": "MIT",
|
|
50
|
-
"homepage": "https://docs.
|
|
44
|
+
"homepage": "https://docs.moda.dev/data-api/cli",
|
|
51
45
|
"bugs": {
|
|
52
46
|
"url": "https://github.com/ModaLabs/moda-cli/issues"
|
|
53
47
|
},
|
|
54
48
|
"dependencies": {
|
|
55
49
|
"@inquirer/prompts": "^8.4.1",
|
|
56
|
-
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
57
|
-
"cors": "^2.8.5",
|
|
58
|
-
"express": "^4.21.2",
|
|
59
50
|
"open": "^11.0.0",
|
|
60
51
|
"zod": "^3.24.1"
|
|
61
52
|
},
|
|
62
53
|
"devDependencies": {
|
|
63
|
-
"@types/cors": "^2.8.19",
|
|
64
|
-
"@types/express": "^5.0.6",
|
|
65
54
|
"@types/node": "^22.10.5",
|
|
66
55
|
"typescript": "^5.7.2",
|
|
67
56
|
"vitest": "^3.0.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integrate-moda
|
|
3
|
+
description: Integrate the Moda SDK into this project for LLM observability. Use when the user asks to add Moda, set up observability, or instrument LLM calls.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Integrate Moda SDK
|
|
7
|
+
|
|
8
|
+
Moda auto-instruments LLM calls (OpenAI, Anthropic) via OpenTelemetry.
|
|
9
|
+
After `Moda.init()`, all LLM calls are captured automatically — no wrappers needed.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. Install the SDK: `npm install moda-ai` (or bun/yarn/pnpm equivalent)
|
|
14
|
+
2. Ensure `MODA_API_KEY` is set in the environment
|
|
15
|
+
3. Initialize once at app startup, before any LLM client is created:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Moda } from 'moda-ai';
|
|
19
|
+
await Moda.init(process.env.MODA_API_KEY!);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Python: `import moda; moda.init()`
|
|
23
|
+
|
|
24
|
+
4. Set conversation ID before LLM calls to group multi-turn conversations.
|
|
25
|
+
Without this, each call appears as a separate conversation in the dashboard.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
Moda.conversationId = sessionId;
|
|
29
|
+
Moda.userId = userId; // optional but recommended
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For concurrent requests, use scoped context:
|
|
33
|
+
```typescript
|
|
34
|
+
import { withConversationId } from 'moda-ai';
|
|
35
|
+
await withConversationId(sessionId, async () => {
|
|
36
|
+
await openai.chat.completions.create({ ... });
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Python: `moda.conversation_id = session_id` or `with moda.set_conversation_id(session_id):`
|
|
41
|
+
|
|
42
|
+
5. Flush pending traces before process exit: `await Moda.flush();`
|
|
43
|
+
|
|
44
|
+
## Vercel AI SDK
|
|
45
|
+
|
|
46
|
+
When using the `ai` package, auto-instrumentation does not cover it. Pass telemetry config:
|
|
47
|
+
```typescript
|
|
48
|
+
const result = await generateText({
|
|
49
|
+
model: openai('gpt-4o'),
|
|
50
|
+
prompt: 'Hello',
|
|
51
|
+
experimental_telemetry: Moda.getVercelAITelemetry(),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Rules
|
|
56
|
+
|
|
57
|
+
- Initialize Moda before creating any LLM client instances
|
|
58
|
+
- Place `Moda.init()` in the app entry point, not in individual route handlers
|
|
59
|
+
- Set `conversationId` per request/session, not globally
|
|
60
|
+
- Use `withConversationId` in server environments handling concurrent requests
|
|
61
|
+
- Call `Moda.flush()` in graceful shutdown handlers and before `process.exit()`
|
|
62
|
+
- Do not wrap LLM calls — auto-instrumentation handles OpenAI and Anthropic automatically
|
|
63
|
+
- Do not import or configure OpenTelemetry manually — Moda manages its own provider
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: moda-cli
|
|
3
|
+
description: Query Moda's AI agent conversation analytics from the terminal — overview KPIs, topic clusters, conversation search, message context, user frustration detections, tool failures, agent jobs (per-run outcomes), recurring workflows, and active loop detections. Use when the user asks about moda, modaflows, conversation analytics, user frustrations, agent observability, tool failure debugging, why an agent run partially completed or looped, or wants to investigate how their AI agent is performing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Moda CLI
|
|
7
|
+
|
|
8
|
+
## What this skill does
|
|
9
|
+
|
|
10
|
+
Wraps the `moda` CLI so the agent can query a Moda tenant's conversation
|
|
11
|
+
analytics directly. Every command returns JSON on stdout; pipe to `jq` to
|
|
12
|
+
filter. Read-only — no command in this skill modifies data.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
Activate this skill when the user wants to:
|
|
17
|
+
|
|
18
|
+
- Inspect agent health: conversation volume, frustration rate, tool failures
|
|
19
|
+
- Debug a specific frustrated conversation
|
|
20
|
+
- Investigate which tools are failing and why
|
|
21
|
+
- Browse conversation topics (clusters)
|
|
22
|
+
- Search conversations by content, user, environment, or time range
|
|
23
|
+
|
|
24
|
+
**Do not** activate for: writing code that calls the Moda Data API directly
|
|
25
|
+
(use the API docs instead), instrumenting an app with the Moda SDK (different
|
|
26
|
+
skill / docs), or any modification action — this CLI is read-only.
|
|
27
|
+
|
|
28
|
+
## Required inputs
|
|
29
|
+
|
|
30
|
+
Before running any command except `moda init`:
|
|
31
|
+
|
|
32
|
+
- `MODA_API_KEY` env var must be set (`moda_sk_...`). If missing, run
|
|
33
|
+
`moda init` to provision one, or have the user export it.
|
|
34
|
+
|
|
35
|
+
Optional:
|
|
36
|
+
|
|
37
|
+
- `MODA_BASE_URL` defaults to `https://moda.dev`; override only for
|
|
38
|
+
self-hosted or staging.
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
### First-time install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g @moda-ai/cli
|
|
46
|
+
moda init # browser-based auth + key provisioning
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or one-shot via npx without installing:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx -p @moda-ai/cli moda <command>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Manual key
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Procedure
|
|
62
|
+
|
|
63
|
+
Default investigation pattern: **broad → narrow → context**. Always start with
|
|
64
|
+
`moda overview` to ground subsequent calls in real numbers.
|
|
65
|
+
|
|
66
|
+
### 1. Overview (always start here)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
moda overview # last 7 days
|
|
70
|
+
moda overview --days-back=30
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Returns total conversations, trend, frustration rate, tool failure summary,
|
|
74
|
+
top clusters, recent activity.
|
|
75
|
+
|
|
76
|
+
### 2. Browse topic clusters
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
moda clusters # root-level
|
|
80
|
+
moda clusters --parent-id=<node_id> # drill in
|
|
81
|
+
moda cluster-conversations <node_id> # conversations in cluster
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Search conversations
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Filters: `--search`, `--cluster-id`, `--user-id`, `--time-range`
|
|
91
|
+
(`all|1h|3d|7d|24h|30d|90d`), `--environment`
|
|
92
|
+
(`all|development|staging|production`), `--limit`, `--offset`.
|
|
93
|
+
|
|
94
|
+
### 4. Read a single conversation
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
moda context <conversation_id> # default window around middle
|
|
98
|
+
moda context <conversation_id> --msg-index=5 # center on message 5
|
|
99
|
+
moda context <conversation_id> --window=3 # 3 messages each side (max 5)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 5. Frustration analysis
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
moda frustrations # last 7 days
|
|
106
|
+
moda frustrations --days-back=14 --limit=20
|
|
107
|
+
moda frustrations --include-window --window=1 --limit=5
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each result includes inline conversation snippet, user quotes, trajectory,
|
|
111
|
+
signal breakdown (exasperation, profanity, anger, sarcasm, giving_up, insult),
|
|
112
|
+
and primary cause.
|
|
113
|
+
|
|
114
|
+
Every row carries a top-level `anchor` block:
|
|
115
|
+
`{ kind: 'frustration', conversation_id, msg_index, signal, quote_preview,
|
|
116
|
+
all_turns, no_anchor }`. `msg_index` is the 0-indexed turn where the
|
|
117
|
+
frustration fired, derived from `key_turns[0]` (the same turn the API centers
|
|
118
|
+
the embedded window on), falling back to `user_quotes[0].turn` when
|
|
119
|
+
`key_turns` is empty — reference `anchor.conversation_id` and
|
|
120
|
+
`anchor.msg_index` directly instead of digging into `user_quotes` to locate
|
|
121
|
+
the moment.
|
|
122
|
+
`no_anchor: true` means no anchor could be derived.
|
|
123
|
+
|
|
124
|
+
Pass `--include-window` to attach a `window` field per row with the message
|
|
125
|
+
slice centered on the anchor. `--window=N` (1–5, default `1`) is the
|
|
126
|
+
half-width; `--window=1` ⇒ 3 messages total (anchor ± 1), the canonical
|
|
127
|
+
"three lines" view.
|
|
128
|
+
|
|
129
|
+
### 6. Tool failure investigation
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
moda tool-failures # which tools are breaking
|
|
133
|
+
moda tool-failure-detail <tool_name> # subtype breakdown + examples
|
|
134
|
+
moda tool-failure-detail search --subtype=SEARCH_NO_RESULTS --limit=10
|
|
135
|
+
moda tool-failure-detail search --include-window --window=1 --limit=5
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Every example row in `tool-failure-detail` output carries a top-level
|
|
139
|
+
`anchor` block:
|
|
140
|
+
`{ kind: 'tool_failure', conversation_id, msg_index, tool_name, tool_use_id,
|
|
141
|
+
error_subtype, no_anchor }`. `msg_index` is the 0-indexed turn of the failing
|
|
142
|
+
tool call — reference `anchor.conversation_id` and `anchor.msg_index`
|
|
143
|
+
directly instead of matching by `tool_use_id` against the conversation.
|
|
144
|
+
`no_anchor: true` means no anchor could be derived.
|
|
145
|
+
|
|
146
|
+
Pass `--include-window` to attach a `window` field per row with the message
|
|
147
|
+
slice centered on the anchor. `--window=N` (1–5, default `1`) is the
|
|
148
|
+
half-width; `--window=1` ⇒ 3 messages total (anchor ± 1).
|
|
149
|
+
|
|
150
|
+
### 7. Workflow runs and outcomes
|
|
151
|
+
|
|
152
|
+
Every triggered run of an agent (cron invocation, scheduled task, or
|
|
153
|
+
interactive session) is classified `succeeded | partial | failed | loop |
|
|
154
|
+
abandoned`. Use this layer to answer "did the job actually do its work?",
|
|
155
|
+
which is structurally different from "did it error?" — most jobs that
|
|
156
|
+
partially completed never threw.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
moda runs # last 7d, all outcomes
|
|
160
|
+
moda runs --time-range=24h --outcome=partial # which runs only half-finished
|
|
161
|
+
moda runs --outcome=loop # runs caught in active loops
|
|
162
|
+
moda runs --class-id=cron:helpscout-triage-hourly --limit=20
|
|
163
|
+
moda run <run_id> # one execution + its side-effect receipts
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`moda run <run_id>` returns the `agent_jobs` row plus every
|
|
167
|
+
`side_effect_receipts` entry for the run — i.e. every tool call the agent
|
|
168
|
+
made (read / write / send / spawn / compute), with success status and
|
|
169
|
+
target system. This is the agent-as-employee log; use it instead of (or
|
|
170
|
+
alongside) `moda context` when debugging "what did the agent actually
|
|
171
|
+
DO".
|
|
172
|
+
|
|
173
|
+
### 8. Recurring workflows / cron status board
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
moda workflows # health per recurring class
|
|
177
|
+
moda workflows --time-range=7d # filter to last 7d of activity
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Each row is one job class (a cron slug, idea-widget id, or sourceType)
|
|
181
|
+
with 30d rolling success / partial / failed / loop counts and a
|
|
182
|
+
`health_score` (0–1). Sort by health to find degraded customer-facing
|
|
183
|
+
crons that aren't crashing but aren't completing either.
|
|
184
|
+
|
|
185
|
+
### 9. Active loop investigation
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
moda loops # everything currently stuck
|
|
189
|
+
moda loops --limit=10
|
|
190
|
+
moda loop <detection_id> # one loop signature in detail
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Loops are detected in real time by the conversation worker (tier 0/1) and
|
|
194
|
+
the analytics container (tier 2). A row with `cycle_visits=378` means the
|
|
195
|
+
agent re-ran the same action 378 times. Use the `loop_signature_human`
|
|
196
|
+
field to recognize cross-conversation patterns (one tool definition bug
|
|
197
|
+
will produce identical signatures across many users).
|
|
198
|
+
|
|
199
|
+
## Common workflow recipes
|
|
200
|
+
|
|
201
|
+
### Daily health check
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
moda overview --days-back=1
|
|
205
|
+
moda frustrations --days-back=1 --limit=5
|
|
206
|
+
moda tool-failures --days-back=1
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Debug a frustrated user
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
moda frustrations --days-back=7 --limit=10
|
|
213
|
+
# Pick a conversation_id from output
|
|
214
|
+
moda context <conversation_id>
|
|
215
|
+
moda context <conversation_id> --msg-index=<turn> # zoom on the bad turn
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Diagnose a flaky tool
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
moda tool-failures
|
|
222
|
+
moda tool-failure-detail <tool_name>
|
|
223
|
+
# Pick an example conversation_id from the output
|
|
224
|
+
moda context <conversation_id>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Explore what users are doing
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
moda clusters
|
|
231
|
+
moda clusters --parent-id=<node_id>
|
|
232
|
+
moda cluster-conversations <node_id>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Find degraded crons before customers do
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Health-sorted workflow board
|
|
239
|
+
moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.runs >= 4 and .health < 0.85)'
|
|
240
|
+
|
|
241
|
+
# What loops are eating tokens right now?
|
|
242
|
+
moda loops --limit=20 | jq '.loops[] | {sig: .loop_signature_human, visits: .cycle_visits, conv: .conversation_id}'
|
|
243
|
+
|
|
244
|
+
# Drill into a partial run
|
|
245
|
+
moda runs --outcome=partial --limit=5 | jq -r '.runs[].job_id' | head -1 | xargs -I{} moda run {}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### One-liner chains with jq
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Pull primary causes of recent frustrations
|
|
252
|
+
moda frustrations --days-back=7 | jq -r '.frustrations[].primary_cause'
|
|
253
|
+
|
|
254
|
+
# Get conversation IDs for a search and fetch context for each
|
|
255
|
+
for id in $(moda conversations --search="timeout" --limit=3 | jq -r '.conversations[].id'); do
|
|
256
|
+
moda context "$id"
|
|
257
|
+
done
|
|
258
|
+
|
|
259
|
+
# Tool failure leaderboard
|
|
260
|
+
moda tool-failures | jq '.tools[] | {tool: .tool_name, failures: .failure_count}'
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Validation — how to know it worked
|
|
264
|
+
|
|
265
|
+
- Every successful command exits 0 and prints valid JSON to stdout.
|
|
266
|
+
- Errors print to stderr with non-zero exit. Check stderr before claiming
|
|
267
|
+
success.
|
|
268
|
+
- If the JSON has `"conversations": []` or `"frustrations": []`, the call
|
|
269
|
+
worked but the time window is empty — widen `--days-back` before
|
|
270
|
+
concluding "no data".
|
|
271
|
+
|
|
272
|
+
## Troubleshooting
|
|
273
|
+
|
|
274
|
+
**`Error: MODA_API_KEY environment variable is required`**
|
|
275
|
+
Run `moda init`, or have the user export `MODA_API_KEY` from
|
|
276
|
+
[moda.dev/settings](https://moda.dev/settings).
|
|
277
|
+
|
|
278
|
+
**`API error (HTTP 401)`**
|
|
279
|
+
Key is invalid or revoked. Re-run `moda init` to get a fresh one.
|
|
280
|
+
|
|
281
|
+
**`API error (HTTP 404)` on `cluster-conversations`, `context`, or
|
|
282
|
+
`tool-failure-detail`**
|
|
283
|
+
The id/name doesn't exist in this tenant. Verify by listing first:
|
|
284
|
+
`moda clusters`, `moda conversations`, or `moda tool-failures`.
|
|
285
|
+
|
|
286
|
+
**`Validation error:`**
|
|
287
|
+
A flag value didn't match the schema. Check enums (`time_range` must be
|
|
288
|
+
`1h|24h|3d|7d|30d|90d|all`, not `"7 days"`) and numeric ranges
|
|
289
|
+
(`days_back` is 1–90, `window` max 5).
|
|
290
|
+
|
|
291
|
+
**Empty results**
|
|
292
|
+
Widen the window: try `--days-back=30` or `--time-range=30d`. Confirm with
|
|
293
|
+
`moda overview` that data exists in the tenant at all.
|
|
294
|
+
|
|
295
|
+
**`moda: command not found`**
|
|
296
|
+
Either install globally (`npm install -g @moda-ai/cli`) or invoke through
|
|
297
|
+
npx: `npx -p @moda-ai/cli moda <command>`.
|
|
298
|
+
|
|
299
|
+
## Reference: all commands at a glance
|
|
300
|
+
|
|
301
|
+
| Command | Purpose |
|
|
302
|
+
|---|---|
|
|
303
|
+
| `moda init` | Browser auth + provision API key + install agent rules |
|
|
304
|
+
| `moda overview` | Dashboard KPIs, top clusters, recent activity |
|
|
305
|
+
| `moda clusters` | Browse topic cluster hierarchy |
|
|
306
|
+
| `moda cluster-conversations <node_id>` | Conversations in a cluster |
|
|
307
|
+
| `moda conversations` | Search/filter conversations |
|
|
308
|
+
| `moda context <conversation_id>` | Windowed message context (max 5 per side) |
|
|
309
|
+
| `moda frustrations` | User frustration detections with evidence |
|
|
310
|
+
| `moda tool-failures` | Tool failure overview |
|
|
311
|
+
| `moda tool-failure-detail <tool_name>` | Per-tool failure breakdown + examples |
|
|
312
|
+
| `moda workflows` | Recurring workflow catalog with 30d health per class |
|
|
313
|
+
| `moda runs` | Recent workflow executions with classified outcomes |
|
|
314
|
+
| `moda run <run_id>` | One execution + its side-effect receipts |
|
|
315
|
+
| `moda loops` | Currently-active loop detections |
|
|
316
|
+
| `moda loop <detection_id>` | One loop signature with blast radius |
|
|
317
|
+
|
|
318
|
+
Common flags across most commands: `--days-back=N` (1–90), `--limit=N`,
|
|
319
|
+
`--offset=N`, `--time-range=<range>`. Runs commands also accept
|
|
320
|
+
`--outcome=<outcome>`, `--class-id=<workflow_class_id>`, and `--user-id=<id>`.
|
|
321
|
+
`frustrations` and `tool-failure-detail` additionally accept
|
|
322
|
+
`--include-window` (attach a `window` slice per row) and `--window=N` (1–5,
|
|
323
|
+
default `1`; half-width, so `--window=1` ⇒ 3 messages).
|