@moda-ai/cli 1.0.3 → 1.0.5

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 (41) hide show
  1. package/README.md +24 -54
  2. package/dist/auth.js +39 -67
  3. package/dist/cli.js +93 -12
  4. package/dist/config.d.ts +1 -2
  5. package/dist/config.js +1 -2
  6. package/dist/init/detect.d.ts +1 -1
  7. package/dist/init/detect.js +28 -5
  8. package/dist/init/index.js +26 -68
  9. package/dist/init/mcp.d.ts +1 -7
  10. package/dist/init/mcp.js +5 -88
  11. package/dist/init/rules.d.ts +1 -1
  12. package/dist/init/rules.js +151 -58
  13. package/dist/init/tenant.js +3 -41
  14. package/dist/schemas.d.ts +59 -0
  15. package/dist/schemas.js +35 -0
  16. package/package.json +7 -17
  17. package/skills/integrate-moda/SKILL.md +63 -0
  18. package/skills/moda-cli/SKILL.md +291 -0
  19. package/dist/__tests__/api-client.test.d.ts +0 -2
  20. package/dist/__tests__/api-client.test.js +0 -73
  21. package/dist/__tests__/cli.test.d.ts +0 -2
  22. package/dist/__tests__/cli.test.js +0 -109
  23. package/dist/__tests__/e2e.test.d.ts +0 -2
  24. package/dist/__tests__/e2e.test.js +0 -239
  25. package/dist/__tests__/handlers.test.d.ts +0 -2
  26. package/dist/__tests__/handlers.test.js +0 -71
  27. package/dist/__tests__/integration.test.d.ts +0 -2
  28. package/dist/__tests__/integration.test.js +0 -114
  29. package/dist/__tests__/schemas.test.d.ts +0 -2
  30. package/dist/__tests__/schemas.test.js +0 -81
  31. package/dist/handlers.d.ts +0 -7
  32. package/dist/handlers.js +0 -117
  33. package/dist/http-server.d.ts +0 -3
  34. package/dist/http-server.js +0 -74
  35. package/dist/index.d.ts +0 -3
  36. package/dist/index.js +0 -26
  37. package/dist/tools.d.ts +0 -260
  38. package/dist/tools.js +0 -133
  39. package/dist/worker.d.ts +0 -9
  40. package/dist/worker.js +0 -24
  41. package/mcp-config.example.json +0 -12
