@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/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",
4
- "description": "CLI and MCP server for Moda - AI agent analytics and observability",
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@modaflows.com>",
42
+ "author": "Moda <hello@moda.dev>",
49
43
  "license": "MIT",
50
- "homepage": "https://docs.modaflows.com/data-api/cli",
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).
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=api-client.test.d.ts.map