@moda-ai/cli 1.0.5 → 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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Moda API Key (required)
2
- # Get this from https://app.modaflows.com/settings
2
+ # Get this from https://moda.dev/settings
3
3
  MODA_API_KEY=moda_sk_...
4
4
 
5
5
  # Moda Backend URL (optional)
6
- # Defaults to production: https://modaflows.com
6
+ # Defaults to production: https://moda.dev
7
7
  # For local development, override with:
8
8
  # MODA_BASE_URL=http://localhost:3001
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # moda-cli
2
2
 
3
- CLI for [Moda](https://modaflows.com) -- AI agent analytics and observability.
3
+ CLI for [Moda](https://moda.dev) -- AI agent analytics and observability.
4
4
 
5
5
  Query your conversation analytics from the terminal.
6
6
 
@@ -30,7 +30,7 @@ Or set the key manually:
30
30
  export MODA_API_KEY="moda_sk_..."
31
31
  ```
32
32
 
33
- Get your API key from [modaflows.com/settings](https://modaflows.com/settings).
33
+ Get your API key from [moda.dev/settings](https://moda.dev/settings).
34
34
 
35
35
  ## Usage
36
36
 
@@ -52,9 +52,11 @@ moda context <conversation_id> --msg-index=5 # Center on message 5
52
52
 
53
53
  moda frustrations # User frustration detections
54
54
  moda frustrations --days-back=14 --limit=20
55
+ moda frustrations --include-window --window=1 # Attach 3-message slice per row
55
56
 
56
57
  moda tool-failures # Tool failure overview
57
58
  moda tool-failure-detail <tool_name> # Per-tool failure detail
59
+ moda tool-failure-detail <tool_name> --include-window --window=1
58
60
 
59
61
  # Agent runs runtime (workflows = recurring class, run = one execution)
60
62
  moda workflows --time-range=7d # Recurring workflow catalog with health
@@ -76,18 +78,48 @@ moda loops | jq '.loops | sort_by(-.cycle_visits) | .[0:5] | .[].loop_signature_
76
78
  moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.health < 0.8)'
77
79
  ```
78
80
 
81
+ ## Anchors and message windows
82
+
83
+ Rows returned by `moda frustrations` and `moda tool-failure-detail` carry
84
+ a top-level `anchor` block naming the exact turn the event fired on:
85
+
86
+ ```json
87
+ // frustrations
88
+ { "kind": "frustration", "conversation_id": "...", "msg_index": 12,
89
+ "signal": "exasperation", "quote_preview": "...", "all_turns": [...],
90
+ "no_anchor": false }
91
+
92
+ // tool-failure-detail examples
93
+ { "kind": "tool_failure", "conversation_id": "...", "msg_index": 7,
94
+ "tool_name": "search", "tool_use_id": "...",
95
+ "error_subtype": "SEARCH_NO_RESULTS", "no_anchor": false }
96
+ ```
97
+
98
+ `msg_index` is the 0-indexed turn where the breakage happened. For
99
+ frustrations it's derived from `key_turns[0]` (the same turn the API centers
100
+ the embedded window on), falling back to `user_quotes[0].turn` when
101
+ `key_turns` is empty. `no_anchor: true` means no anchor could be derived for
102
+ that row.
103
+
104
+ Both commands accept two flags to inline a message window per row:
105
+
106
+ | Flag | Default | Description |
107
+ |------|---------|-------------|
108
+ | `--include-window` | off | Attach a `window` field with the message slice centered on the anchor |
109
+ | `--window=N` | `1` | Half-width (1–5). `--window=1` ⇒ 3 messages total (anchor ± 1) |
110
+
79
111
  ## Environment Variables
80
112
 
81
113
  | Variable | Required | Default | Description |
82
114
  |----------|----------|---------|-------------|
83
115
  | `MODA_API_KEY` | Yes | -- | Your Moda API key (`moda_sk_...`) |
84
- | `MODA_BASE_URL` | No | `https://modaflows.com` | Base URL for the Data API |
116
+ | `MODA_BASE_URL` | No | `https://moda.dev` | Base URL for the Data API |
85
117
 
86
118
  ## Documentation
87
119
 
88
- - [CLI Reference](https://docs.modaflows.com/data-api/cli)
89
- - [Data API Overview](https://docs.modaflows.com/data-api/overview)
90
- - [Quickstart](https://docs.modaflows.com/quickstart)
120
+ - [CLI Reference](https://docs.moda.dev/data-api/cli)
121
+ - [Data API Overview](https://docs.moda.dev/data-api/overview)
122
+ - [Quickstart](https://docs.moda.dev/quickstart)
91
123
 
92
124
  ## License
93
125
 
package/dist/cli.d.ts CHANGED
@@ -6,5 +6,6 @@ interface ParsedArgs {
6
6
  }
7
7
  export declare function parseArgs(argv: string[]): ParsedArgs;
8
8
  export declare function flagsToArgs(flags: Record<string, string>, positionalKey?: string, positionalValue?: string): Record<string, unknown>;
9
+ export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>): Promise<void>;
9
10
  export {};
10
11
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { validateConfig, VERSION } from './config.js';
4
4
  import { callDataAPI, ApiError } from './api-client.js';
5
5
  import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
6
+ import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
6
7
  export function parseArgs(argv) {
7
8
  const args = argv.slice(2);
8
9
  const command = args[0] || '';
@@ -107,7 +108,51 @@ Examples:
107
108
  moda loop 12345678-1234-1234-1234-1234567890ab`);
108
109
  }
109
110
  // --- Command Handlers ---
110
- async function runCommand(command, positional, flags) {
111
+ async function attachWindows(rows, windowSize) {
112
+ return Promise.all(rows.map(async (row) => {
113
+ const anchor = asRecord(row.anchor);
114
+ if (!anchor || anchor.no_anchor === true) {
115
+ return { ...row, window: null };
116
+ }
117
+ const msg_index = asNumber(anchor.msg_index);
118
+ if (msg_index === null) {
119
+ return { ...row, window: null };
120
+ }
121
+ const conv_id = asString(anchor.conversation_id) ?? '';
122
+ if (!conv_id) {
123
+ return { ...row, window: null };
124
+ }
125
+ const conversation = asRecord(row.conversation);
126
+ // embedded window may be wider than requested (backend default = 2); reuse anything ≥ requested
127
+ const embedded = conversation ? embeddedWindowSize(conversation) : null;
128
+ if (conversation && embedded !== null && embedded >= windowSize) {
129
+ return { ...row, window: conversation };
130
+ }
131
+ try {
132
+ const query = new URLSearchParams();
133
+ query.set('msg_index', msg_index.toString());
134
+ query.set('window', windowSize.toString());
135
+ const data = await callDataAPI(`/conversations/${conv_id}/context?${query.toString()}`);
136
+ return { ...row, window: data };
137
+ }
138
+ catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ return { ...row, window: { error: message } };
141
+ }
142
+ }));
143
+ }
144
+ async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize) {
145
+ if (!includeWindow) {
146
+ console.log(JSON.stringify(enriched, null, 2));
147
+ return;
148
+ }
149
+ const list = Array.isArray(enriched[listKey])
150
+ ? enriched[listKey]
151
+ : [];
152
+ const withWindows = await attachWindows(list, windowSize);
153
+ console.log(JSON.stringify({ ...enriched, [listKey]: withWindows }, null, 2));
154
+ }
155
+ export async function runCommand(command, positional, flags) {
111
156
  switch (command) {
112
157
  case 'overview': {
113
158
  const args = flagsToArgs(flags);
@@ -201,7 +246,7 @@ async function runCommand(command, positional, flags) {
201
246
  query.set('offset', params.offset.toString());
202
247
  const queryString = query.toString() ? `?${query.toString()}` : '';
203
248
  const data = await callDataAPI(`/frustrations${queryString}`);
204
- console.log(JSON.stringify(data, null, 2));
249
+ await printWithOptionalWindows(withFrustrationAnchors(data), 'frustrations', params.include_window === true, params.window ?? 1);
205
250
  break;
206
251
  }
207
252
  case 'tool-failures': {
@@ -231,7 +276,7 @@ async function runCommand(command, positional, flags) {
231
276
  query.set('offset', params.offset.toString());
232
277
  const queryString = query.toString() ? `?${query.toString()}` : '';
233
278
  const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
234
- console.log(JSON.stringify(data, null, 2));
279
+ await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1);
235
280
  break;
236
281
  }
237
282
  // ─── Workflows + Runs ──────────────────────────────────────
package/dist/config.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/config.ts
2
2
  export const VERSION = '1.0.4';
3
3
  export const MODA_API_KEY = process.env.MODA_API_KEY;
4
- export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://modaflows.com';
4
+ export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://moda.dev';
5
5
  export function validateConfig() {
6
6
  if (!MODA_API_KEY) {
7
7
  console.error('Error: MODA_API_KEY environment variable is required');
8
- console.error('Get your API key from https://modaflows.com/settings');
8
+ console.error('Get your API key from https://moda.dev/settings');
9
9
  process.exit(1);
10
10
  }
11
11
  }
@@ -46,7 +46,7 @@ export async function runInit(flags) {
46
46
  console.log(' Now paste this into your coding agent to integrate Moda:\n');
47
47
  console.log(' Integrate the Moda SDK into this project. Follow the Moda SDK');
48
48
  console.log(' integration guide in the project rules.\n');
49
- console.log(' Dashboard: https://modaflows.com');
49
+ console.log(' Dashboard: https://moda.dev');
50
50
  console.log('');
51
51
  }
52
52
  //# sourceMappingURL=index.js.map
@@ -12,7 +12,7 @@ export async function selectTenantAndCreateKey() {
12
12
  console.log(` Authenticated as ${ctx.user.email}`);
13
13
  // 2. Select tenant
14
14
  if (ctx.tenants.length === 0) {
15
- console.error(' No organizations found. Create one at https://app.modaflows.com');
15
+ console.error(' No organizations found. Create one at https://moda.dev');
16
16
  process.exit(1);
17
17
  }
18
18
  let tenantId;
package/dist/schemas.d.ts CHANGED
@@ -71,14 +71,20 @@ export declare const FrustrationsSchema: z.ZodObject<{
71
71
  days_back: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
72
72
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
73
73
  offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
74
+ include_window: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
75
+ window: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
74
76
  }, "strip", z.ZodTypeAny, {
75
77
  days_back?: number | undefined;
76
78
  limit?: number | undefined;
77
79
  offset?: number | undefined;
80
+ window?: number | undefined;
81
+ include_window?: boolean | undefined;
78
82
  }, {
79
83
  days_back?: number | undefined;
80
84
  limit?: number | undefined;
81
85
  offset?: number | undefined;
86
+ window?: number | undefined;
87
+ include_window?: unknown;
82
88
  }>;
83
89
  export declare const ToolFailuresSchema: z.ZodObject<{
84
90
  days_back: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
@@ -93,17 +99,23 @@ export declare const ToolFailureDetailSchema: z.ZodObject<{
93
99
  days_back: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
94
100
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
95
101
  offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
102
+ include_window: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
103
+ window: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
96
104
  }, "strip", z.ZodTypeAny, {
97
105
  tool_name: string;
98
106
  days_back?: number | undefined;
99
107
  limit?: number | undefined;
100
108
  offset?: number | undefined;
109
+ window?: number | undefined;
110
+ include_window?: boolean | undefined;
101
111
  subtype?: string | undefined;
102
112
  }, {
103
113
  tool_name: string;
104
114
  days_back?: number | undefined;
105
115
  limit?: number | undefined;
106
116
  offset?: number | undefined;
117
+ window?: number | undefined;
118
+ include_window?: unknown;
107
119
  subtype?: string | undefined;
108
120
  }>;
109
121
  /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
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,6 +51,8 @@ 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(),
47
56
  });
48
57
  // ─── Workflows + Runs (agent runs runtime) ──────────────────
49
58
  //
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,9 +39,9 @@
39
39
  "analytics",
40
40
  "ai-agents"
41
41
  ],
42
- "author": "Moda <hello@modaflows.com>",
42
+ "author": "Moda <hello@moda.dev>",
43
43
  "license": "MIT",
44
- "homepage": "https://docs.modaflows.com/data-api/cli",
44
+ "homepage": "https://docs.moda.dev/data-api/cli",
45
45
  "bugs": {
46
46
  "url": "https://github.com/ModaLabs/moda-cli/issues"
47
47
  },
@@ -34,7 +34,7 @@ Before running any command except `moda init`:
34
34
 
35
35
  Optional:
36
36
 
37
- - `MODA_BASE_URL` defaults to `https://modaflows.com`; override only for
37
+ - `MODA_BASE_URL` defaults to `https://moda.dev`; override only for
38
38
  self-hosted or staging.
39
39
 
40
40
  ## Setup
@@ -55,7 +55,7 @@ npx -p @moda-ai/cli moda <command>
55
55
  ### Manual key
56
56
 
57
57
  ```bash
58
- export MODA_API_KEY="moda_sk_..." # from modaflows.com/settings
58
+ export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
59
59
  ```
60
60
 
61
61
  ## Procedure
@@ -84,7 +84,7 @@ moda cluster-conversations <node_id> # conversations in cluster
84
84
  ### 3. Search conversations
85
85
 
86
86
  ```bash
87
- export MODA_API_KEY="moda_sk_..." # from modaflows.com/settings
87
+ export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
88
88
  ```
89
89
 
90
90
  Filters: `--search`, `--cluster-id`, `--user-id`, `--time-range`
@@ -104,20 +104,49 @@ moda context <conversation_id> --window=3 # 3 messages each side (max 5)
104
104
  ```bash
105
105
  moda frustrations # last 7 days
106
106
  moda frustrations --days-back=14 --limit=20
107
+ moda frustrations --include-window --window=1 --limit=5
107
108
  ```
108
109
 
109
110
  Each result includes inline conversation snippet, user quotes, trajectory,
110
111
  signal breakdown (exasperation, profanity, anger, sarcasm, giving_up, insult),
111
112
  and primary cause.
112
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
+
113
129
  ### 6. Tool failure investigation
114
130
 
115
131
  ```bash
116
132
  moda tool-failures # which tools are breaking
117
133
  moda tool-failure-detail <tool_name> # subtype breakdown + examples
118
134
  moda tool-failure-detail search --subtype=SEARCH_NO_RESULTS --limit=10
135
+ moda tool-failure-detail search --include-window --window=1 --limit=5
119
136
  ```
120
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
+
121
150
  ### 7. Workflow runs and outcomes
122
151
 
123
152
  Every triggered run of an agent (cron invocation, scheduled task, or
@@ -244,7 +273,7 @@ moda tool-failures | jq '.tools[] | {tool: .tool_name, failures: .failure_count}
244
273
 
245
274
  **`Error: MODA_API_KEY environment variable is required`**
246
275
  Run `moda init`, or have the user export `MODA_API_KEY` from
247
- [modaflows.com/settings](https://modaflows.com/settings).
276
+ [moda.dev/settings](https://moda.dev/settings).
248
277
 
249
278
  **`API error (HTTP 401)`**
250
279
  Key is invalid or revoked. Re-run `moda init` to get a fresh one.
@@ -289,3 +318,6 @@ npx: `npx -p @moda-ai/cli moda <command>`.
289
318
  Common flags across most commands: `--days-back=N` (1–90), `--limit=N`,
290
319
  `--offset=N`, `--time-range=<range>`. Runs commands also accept
291
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).