@moda-ai/cli 1.0.5 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/README.md +38 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +48 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +5 -4
- package/dist/init/index.js +1 -1
- package/dist/init/tenant.js +1 -1
- package/dist/schemas.d.ts +12 -0
- package/dist/schemas.js +9 -0
- package/dist/transforms.d.ts +25 -0
- package/dist/transforms.js +102 -0
- package/package.json +3 -3
- package/skills/moda-cli/SKILL.md +36 -4
package/.env.example
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Moda API Key (required)
|
|
2
|
-
# Get this from https://
|
|
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://
|
|
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://
|
|
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 [
|
|
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://
|
|
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.
|
|
89
|
-
- [Data API Overview](https://docs.
|
|
90
|
-
- [Quickstart](https://docs.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
3
|
+
export const VERSION = pkg.version;
|
|
3
4
|
export const MODA_API_KEY = process.env.MODA_API_KEY;
|
|
4
|
-
export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://
|
|
5
|
+
export const MODA_BASE_URL = process.env.MODA_BASE_URL || 'https://moda.dev';
|
|
5
6
|
export function validateConfig() {
|
|
6
7
|
if (!MODA_API_KEY) {
|
|
7
8
|
console.error('Error: MODA_API_KEY environment variable is required');
|
|
8
|
-
console.error('Get your API key from https://
|
|
9
|
+
console.error('Get your API key from https://moda.dev/settings');
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
12
|
}
|
package/dist/init/index.js
CHANGED
|
@@ -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://
|
|
49
|
+
console.log(' Dashboard: https://moda.dev');
|
|
50
50
|
console.log('');
|
|
51
51
|
}
|
|
52
52
|
//# sourceMappingURL=index.js.map
|
package/dist/init/tenant.js
CHANGED
|
@@ -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://
|
|
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.
|
|
3
|
+
"version": "1.1.1",
|
|
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@
|
|
42
|
+
"author": "Moda <hello@moda.dev>",
|
|
43
43
|
"license": "MIT",
|
|
44
|
-
"homepage": "https://docs.
|
|
44
|
+
"homepage": "https://docs.moda.dev/data-api/cli",
|
|
45
45
|
"bugs": {
|
|
46
46
|
"url": "https://github.com/ModaLabs/moda-cli/issues"
|
|
47
47
|
},
|
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -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://
|
|
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
|
|
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
|
|
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
|
-
[
|
|
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).
|