@@ -0,0 +1,291 @@
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://modaflows.com`; 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 modaflows.com/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 modaflows.com/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
+ ```
108
+
109
+ Each result includes inline conversation snippet, user quotes, trajectory,
110
+ signal breakdown (exasperation, profanity, anger, sarcasm, giving_up, insult),
111
+ and primary cause.
112
+
113
+ ### 6. Tool failure investigation
114
+
115
+ ```bash
116
+ moda tool-failures # which tools are breaking
117
+ moda tool-failure-detail <tool_name> # subtype breakdown + examples
118
+ moda tool-failure-detail search --subtype=SEARCH_NO_RESULTS --limit=10
119
+ ```
120
+
121
+ ### 7. Workflow runs and outcomes
122
+
123
+ Every triggered run of an agent (cron invocation, scheduled task, or
124
+ interactive session) is classified `succeeded | partial | failed | loop |
125
+ abandoned`. Use this layer to answer "did the job actually do its work?",
126
+ which is structurally different from "did it error?" — most jobs that
127
+ partially completed never threw.
128
+
129
+ ```bash
130
+ moda runs # last 7d, all outcomes
131
+ moda runs --time-range=24h --outcome=partial # which runs only half-finished
132
+ moda runs --outcome=loop # runs caught in active loops
133
+ moda runs --class-id=cron:helpscout-triage-hourly --limit=20
134
+ moda run <run_id> # one execution + its side-effect receipts
135
+ ```
136
+
137
+ `moda run <run_id>` returns the `agent_jobs` row plus every
138
+ `side_effect_receipts` entry for the run — i.e. every tool call the agent
139
+ made (read / write / send / spawn / compute), with success status and
140
+ target system. This is the agent-as-employee log; use it instead of (or
141
+ alongside) `moda context` when debugging "what did the agent actually
142
+ DO".
143
+
144
+ ### 8. Recurring workflows / cron status board
145
+
146
+ ```bash
147
+ moda workflows # health per recurring class
148
+ moda workflows --time-range=7d # filter to last 7d of activity
149
+ ```
150
+
151
+ Each row is one job class (a cron slug, idea-widget id, or sourceType)
152
+ with 30d rolling success / partial / failed / loop counts and a
153
+ `health_score` (0–1). Sort by health to find degraded customer-facing
154
+ crons that aren't crashing but aren't completing either.
155
+
156
+ ### 9. Active loop investigation
157
+
158
+ ```bash
159
+ moda loops # everything currently stuck
160
+ moda loops --limit=10
161
+ moda loop <detection_id> # one loop signature in detail
162
+ ```
163
+
164
+ Loops are detected in real time by the conversation worker (tier 0/1) and
165
+ the analytics container (tier 2). A row with `cycle_visits=378` means the
166
+ agent re-ran the same action 378 times. Use the `loop_signature_human`
167
+ field to recognize cross-conversation patterns (one tool definition bug
168
+ will produce identical signatures across many users).
169
+
170
+ ## Common workflow recipes
171
+
172
+ ### Daily health check
173
+
174
+ ```bash
175
+ moda overview --days-back=1
176
+ moda frustrations --days-back=1 --limit=5
177
+ moda tool-failures --days-back=1
178
+ ```
179
+
180
+ ### Debug a frustrated user
181
+
182
+ ```bash
183
+ moda frustrations --days-back=7 --limit=10
184
+ # Pick a conversation_id from output
185
+ moda context <conversation_id>
186
+ moda context <conversation_id> --msg-index=<turn> # zoom on the bad turn
187
+ ```
188
+
189
+ ### Diagnose a flaky tool
190
+
191
+ ```bash
192
+ moda tool-failures
193
+ moda tool-failure-detail <tool_name>
194
+ # Pick an example conversation_id from the output
195
+ moda context <conversation_id>
196
+ ```
197
+
198
+ ### Explore what users are doing
199
+
200
+ ```bash
201
+ moda clusters
202
+ moda clusters --parent-id=<node_id>
203
+ moda cluster-conversations <node_id>
204
+ ```
205
+
206
+ ### Find degraded crons before customers do
207
+
208
+ ```bash
209
+ # Health-sorted workflow board
210
+ moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.runs >= 4 and .health < 0.85)'
211
+
212
+ # What loops are eating tokens right now?
213
+ moda loops --limit=20 | jq '.loops[] | {sig: .loop_signature_human, visits: .cycle_visits, conv: .conversation_id}'
214
+
215
+ # Drill into a partial run
216
+ moda runs --outcome=partial --limit=5 | jq -r '.runs[].job_id' | head -1 | xargs -I{} moda run {}
217
+ ```
218
+
219
+ ### One-liner chains with jq
220
+
221
+ ```bash
222
+ # Pull primary causes of recent frustrations
223
+ moda frustrations --days-back=7 | jq -r '.frustrations[].primary_cause'
224
+
225
+ # Get conversation IDs for a search and fetch context for each
226
+ for id in $(moda conversations --search="timeout" --limit=3 | jq -r '.conversations[].id'); do
227
+ moda context "$id"
228
+ done
229
+
230
+ # Tool failure leaderboard
231
+ moda tool-failures | jq '.tools[] | {tool: .tool_name, failures: .failure_count}'
232
+ ```
233
+
234
+ ## Validation — how to know it worked
235
+
236
+ - Every successful command exits 0 and prints valid JSON to stdout.
237
+ - Errors print to stderr with non-zero exit. Check stderr before claiming
238
+ success.
239
+ - If the JSON has `"conversations": []` or `"frustrations": []`, the call
240
+ worked but the time window is empty — widen `--days-back` before
241
+ concluding "no data".
242
+
243
+ ## Troubleshooting
244
+
245
+ **`Error: MODA_API_KEY environment variable is required`**
246
+ Run `moda init`, or have the user export `MODA_API_KEY` from
247
+ [modaflows.com/settings](https://modaflows.com/settings).
248
+
249
+ **`API error (HTTP 401)`**
250
+ Key is invalid or revoked. Re-run `moda init` to get a fresh one.
251
+
252
+ **`API error (HTTP 404)` on `cluster-conversations`, `context`, or
253
+ `tool-failure-detail`**
254
+ The id/name doesn't exist in this tenant. Verify by listing first:
255
+ `moda clusters`, `moda conversations`, or `moda tool-failures`.
256
+
257
+ **`Validation error:`**
258
+ A flag value didn't match the schema. Check enums (`time_range` must be
259
+ `1h|24h|3d|7d|30d|90d|all`, not `"7 days"`) and numeric ranges
260
+ (`days_back` is 1–90, `window` max 5).
261
+
262
+ **Empty results**
263
+ Widen the window: try `--days-back=30` or `--time-range=30d`. Confirm with
264
+ `moda overview` that data exists in the tenant at all.
265
+
266
+ **`moda: command not found`**
267
+ Either install globally (`npm install -g @moda-ai/cli`) or invoke through
268
+ npx: `npx -p @moda-ai/cli moda <command>`.
269
+
270
+ ## Reference: all commands at a glance
271
+
272
+ | Command | Purpose |
273
+ |---|---|
274
+ | `moda init` | Browser auth + provision API key + install agent rules |
275
+ | `moda overview` | Dashboard KPIs, top clusters, recent activity |
276
+ | `moda clusters` | Browse topic cluster hierarchy |
277
+ | `moda cluster-conversations <node_id>` | Conversations in a cluster |
278
+ | `moda conversations` | Search/filter conversations |
279
+ | `moda context <conversation_id>` | Windowed message context (max 5 per side) |
280
+ | `moda frustrations` | User frustration detections with evidence |
281
+ | `moda tool-failures` | Tool failure overview |
282
+ | `moda tool-failure-detail <tool_name>` | Per-tool failure breakdown + examples |
283
+ | `moda workflows` | Recurring workflow catalog with 30d health per class |
284
+ | `moda runs` | Recent workflow executions with classified outcomes |
285
+ | `moda run <run_id>` | One execution + its side-effect receipts |
286
+ | `moda loops` | Currently-active loop detections |
287
+ | `moda loop <detection_id>` | One loop signature with blast radius |
288
+
289
+ Common flags across most commands: `--days-back=N` (1–90), `--limit=N`,
290
+ `--offset=N`, `--time-range=<range>`. Runs commands also accept
291
+ `--outcome=<outcome>`, `--class-id=<workflow_class_id>`, and `--user-id=<id>`.
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=api-client.test.d.ts.map
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // Mock config before importing api-client
3
- vi.mock('../config.js', () => ({
4
- MODA_API_KEY: 'moda_sk_test_key',
5
- MODA_BASE_URL: 'https://test.modaflows.com',
6
- }));
7
- import { callDataAPI, ApiError } from '../api-client.js';
8
- describe('callDataAPI', () => {
9
- beforeEach(() => {
10
- vi.restoreAllMocks();
11
- });
12
- it('constructs correct URL', async () => {
13
- const mockFetch = vi.fn().mockResolvedValue({
14
- ok: true,
15
- json: () => Promise.resolve({ data: 'test' }),
16
- });
17
- vi.stubGlobal('fetch', mockFetch);
18
- await callDataAPI('/overview?days_back=7');
19
- expect(mockFetch).toHaveBeenCalledWith('https://test.modaflows.com/api/v1/data/overview?days_back=7', expect.objectContaining({
20
- headers: expect.objectContaining({
21
- 'x-api-key': 'moda_sk_test_key',
22
- }),
23
- }));
24
- });
25
- it('throws ApiError on non-ok response', async () => {
26
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
27
- ok: false,
28
- status: 401,
29
- text: () => Promise.resolve('Unauthorized'),
30
- }));
31
- await expect(callDataAPI('/overview')).rejects.toThrow('API error (401): Unauthorized');
32
- await vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
33
- ok: false,
34
- status: 401,
35
- text: () => Promise.resolve('Unauthorized'),
36
- }));
37
- try {
38
- await callDataAPI('/overview');
39
- }
40
- catch (e) {
41
- expect(e).toBeInstanceOf(ApiError);
42
- expect(e.statusCode).toBe(401);
43
- }
44
- });
45
- it('retries on 500 errors', async () => {
46
- const mockFetch = vi.fn()
47
- .mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve('Internal Server Error') })
48
- .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: 'ok' }) });
49
- vi.stubGlobal('fetch', mockFetch);
50
- const result = await callDataAPI('/overview');
51
- expect(result).toEqual({ data: 'ok' });
52
- expect(mockFetch).toHaveBeenCalledTimes(2);
53
- });
54
- it('does not retry on 4xx errors (except 429)', async () => {
55
- const mockFetch = vi.fn().mockResolvedValue({
56
- ok: false,
57
- status: 403,
58
- text: () => Promise.resolve('Forbidden'),
59
- });
60
- vi.stubGlobal('fetch', mockFetch);
61
- await expect(callDataAPI('/overview')).rejects.toThrow('API error (403)');
62
- expect(mockFetch).toHaveBeenCalledTimes(1);
63
- });
64
- it('returns parsed JSON on success', async () => {
65
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
66
- ok: true,
67
- json: () => Promise.resolve({ conversations: { total: 100 } }),
68
- }));
69
- const result = await callDataAPI('/overview');
70
- expect(result).toEqual({ conversations: { total: 100 } });
71
- });
72
- });
73
- //# sourceMappingURL=api-client.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=cli.test.d.ts.map
@@ -1,109 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // Mock api-client (same pattern as handlers.test.ts)
3
- vi.mock('../api-client.js', () => ({
4
- callDataAPI: vi.fn().mockResolvedValue({ mock: true }),
5
- }));
6
- // Mock config to prevent process.exit
7
- vi.mock('../config.js', () => ({
8
- validateConfig: vi.fn(),
9
- MODA_API_KEY: 'test-key',
10
- MODA_BASE_URL: 'https://test.example.com',
11
- PORT: 3003,
12
- }));
13
- // Import after mocks
14
- import { parseArgs, flagsToArgs } from '../cli.js';
15
- import { callDataAPI } from '../api-client.js';
16
- const mockCallDataAPI = vi.mocked(callDataAPI);
17
- describe('parseArgs', () => {
18
- it('extracts command from argv', () => {
19
- const result = parseArgs(['node', 'cli.js', 'overview']);
20
- expect(result.command).toBe('overview');
21
- expect(result.positional).toBeUndefined();
22
- expect(result.flags).toEqual({});
23
- });
24
- it('extracts positional argument', () => {
25
- const result = parseArgs(['node', 'cli.js', 'cluster-conversations', 'node-123']);
26
- expect(result.command).toBe('cluster-conversations');
27
- expect(result.positional).toBe('node-123');
28
- });
29
- it('parses --key=value flags', () => {
30
- const result = parseArgs(['node', 'cli.js', 'overview', '--days-back=30']);
31
- expect(result.flags).toEqual({ 'days-back': '30' });
32
- });
33
- it('parses --key value flags', () => {
34
- const result = parseArgs(['node', 'cli.js', 'overview', '--days-back', '30']);
35
- expect(result.flags).toEqual({ 'days-back': '30' });
36
- });
37
- it('parses mixed flag formats', () => {
38
- const result = parseArgs(['node', 'cli.js', 'conversations', '--search=error', '--limit', '5']);
39
- expect(result.flags).toEqual({ search: 'error', limit: '5' });
40
- });
41
- it('handles boolean flags (no value)', () => {
42
- const result = parseArgs(['node', 'cli.js', '--help']);
43
- expect(result.command).toBe('--help');
44
- });
45
- it('returns empty command for no args', () => {
46
- const result = parseArgs(['node', 'cli.js']);
47
- expect(result.command).toBe('');
48
- });
49
- it('handles positional before flags', () => {
50
- const result = parseArgs(['node', 'cli.js', 'context', 'conv-abc', '--window=3']);
51
- expect(result.command).toBe('context');
52
- expect(result.positional).toBe('conv-abc');
53
- expect(result.flags).toEqual({ window: '3' });
54
- });
55
- });
56
- describe('flagsToArgs', () => {
57
- it('converts kebab-case to snake_case', () => {
58
- const result = flagsToArgs({ 'days-back': '30', 'time-range': '7d' });
59
- expect(result).toEqual({ days_back: 30, time_range: '7d' });
60
- });
61
- it('parses numeric values', () => {
62
- const result = flagsToArgs({ limit: '10', offset: '0' });
63
- expect(result).toEqual({ limit: 10, offset: 0 });
64
- });
65
- it('keeps string values as strings', () => {
66
- const result = flagsToArgs({ search: 'error message' });
67
- expect(result).toEqual({ search: 'error message' });
68
- });
69
- it('includes positional key/value', () => {
70
- const result = flagsToArgs({ limit: '5' }, 'node_id', 'node-123');
71
- expect(result).toEqual({ node_id: 'node-123', limit: 5 });
72
- });
73
- });
74
- describe('CLI command dispatch', () => {
75
- beforeEach(() => {
76
- vi.clearAllMocks();
77
- });
78
- // We test the commands by dynamically importing and calling runCommand.
79
- // Since runCommand is not exported, we test indirectly via the module's behavior.
80
- // The key thing to test is that each command constructs the correct API URL.
81
- // This is tested at the integration level via handlers.test.ts already.
82
- // Here we focus on the arg parsing + flag conversion that is unique to the CLI.
83
- it('overview command builds correct args from flags', () => {
84
- const args = flagsToArgs({ 'days-back': '14' });
85
- expect(args).toEqual({ days_back: 14 });
86
- });
87
- it('cluster-conversations builds args with positional node_id', () => {
88
- const args = flagsToArgs({ limit: '5', offset: '0' }, 'node_id', 'cluster-abc');
89
- expect(args).toEqual({ node_id: 'cluster-abc', limit: 5, offset: 0 });
90
- });
91
- it('conversations builds args with multiple flags', () => {
92
- const args = flagsToArgs({ search: 'error', 'user-id': 'user-1', 'time-range': '7d', limit: '10', offset: '0' });
93
- expect(args).toEqual({ search: 'error', user_id: 'user-1', time_range: '7d', limit: 10, offset: 0 });
94
- });
95
- it('context builds args with positional conversation_id', () => {
96
- const args = flagsToArgs({ 'msg-index': '5', window: '3' }, 'conversation_id', 'conv-xyz');
97
- expect(args).toEqual({ conversation_id: 'conv-xyz', msg_index: 5, window: 3 });
98
- });
99
- it('tool-failure-detail builds args with positional tool_name', () => {
100
- const args = flagsToArgs({ subtype: 'timeout', 'days-back': '14' }, 'tool_name', 'search');
101
- expect(args).toEqual({ tool_name: 'search', subtype: 'timeout', days_back: 14 });
102
- });
103
- it('offset=0 is preserved (not dropped as falsy)', () => {
104
- const args = flagsToArgs({ offset: '0' });
105
- expect(args.offset).toBe(0);
106
- expect(args.offset).not.toBeUndefined();
107
- });
108
- });
109
- //# sourceMappingURL=cli.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=e2e.test.d.ts.map