@moda-ai/cli 1.8.0 → 1.9.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/dist/cli.js +99 -2
- package/dist/command-context.d.ts +15 -0
- package/dist/command-context.js +7 -0
- package/dist/schemas.d.ts +24 -0
- package/dist/schemas.js +13 -0
- package/dist/telemetry.d.ts +38 -0
- package/dist/telemetry.js +104 -0
- package/package.json +1 -1
- package/skills/moda-cli/SKILL.md +112 -10
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { validateConfig, VERSION, resolveApiKey } from './config.js';
|
|
4
4
|
import { callDataAPI, ApiError, getApiRequestCount, resetApiRequestCount, } from './api-client.js';
|
|
5
|
-
import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, WorldStateSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
|
|
5
|
+
import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
|
|
6
6
|
import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
|
|
7
7
|
import { runPromptsCommand } from './prompts.js';
|
|
8
8
|
// Coding-agent capture (shadow workspace / Shadow Spark / trace hooks) is temporarily
|
|
@@ -13,7 +13,7 @@ import { runHarnessCommand } from './harness.js';
|
|
|
13
13
|
import { runSyncCommand } from './sync.js';
|
|
14
14
|
import { runDoctorCommand, runStatusCommand } from './doctor.js';
|
|
15
15
|
import { CliInputError } from './errors.js';
|
|
16
|
-
import { isTelemetryDisabled, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
|
|
16
|
+
import { buildSearchAdoptionTelemetry, isTelemetryDisabled, SEARCH_MODES, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
|
|
17
17
|
import { isUpdateCheckDisabled, printUpdateNotice, startUpdateCheck, stripUpdateCheckFlag, } from './update-check.js';
|
|
18
18
|
import { createCommandContext } from './command-context.js';
|
|
19
19
|
import { createCommandRegistry } from './command-registry.js';
|
|
@@ -137,6 +137,7 @@ Commands:
|
|
|
137
137
|
clusters Browse topic cluster hierarchy
|
|
138
138
|
cluster-conversations <node_id> List conversations in a cluster
|
|
139
139
|
conversations Search and filter conversations
|
|
140
|
+
search "<query>" Search conversation messages (keyword/semantic/hybrid)
|
|
140
141
|
world-state <conversation_id> Get a conversation's world state (slots/threads/events)
|
|
141
142
|
context <conversation_id> Get windowed conversation context
|
|
142
143
|
frustrations Get user frustration detections
|
|
@@ -192,6 +193,12 @@ Conversations flags:
|
|
|
192
193
|
--user-id=ID Filter to a single user
|
|
193
194
|
--environment=ENV Filter by environment (all|development|staging|production)
|
|
194
195
|
|
|
196
|
+
Search flags:
|
|
197
|
+
--mode=keyword|semantic|hybrid Search mode (default hybrid)
|
|
198
|
+
--user-id=ID Filter to a single user
|
|
199
|
+
--time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
|
|
200
|
+
--limit=N Maximum results to return (1-100, default 20)
|
|
201
|
+
|
|
195
202
|
World-state flags:
|
|
196
203
|
--summary-only Return segment summaries only (omit the event stream)
|
|
197
204
|
--event-limit=N Max world-state events to return (1-5000, default 2000)
|
|
@@ -249,6 +256,8 @@ Examples:
|
|
|
249
256
|
moda conversations --search="error" --limit=5
|
|
250
257
|
moda conversations --world-state="enterprise" --outcome=positive
|
|
251
258
|
moda conversations --world-state="refund,billing" --include-world-state
|
|
259
|
+
moda search "billing error" --mode=hybrid
|
|
260
|
+
moda search "refund flow" --mode=semantic --time-range=7d --limit=10
|
|
252
261
|
moda world-state <conversation_id> --summary-only
|
|
253
262
|
moda frustrations --days-back=14 --limit=20
|
|
254
263
|
moda context <conversation_id> --window=3
|
|
@@ -327,6 +336,44 @@ async function attachWorldState(rows) {
|
|
|
327
336
|
}
|
|
328
337
|
}));
|
|
329
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Record response-derived search telemetry from a `/search` payload. The
|
|
341
|
+
* endpoint returns the effective `search_mode` (which may differ from the
|
|
342
|
+
* requested mode after a degrade) plus a first-class `pagination.has_more`
|
|
343
|
+
* (the service fetches a candidate pool larger than the page, so this is a real
|
|
344
|
+
* signal — not a fragile "page filled the limit" heuristic).
|
|
345
|
+
*/
|
|
346
|
+
function recordSearchTelemetry(context, data) {
|
|
347
|
+
const record = asRecord(data);
|
|
348
|
+
if (!record)
|
|
349
|
+
return;
|
|
350
|
+
const results = Array.isArray(record.results) ? record.results : [];
|
|
351
|
+
const effective = typeof record.search_mode === 'string'
|
|
352
|
+
? record.search_mode.toLowerCase()
|
|
353
|
+
: undefined;
|
|
354
|
+
const pagination = asRecord(record.pagination);
|
|
355
|
+
const hasMore = pagination?.has_more;
|
|
356
|
+
context.recordTelemetry({
|
|
357
|
+
search_mode_effective: SEARCH_MODES.includes(effective)
|
|
358
|
+
? effective
|
|
359
|
+
: undefined,
|
|
360
|
+
result_count: results.length,
|
|
361
|
+
has_more: hasMore === true ? 'yes' : hasMore === false ? 'no' : 'na',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/** Record response-derived telemetry from a `/conversations` list payload. */
|
|
365
|
+
function recordConversationsTelemetry(context, data) {
|
|
366
|
+
const record = asRecord(data);
|
|
367
|
+
if (!record)
|
|
368
|
+
return;
|
|
369
|
+
const list = Array.isArray(record.conversations) ? record.conversations : [];
|
|
370
|
+
const pagination = asRecord(record.pagination);
|
|
371
|
+
const hasMore = pagination?.has_more;
|
|
372
|
+
context.recordTelemetry({
|
|
373
|
+
result_count: list.length,
|
|
374
|
+
has_more: hasMore === true ? 'yes' : hasMore === false ? 'no' : 'na',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
330
377
|
export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
|
|
331
378
|
switch (command) {
|
|
332
379
|
case 'prompts': {
|
|
@@ -402,6 +449,7 @@ export async function runCommand(command, positional, flags, positionals = posit
|
|
|
402
449
|
query.set('offset', params.offset.toString());
|
|
403
450
|
const queryString = query.toString() ? `?${query.toString()}` : '';
|
|
404
451
|
const data = await callDataAPI(`/conversations${queryString}`);
|
|
452
|
+
recordConversationsTelemetry(context, data);
|
|
405
453
|
if (params.include_world_state === true) {
|
|
406
454
|
const enriched = asRecord(data) ?? {};
|
|
407
455
|
const list = Array.isArray(enriched.conversations)
|
|
@@ -415,6 +463,30 @@ export async function runCommand(command, positional, flags, positionals = posit
|
|
|
415
463
|
}
|
|
416
464
|
break;
|
|
417
465
|
}
|
|
466
|
+
case 'search': {
|
|
467
|
+
// Positional query mirrors world-state/context/cluster-conversations:
|
|
468
|
+
// reject an empty/whitespace query with the shared CLI input error before
|
|
469
|
+
// it reaches the schema (which also guards via q.min(1)).
|
|
470
|
+
if (!positional || positional.trim() === '') {
|
|
471
|
+
throw new CliInputError('<query> is required', 'Usage: moda search "<query>" [--mode=keyword|semantic|hybrid] [--user-id=ID] [--time-range=RANGE] [--limit=N]');
|
|
472
|
+
}
|
|
473
|
+
const args = flagsToArgs(flags, 'q', positional);
|
|
474
|
+
const params = SearchSchema.parse(args);
|
|
475
|
+
const query = new URLSearchParams();
|
|
476
|
+
query.set('q', params.q);
|
|
477
|
+
if (params.mode)
|
|
478
|
+
query.set('mode', params.mode);
|
|
479
|
+
if (params.user_id)
|
|
480
|
+
query.set('user_id', params.user_id);
|
|
481
|
+
if (params.time_range)
|
|
482
|
+
query.set('time_range', params.time_range);
|
|
483
|
+
if (params.limit)
|
|
484
|
+
query.set('limit', params.limit.toString());
|
|
485
|
+
const data = await callDataAPI(`/search?${query.toString()}`);
|
|
486
|
+
recordSearchTelemetry(context, data);
|
|
487
|
+
context.output.writeData(data);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
418
490
|
case 'world-state': {
|
|
419
491
|
if (!positional) {
|
|
420
492
|
throw new CliInputError('<conversation_id> is required', 'Usage: moda world-state <conversation_id> [--summary-only] [--event-limit=N]');
|
|
@@ -856,6 +928,15 @@ export const commandRegistry = createCommandRegistry([
|
|
|
856
928
|
],
|
|
857
929
|
...dataApiDefaults,
|
|
858
930
|
}),
|
|
931
|
+
legacyCommand({
|
|
932
|
+
name: 'search',
|
|
933
|
+
description: 'Search conversation messages (keyword, semantic, or hybrid)',
|
|
934
|
+
examples: [
|
|
935
|
+
'moda search "billing error"',
|
|
936
|
+
'moda search "refund flow" --mode=semantic --time-range=7d --limit=10',
|
|
937
|
+
],
|
|
938
|
+
...dataApiDefaults,
|
|
939
|
+
}),
|
|
859
940
|
legacyCommand({
|
|
860
941
|
name: 'world-state',
|
|
861
942
|
description: "Get a conversation's world state (slots, threads, events)",
|
|
@@ -1104,6 +1185,14 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1104
1185
|
exitCode: result.exitCode,
|
|
1105
1186
|
durationMs: Date.now() - startedAt,
|
|
1106
1187
|
apiRequestCount: getApiRequestCount(),
|
|
1188
|
+
adoption: buildSearchAdoptionTelemetry({
|
|
1189
|
+
command: definition.name,
|
|
1190
|
+
positional: parsed.positional,
|
|
1191
|
+
flags: commandFlags,
|
|
1192
|
+
outputMode: context.outputMode,
|
|
1193
|
+
recorded: context.getTelemetryMeta(),
|
|
1194
|
+
skillVersion: process.env.MODA_SKILL_VERSION,
|
|
1195
|
+
}),
|
|
1107
1196
|
});
|
|
1108
1197
|
}
|
|
1109
1198
|
if (!updateCheckDisabled) {
|
|
@@ -1122,6 +1211,14 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1122
1211
|
exitCode: 1,
|
|
1123
1212
|
durationMs: Date.now() - startedAt,
|
|
1124
1213
|
apiRequestCount: getApiRequestCount(),
|
|
1214
|
+
adoption: buildSearchAdoptionTelemetry({
|
|
1215
|
+
command: definition.name,
|
|
1216
|
+
positional: parsed.positional,
|
|
1217
|
+
flags: commandFlags,
|
|
1218
|
+
outputMode: context.outputMode,
|
|
1219
|
+
recorded: context.getTelemetryMeta(),
|
|
1220
|
+
skillVersion: process.env.MODA_SKILL_VERSION,
|
|
1221
|
+
}),
|
|
1125
1222
|
});
|
|
1126
1223
|
}
|
|
1127
1224
|
if (context.outputMode === 'agent' || context.outputMode === 'agent-stream') {
|
|
@@ -5,6 +5,17 @@ export interface CommandContextInput {
|
|
|
5
5
|
positionals: string[];
|
|
6
6
|
flags: Record<string, string>;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Response-derived telemetry a handler can surface back to the dispatcher after
|
|
10
|
+
* the API call completes. Static fields (surface, requested mode, query length,
|
|
11
|
+
* etc.) are derived from the command + flags in the dispatcher and don't belong
|
|
12
|
+
* here — only values that require the runtime response are recorded.
|
|
13
|
+
*/
|
|
14
|
+
export interface CommandTelemetryMeta {
|
|
15
|
+
search_mode_effective?: 'na' | 'keyword' | 'semantic' | 'hybrid';
|
|
16
|
+
result_count?: number;
|
|
17
|
+
has_more?: 'na' | 'no' | 'yes';
|
|
18
|
+
}
|
|
8
19
|
export interface CommandContext {
|
|
9
20
|
command: string;
|
|
10
21
|
positional: string | undefined;
|
|
@@ -22,6 +33,10 @@ export interface CommandContext {
|
|
|
22
33
|
quiet: boolean;
|
|
23
34
|
verbose: boolean;
|
|
24
35
|
interactive: boolean;
|
|
36
|
+
/** Merge response-derived telemetry; safe to call multiple times. */
|
|
37
|
+
recordTelemetry(meta: Partial<CommandTelemetryMeta>): void;
|
|
38
|
+
/** Snapshot of telemetry recorded so far by the handler. */
|
|
39
|
+
getTelemetryMeta(): CommandTelemetryMeta;
|
|
25
40
|
}
|
|
26
41
|
export declare function createCommandContext(input: CommandContextInput, options?: {
|
|
27
42
|
commandName?: string;
|
package/dist/command-context.js
CHANGED
|
@@ -12,6 +12,7 @@ export function createCommandContext(input, options = {}) {
|
|
|
12
12
|
!flagEnabled(flags, 'yes') &&
|
|
13
13
|
process.stdin.isTTY === true &&
|
|
14
14
|
process.stderr.isTTY === true;
|
|
15
|
+
const telemetryMeta = {};
|
|
15
16
|
return {
|
|
16
17
|
command: options.commandName ?? input.command,
|
|
17
18
|
positional: input.positional,
|
|
@@ -33,6 +34,12 @@ export function createCommandContext(input, options = {}) {
|
|
|
33
34
|
quiet,
|
|
34
35
|
verbose: flagEnabled(flags, 'verbose'),
|
|
35
36
|
interactive,
|
|
37
|
+
recordTelemetry(meta) {
|
|
38
|
+
Object.assign(telemetryMeta, meta);
|
|
39
|
+
},
|
|
40
|
+
getTelemetryMeta() {
|
|
41
|
+
return telemetryMeta;
|
|
42
|
+
},
|
|
36
43
|
};
|
|
37
44
|
}
|
|
38
45
|
//# sourceMappingURL=command-context.js.map
|
package/dist/schemas.d.ts
CHANGED
|
@@ -141,6 +141,30 @@ export declare const ToolFailureDetailSchema: z.ZodObject<{
|
|
|
141
141
|
include_window?: unknown;
|
|
142
142
|
subtype?: string | undefined;
|
|
143
143
|
}>;
|
|
144
|
+
/**
|
|
145
|
+
* moda search "<query>" — keyword/semantic/hybrid search across conversation
|
|
146
|
+
* messages. `q` arrives as a positional arg; the runCommand handler rejects an
|
|
147
|
+
* empty/whitespace query before parsing, and min(1) guards the schema path too.
|
|
148
|
+
*/
|
|
149
|
+
export declare const SearchSchema: z.ZodObject<{
|
|
150
|
+
q: z.ZodString;
|
|
151
|
+
mode: z.ZodOptional<z.ZodDefault<z.ZodEnum<["keyword", "semantic", "hybrid"]>>>;
|
|
152
|
+
user_id: z.ZodOptional<z.ZodString>;
|
|
153
|
+
time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
|
|
154
|
+
limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
155
|
+
}, "strip", z.ZodTypeAny, {
|
|
156
|
+
q: string;
|
|
157
|
+
mode?: "keyword" | "semantic" | "hybrid" | undefined;
|
|
158
|
+
time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
|
|
159
|
+
limit?: number | undefined;
|
|
160
|
+
user_id?: string | undefined;
|
|
161
|
+
}, {
|
|
162
|
+
q: string;
|
|
163
|
+
mode?: "keyword" | "semantic" | "hybrid" | undefined;
|
|
164
|
+
time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
|
|
165
|
+
limit?: number | undefined;
|
|
166
|
+
user_id?: string | undefined;
|
|
167
|
+
}>;
|
|
144
168
|
/** moda workflows — recurring workflow catalog (read from agent_job_classes). */
|
|
145
169
|
export declare const WorkflowsSchema: z.ZodObject<{
|
|
146
170
|
time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
|
package/dist/schemas.js
CHANGED
|
@@ -76,6 +76,19 @@ export const ToolFailureDetailSchema = z.object({
|
|
|
76
76
|
// `moda workflows` lists the catalog; `moda runs` lists executions.
|
|
77
77
|
const RUN_OUTCOMES = ['running', 'succeeded', 'partial', 'failed', 'loop', 'abandoned', 'unknown'];
|
|
78
78
|
const TIME_RANGES = ['all', '1h', '3d', '7d', '24h', '30d', '90d'];
|
|
79
|
+
const SEARCH_MODES = ['keyword', 'semantic', 'hybrid'];
|
|
80
|
+
/**
|
|
81
|
+
* moda search "<query>" — keyword/semantic/hybrid search across conversation
|
|
82
|
+
* messages. `q` arrives as a positional arg; the runCommand handler rejects an
|
|
83
|
+
* empty/whitespace query before parsing, and min(1) guards the schema path too.
|
|
84
|
+
*/
|
|
85
|
+
export const SearchSchema = z.object({
|
|
86
|
+
q: z.string().min(1).max(500),
|
|
87
|
+
mode: z.enum(SEARCH_MODES).default('hybrid').optional(),
|
|
88
|
+
user_id: z.string().optional(),
|
|
89
|
+
time_range: z.enum(TIME_RANGES).default('all').optional(),
|
|
90
|
+
limit: z.number().min(1).max(100).default(20).optional(),
|
|
91
|
+
});
|
|
79
92
|
/** moda workflows — recurring workflow catalog (read from agent_job_classes). */
|
|
80
93
|
export const WorkflowsSchema = z.object({
|
|
81
94
|
time_range: z.enum(TIME_RANGES).default('all').optional(),
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
+
import type { CliOutputMode } from './output.js';
|
|
2
|
+
import type { CommandTelemetryMeta } from './command-context.js';
|
|
1
3
|
export type TelemetryStatus = 'success' | 'error';
|
|
2
4
|
export type TelemetryErrorType = 'validation_error' | 'api_error' | 'network_error' | 'unknown_error';
|
|
5
|
+
export type SearchSurface = 'none' | 'conversation' | 'world_state' | 'cluster' | 'message' | 'context';
|
|
6
|
+
export type SearchMode = 'na' | 'keyword' | 'semantic' | 'hybrid';
|
|
7
|
+
export type Tristate = 'na' | 'no' | 'yes';
|
|
8
|
+
/**
|
|
9
|
+
* Phase 6 search-adoption telemetry. Every field is optional and the backend
|
|
10
|
+
* defaults absent values, so it is always safe to omit a field we can't derive.
|
|
11
|
+
*/
|
|
12
|
+
export interface SearchAdoptionTelemetry {
|
|
13
|
+
output_mode?: CliOutputMode;
|
|
14
|
+
subcommand?: string;
|
|
15
|
+
search_surface?: SearchSurface;
|
|
16
|
+
search_mode_requested?: SearchMode;
|
|
17
|
+
search_mode_effective?: SearchMode;
|
|
18
|
+
query_length?: number;
|
|
19
|
+
query_term_count?: number;
|
|
20
|
+
result_count?: number;
|
|
21
|
+
zero_result?: Tristate;
|
|
22
|
+
has_more?: Tristate;
|
|
23
|
+
used_msg_index?: 0 | 1;
|
|
24
|
+
skill_version?: string;
|
|
25
|
+
}
|
|
3
26
|
export interface CliUsageTelemetryInput {
|
|
4
27
|
apiKey: string | undefined;
|
|
5
28
|
command: string;
|
|
@@ -9,7 +32,22 @@ export interface CliUsageTelemetryInput {
|
|
|
9
32
|
exitCode: number;
|
|
10
33
|
durationMs: number;
|
|
11
34
|
apiRequestCount: number;
|
|
35
|
+
adoption?: SearchAdoptionTelemetry;
|
|
12
36
|
}
|
|
37
|
+
export declare const SEARCH_MODES: readonly SearchMode[];
|
|
38
|
+
/**
|
|
39
|
+
* Derive the search-adoption telemetry for a command. Static fields come from
|
|
40
|
+
* the command name + flags; response-derived fields (effective mode, result
|
|
41
|
+
* count, has_more) are read from whatever the handler recorded on the context.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildSearchAdoptionTelemetry(input: {
|
|
44
|
+
command: string;
|
|
45
|
+
positional: string | undefined;
|
|
46
|
+
flags: Record<string, string>;
|
|
47
|
+
outputMode: CliOutputMode;
|
|
48
|
+
recorded: CommandTelemetryMeta;
|
|
49
|
+
skillVersion?: string;
|
|
50
|
+
}): SearchAdoptionTelemetry;
|
|
13
51
|
export declare function stripTelemetryFlag(flags: Record<string, string>): Record<string, string>;
|
|
14
52
|
export declare function isTelemetryDisabled(flags: Record<string, string>): boolean;
|
|
15
53
|
export declare function getInvocationId(): string;
|
package/dist/telemetry.js
CHANGED
|
@@ -3,6 +3,109 @@ import { VERSION, resolveModaBaseUrl } from './config.js';
|
|
|
3
3
|
const TELEMETRY_TIMEOUT_MS = 500;
|
|
4
4
|
const INVOCATION_ID = randomUUID();
|
|
5
5
|
const TELEMETRY_FLAG = 'no-telemetry';
|
|
6
|
+
const SEARCH_SURFACE_BY_COMMAND = {
|
|
7
|
+
search: 'message',
|
|
8
|
+
conversations: 'conversation',
|
|
9
|
+
clusters: 'cluster',
|
|
10
|
+
'cluster-conversations': 'cluster',
|
|
11
|
+
'world-state': 'world_state',
|
|
12
|
+
context: 'context',
|
|
13
|
+
};
|
|
14
|
+
export const SEARCH_MODES = ['keyword', 'semantic', 'hybrid'];
|
|
15
|
+
function termCount(query) {
|
|
16
|
+
return query.trim().split(/\s+/).filter(Boolean).length;
|
|
17
|
+
}
|
|
18
|
+
function clampInt(value, min, max) {
|
|
19
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
20
|
+
return undefined;
|
|
21
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Derive the search-adoption telemetry for a command. Static fields come from
|
|
25
|
+
* the command name + flags; response-derived fields (effective mode, result
|
|
26
|
+
* count, has_more) are read from whatever the handler recorded on the context.
|
|
27
|
+
*/
|
|
28
|
+
export function buildSearchAdoptionTelemetry(input) {
|
|
29
|
+
const { command, positional, flags, outputMode, recorded } = input;
|
|
30
|
+
const adoption = {
|
|
31
|
+
output_mode: outputMode,
|
|
32
|
+
search_surface: SEARCH_SURFACE_BY_COMMAND[command] ?? 'none',
|
|
33
|
+
};
|
|
34
|
+
// The query lives in the positional for `moda search`, but in `--search` for
|
|
35
|
+
// the conversations list surface.
|
|
36
|
+
let queryText;
|
|
37
|
+
if (command === 'search')
|
|
38
|
+
queryText = positional;
|
|
39
|
+
else if (command === 'conversations')
|
|
40
|
+
queryText = flags.search;
|
|
41
|
+
// Trim before measuring both dimensions so query_length and query_term_count
|
|
42
|
+
// describe the same (trimmed) query the backend actually sees.
|
|
43
|
+
const trimmedQuery = queryText?.trim() ?? '';
|
|
44
|
+
if (trimmedQuery !== '') {
|
|
45
|
+
adoption.query_length = trimmedQuery.length;
|
|
46
|
+
adoption.query_term_count = termCount(trimmedQuery);
|
|
47
|
+
}
|
|
48
|
+
// search_mode is only meaningful for the message-grain search command.
|
|
49
|
+
if (command === 'search') {
|
|
50
|
+
const requested = (flags.mode ?? '').toLowerCase();
|
|
51
|
+
adoption.search_mode_requested = SEARCH_MODES.includes(requested)
|
|
52
|
+
? requested
|
|
53
|
+
: 'hybrid'; // mirrors SearchSchema's default
|
|
54
|
+
}
|
|
55
|
+
// used_msg_index only applies to the conversation context surface.
|
|
56
|
+
if (command === 'context') {
|
|
57
|
+
adoption.used_msg_index = flags['msg-index'] !== undefined ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
if (recorded.search_mode_effective) {
|
|
60
|
+
adoption.search_mode_effective = recorded.search_mode_effective;
|
|
61
|
+
}
|
|
62
|
+
if (typeof recorded.result_count === 'number') {
|
|
63
|
+
adoption.result_count = recorded.result_count;
|
|
64
|
+
adoption.zero_result = recorded.result_count === 0 ? 'yes' : 'no';
|
|
65
|
+
}
|
|
66
|
+
if (recorded.has_more)
|
|
67
|
+
adoption.has_more = recorded.has_more;
|
|
68
|
+
const skill = input.skillVersion?.trim();
|
|
69
|
+
if (skill)
|
|
70
|
+
adoption.skill_version = skill.slice(0, 64);
|
|
71
|
+
return adoption;
|
|
72
|
+
}
|
|
73
|
+
function serializeAdoption(adoption) {
|
|
74
|
+
if (!adoption)
|
|
75
|
+
return {};
|
|
76
|
+
const out = {};
|
|
77
|
+
if (adoption.output_mode)
|
|
78
|
+
out.output_mode = adoption.output_mode;
|
|
79
|
+
if (adoption.subcommand)
|
|
80
|
+
out.subcommand = adoption.subcommand.slice(0, 64);
|
|
81
|
+
if (adoption.search_surface)
|
|
82
|
+
out.search_surface = adoption.search_surface;
|
|
83
|
+
if (adoption.search_mode_requested) {
|
|
84
|
+
out.search_mode_requested = adoption.search_mode_requested;
|
|
85
|
+
}
|
|
86
|
+
if (adoption.search_mode_effective) {
|
|
87
|
+
out.search_mode_effective = adoption.search_mode_effective;
|
|
88
|
+
}
|
|
89
|
+
const queryLength = clampInt(adoption.query_length, 0, 2000);
|
|
90
|
+
if (queryLength !== undefined)
|
|
91
|
+
out.query_length = queryLength;
|
|
92
|
+
const queryTermCount = clampInt(adoption.query_term_count, 0, 200);
|
|
93
|
+
if (queryTermCount !== undefined)
|
|
94
|
+
out.query_term_count = queryTermCount;
|
|
95
|
+
const resultCount = clampInt(adoption.result_count, 0, 100_000);
|
|
96
|
+
if (resultCount !== undefined)
|
|
97
|
+
out.result_count = resultCount;
|
|
98
|
+
if (adoption.zero_result)
|
|
99
|
+
out.zero_result = adoption.zero_result;
|
|
100
|
+
if (adoption.has_more)
|
|
101
|
+
out.has_more = adoption.has_more;
|
|
102
|
+
if (adoption.used_msg_index !== undefined) {
|
|
103
|
+
out.used_msg_index = adoption.used_msg_index ? 1 : 0;
|
|
104
|
+
}
|
|
105
|
+
if (adoption.skill_version)
|
|
106
|
+
out.skill_version = adoption.skill_version.slice(0, 64);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
6
109
|
export function stripTelemetryFlag(flags) {
|
|
7
110
|
const { [TELEMETRY_FLAG]: _drop, ...rest } = flags;
|
|
8
111
|
return rest;
|
|
@@ -49,6 +152,7 @@ export async function sendCliUsageTelemetry(input) {
|
|
|
49
152
|
platform: process.platform,
|
|
50
153
|
arch: process.arch,
|
|
51
154
|
flags_json: { names: sanitizedFlagNames(input.flags) },
|
|
155
|
+
...serializeAdoption(input.adoption),
|
|
52
156
|
}),
|
|
53
157
|
signal: controller.signal,
|
|
54
158
|
});
|
package/package.json
CHANGED
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: moda-cli
|
|
3
|
-
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: Query Moda's AI agent conversation analytics and manage code-first prompt versions from the terminal — semantic/keyword/hybrid message search, overview KPIs, topic clusters, message context, user frustration detections, tool failures, agent jobs, recurring workflows, active loop detections, and moda prompts status/sync/promote. Use when the user asks about moda, modaflows, conversation analytics, prompt management, user frustrations, agent observability, tool failure debugging, why an agent run partially completed or looped, wants to find conversations or tool calls about a topic, or wants to investigate how their AI agent is performing.
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Moda CLI
|
|
@@ -13,15 +14,26 @@ filter. Analytics commands are read-only. Prompt management includes both
|
|
|
13
14
|
read-only commands (`prompts status`, `prompts diff`) and commands that sync
|
|
14
15
|
or promote remote state (`prompts sync`, `prompts promote`).
|
|
15
16
|
|
|
17
|
+
**`moda search` is the primary way to find anything by content.** It runs
|
|
18
|
+
message-grain semantic, keyword, or hybrid retrieval across every message —
|
|
19
|
+
including tool calls and tool results — and returns ranked snippets with
|
|
20
|
+
direct anchors (`conversation_id` + `message_index`) you can hand straight to
|
|
21
|
+
`moda context`. Reach for it first whenever the question is "where did X
|
|
22
|
+
happen?" or "find conversations/tool calls about Y". Use `moda conversations`
|
|
23
|
+
only when you need to *list/filter* by structured fields (cluster, user,
|
|
24
|
+
environment, outcome), not to search by meaning.
|
|
25
|
+
|
|
16
26
|
## When to use
|
|
17
27
|
|
|
18
28
|
Activate this skill when the user wants to:
|
|
19
29
|
|
|
30
|
+
- **Find conversations or tool calls about a topic, error, or behavior**
|
|
31
|
+
(semantic/keyword/hybrid search) — start with `moda search`
|
|
20
32
|
- Inspect agent health: conversation volume, frustration rate, tool failures
|
|
21
33
|
- Debug a specific frustrated conversation
|
|
22
34
|
- Investigate which tools are failing and why
|
|
23
35
|
- Browse conversation topics (clusters)
|
|
24
|
-
-
|
|
36
|
+
- List/filter conversations by user, environment, cluster, outcome, or time
|
|
25
37
|
- Manage code-first prompt versions when the user explicitly asks for prompt
|
|
26
38
|
sync, prompt status, or prompt promotion
|
|
27
39
|
|
|
@@ -41,6 +53,9 @@ Optional:
|
|
|
41
53
|
|
|
42
54
|
- `MODA_BASE_URL` defaults to `https://moda.dev`; override only for
|
|
43
55
|
self-hosted or staging.
|
|
56
|
+
- `MODA_SKILL_VERSION` — export to `2.0.0` so search-adoption telemetry can
|
|
57
|
+
attribute usage to this skill version. Set it once per session:
|
|
58
|
+
`export MODA_SKILL_VERSION=2.0.0`.
|
|
44
59
|
|
|
45
60
|
## Setup
|
|
46
61
|
|
|
@@ -65,10 +80,63 @@ export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
|
|
|
65
80
|
|
|
66
81
|
## Procedure
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
Two entry points depending on the question:
|
|
84
|
+
|
|
85
|
+
- **"Find / where did X happen?"** → start with `moda search` (semantic,
|
|
86
|
+
keyword, or hybrid retrieval over every message), then `moda context` on the
|
|
87
|
+
returned anchors.
|
|
88
|
+
- **"How healthy is the agent overall?"** → start with `moda overview`, then
|
|
89
|
+
drill down (clusters → conversations → context).
|
|
90
|
+
|
|
91
|
+
Default investigation pattern for content questions: **search → context**.
|
|
92
|
+
For health questions: **broad → narrow → context**.
|
|
93
|
+
|
|
94
|
+
### 1. Search messages (primary content-discovery path)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
moda search "refund flow timeout" # hybrid (default)
|
|
98
|
+
moda search "customer asked for a refund" --mode=semantic
|
|
99
|
+
moda search "SEARCH_NO_RESULTS" --mode=keyword
|
|
100
|
+
moda search "checkout error" --time-range=7d --limit=10
|
|
101
|
+
moda search "stripe.charges.create failed" --user-id=<id>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Searches conversation messages at message grain, **including tool calls and
|
|
105
|
+
tool results** (tool name, input arguments, and output previews are embedded),
|
|
106
|
+
so it finds where an agent *did* something, not just where it talked about it.
|
|
107
|
+
|
|
108
|
+
Flags: `--mode=keyword|semantic|hybrid` (default `hybrid`), `--user-id`,
|
|
109
|
+
`--time-range` (`all|1h|3d|7d|24h|30d|90d`, default `all`), `--limit` (1–100,
|
|
110
|
+
default 20). The query is a positional arg (max 500 chars).
|
|
111
|
+
|
|
112
|
+
Choosing a mode:
|
|
113
|
+
|
|
114
|
+
- `hybrid` (default) — best general recall; fuses keyword + semantic. Use it
|
|
115
|
+
unless you have a reason not to.
|
|
116
|
+
- `keyword` — exact substring/identifier matches (error codes, tool names,
|
|
117
|
+
IDs). `score` is the integer match count.
|
|
118
|
+
- `semantic` — meaning-based; finds paraphrases the user never typed
|
|
119
|
+
verbatim. `score` is cosine similarity.
|
|
120
|
+
|
|
121
|
+
Each result row carries `conversation_id`, `message_index`, `role`,
|
|
122
|
+
`timestamp`, `snippet`, `score`, and `cluster_id` / `cluster_name`. The
|
|
123
|
+
response also reports `search_mode` — the mode that **actually ran**; a
|
|
124
|
+
`semantic`/`hybrid` request transparently degrades to `keyword` when no
|
|
125
|
+
embedding provider is reachable, so check it before trusting semantic ranking.
|
|
126
|
+
`score` scales differ per mode (see above) — branch on `search_mode`, never
|
|
127
|
+
compare scores across modes.
|
|
128
|
+
|
|
129
|
+
Hand the anchors straight to `moda context`:
|
|
70
130
|
|
|
71
|
-
|
|
131
|
+
```bash
|
|
132
|
+
# Find, then read the exact turn
|
|
133
|
+
hit=$(moda search "double charge" --limit=1 | jq -r '.results[0]')
|
|
134
|
+
cid=$(echo "$hit" | jq -r '.conversation_id')
|
|
135
|
+
idx=$(echo "$hit" | jq -r '.message_index')
|
|
136
|
+
moda context "$cid" --msg-index="$idx"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 2. Overview (start here for health questions)
|
|
72
140
|
|
|
73
141
|
```bash
|
|
74
142
|
moda overview # last 7 days
|
|
@@ -78,7 +146,7 @@ moda overview --days-back=30
|
|
|
78
146
|
Returns total conversations, trend, frustration rate, tool failure summary,
|
|
79
147
|
top clusters, recent activity.
|
|
80
148
|
|
|
81
|
-
###
|
|
149
|
+
### 3. Browse topic clusters
|
|
82
150
|
|
|
83
151
|
```bash
|
|
84
152
|
moda clusters # root-level
|
|
@@ -86,15 +154,21 @@ moda clusters --parent-id=<node_id> # drill in
|
|
|
86
154
|
moda cluster-conversations <node_id> # conversations in cluster
|
|
87
155
|
```
|
|
88
156
|
|
|
89
|
-
###
|
|
157
|
+
### 3b. List / filter conversations (structured, not semantic)
|
|
158
|
+
|
|
159
|
+
Use `moda conversations` to enumerate or filter by structured fields — not to
|
|
160
|
+
search by meaning (use `moda search` for that). The `--search` flag here is a
|
|
161
|
+
plain keyword filter over conversation text.
|
|
90
162
|
|
|
91
163
|
```bash
|
|
92
|
-
|
|
164
|
+
moda conversations --user-id=<id> --time-range=7d
|
|
165
|
+
moda conversations --cluster-id=<node_id> --environment=production
|
|
166
|
+
moda conversations --search="timeout" --limit=20 # keyword filter only
|
|
93
167
|
```
|
|
94
168
|
|
|
95
169
|
Filters: `--search`, `--cluster-id`, `--user-id`, `--time-range`
|
|
96
170
|
(`all|1h|3d|7d|24h|30d|90d`), `--environment`
|
|
97
|
-
(`all|development|staging|production`), `--limit`, `--offset`.
|
|
171
|
+
(`all|development|staging|production`), `--outcome`, `--limit`, `--offset`.
|
|
98
172
|
|
|
99
173
|
### 4. Read a single conversation
|
|
100
174
|
|
|
@@ -238,6 +312,22 @@ and future evals.
|
|
|
238
312
|
|
|
239
313
|
## Common workflow recipes
|
|
240
314
|
|
|
315
|
+
### Find where something happened (search → context)
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# Semantic: find the moment a user expressed an intent, in their own words
|
|
319
|
+
moda search "user wants to cancel subscription" --mode=semantic --limit=5 \
|
|
320
|
+
| jq -r '.results[] | "\(.conversation_id) @\(.message_index) \(.snippet)"'
|
|
321
|
+
|
|
322
|
+
# Keyword: locate every tool call/result mentioning an exact identifier
|
|
323
|
+
moda search "stripe.refunds.create" --mode=keyword --limit=10
|
|
324
|
+
|
|
325
|
+
# Search, then open the top hit at the exact turn
|
|
326
|
+
moda search "rate limit exceeded" --limit=1 \
|
|
327
|
+
| jq -r '.results[0] | "\(.conversation_id) \(.message_index)"' \
|
|
328
|
+
| xargs -n2 sh -c 'moda context "$0" --msg-index="$1"'
|
|
329
|
+
```
|
|
330
|
+
|
|
241
331
|
### Daily health check
|
|
242
332
|
|
|
243
333
|
```bash
|
|
@@ -332,6 +422,14 @@ A flag value didn't match the schema. Check enums (`time_range` must be
|
|
|
332
422
|
Widen the window: try `--days-back=30` or `--time-range=30d`. Confirm with
|
|
333
423
|
`moda overview` that data exists in the tenant at all.
|
|
334
424
|
|
|
425
|
+
**`moda search` returns nothing, or semantic feels like keyword**
|
|
426
|
+
First widen `--time-range` (default is `all`, so an empty result usually means
|
|
427
|
+
the phrasing, not the window). If the response `search_mode` came back
|
|
428
|
+
`keyword` for a `semantic`/`hybrid` request, the embedding provider was
|
|
429
|
+
unreachable and search degraded — retry, or fall back to `--mode=keyword` with
|
|
430
|
+
exact identifiers. For meaning-based queries prefer `--mode=semantic`; for
|
|
431
|
+
exact error codes, tool names, or IDs prefer `--mode=keyword`.
|
|
432
|
+
|
|
335
433
|
**`moda: command not found`**
|
|
336
434
|
Either install globally (`npm install -g @moda-ai/cli`) or invoke through
|
|
337
435
|
npx: `npx -p @moda-ai/cli moda <command>`.
|
|
@@ -341,10 +439,11 @@ npx: `npx -p @moda-ai/cli moda <command>`.
|
|
|
341
439
|
| Command | Purpose |
|
|
342
440
|
|---|---|
|
|
343
441
|
| `moda init` | Browser auth + provision API key + install agent rules |
|
|
442
|
+
| `moda search "<query>"` | **Message-grain semantic/keyword/hybrid search (incl. tool calls); primary content-discovery path** |
|
|
344
443
|
| `moda overview` | Dashboard KPIs, top clusters, recent activity |
|
|
345
444
|
| `moda clusters` | Browse topic cluster hierarchy |
|
|
346
445
|
| `moda cluster-conversations <node_id>` | Conversations in a cluster |
|
|
347
|
-
| `moda conversations` |
|
|
446
|
+
| `moda conversations` | List/filter conversations by structured fields |
|
|
348
447
|
| `moda context <conversation_id>` | Windowed message context (max 5 per side) |
|
|
349
448
|
| `moda frustrations` | User frustration detections with evidence |
|
|
350
449
|
| `moda tool-failures` | Tool failure overview |
|
|
@@ -359,6 +458,9 @@ npx: `npx -p @moda-ai/cli moda <command>`.
|
|
|
359
458
|
| `moda prompts sync` | Upload changed prompt versions and update lockfile |
|
|
360
459
|
| `moda prompts promote <key>` | Move a remote prompt label |
|
|
361
460
|
|
|
461
|
+
`moda search` flags: `--mode=keyword|semantic|hybrid` (default `hybrid`),
|
|
462
|
+
`--user-id`, `--time-range=<range>`, `--limit=N` (1–100, default 20).
|
|
463
|
+
|
|
362
464
|
Common flags across most commands: `--days-back=N` (1–90), `--limit=N`,
|
|
363
465
|
`--offset=N`, `--time-range=<range>`. Runs commands also accept
|
|
364
466
|
`--outcome=<outcome>`, `--class-id=<workflow_class_id>`, and `--user-id=<id>`.
|