@moda-ai/cli 1.0.4 → 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.
- package/README.md +24 -54
- package/dist/auth.js +39 -67
- package/dist/cli.js +93 -12
- package/dist/config.d.ts +1 -2
- package/dist/config.js +1 -2
- 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 +3 -41
- package/dist/schemas.d.ts +59 -0
- package/dist/schemas.js +35 -0
- package/package.json +6 -17
- package/skills/integrate-moda/SKILL.md +63 -0
- package/skills/moda-cli/SKILL.md +291 -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
|
@@ -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,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,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
|