@moda-ai/cli 1.7.0 → 1.8.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/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, 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
@@ -137,6 +137,8 @@ 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)
141
+ world-state <conversation_id> Get a conversation's world state (slots/threads/events)
140
142
  context <conversation_id> Get windowed conversation context
141
143
  frustrations Get user frustration detections
142
144
  tool-failures Get tool failure overview
@@ -183,6 +185,24 @@ Common flags:
183
185
  --no-tui Show raw analyst stream instead of dashboard
184
186
  --no-update-check Skip the daily new-version check (or MODA_CLI_UPDATE_CHECK=0)
185
187
 
188
+ Conversations flags:
189
+ --search=TEXT Substring match on the conversation summary
190
+ --world-state=KEYWORDS Match world-state content (slots + durable profile); comma = AND
191
+ --outcome=any|positive|negative Filter by conversation outcome (trajectory + frustration)
192
+ --include-world-state Attach each result's world-state summary
193
+ --user-id=ID Filter to a single user
194
+ --environment=ENV Filter by environment (all|development|staging|production)
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
+
202
+ World-state flags:
203
+ --summary-only Return segment summaries only (omit the event stream)
204
+ --event-limit=N Max world-state events to return (1-5000, default 2000)
205
+
186
206
  Runs flags:
187
207
  --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
188
208
  --class-id=ID Filter to a single workflow class (e.g. cron:helpscout-triage-hourly)
@@ -234,6 +254,11 @@ Examples:
234
254
  moda overview --days-back=30
235
255
  moda clusters --time-range=7d
236
256
  moda conversations --search="error" --limit=5
257
+ moda conversations --world-state="enterprise" --outcome=positive
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
261
+ moda world-state <conversation_id> --summary-only
237
262
  moda frustrations --days-back=14 --limit=20
238
263
  moda context <conversation_id> --window=3
239
264
  moda workflows --time-range=7d
@@ -292,6 +317,25 @@ async function printWithOptionalWindows(enriched, listKey, includeWindow, window
292
317
  const withWindows = await attachWindows(list, windowSize);
293
318
  output.writeData({ ...enriched, [listKey]: withWindows });
294
319
  }
320
+ // Hydrate each conversation row with its world-state summary (slots + segment
321
+ // summaries, no event stream) via the drill-in endpoint. Mirrors attachWindows:
322
+ // best-effort per row, never throws.
323
+ async function attachWorldState(rows) {
324
+ return Promise.all(rows.map(async (row) => {
325
+ const conv_id = asString(row.conversation_id) ?? '';
326
+ if (!conv_id) {
327
+ return { ...row, world_state: null };
328
+ }
329
+ try {
330
+ const data = await callDataAPI(`/conversations/${conv_id}/world-state?summary_only=1`);
331
+ return { ...row, world_state: data };
332
+ }
333
+ catch (err) {
334
+ const message = err instanceof Error ? err.message : String(err);
335
+ return { ...row, world_state: { error: message } };
336
+ }
337
+ }));
338
+ }
295
339
  export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
296
340
  switch (command) {
297
341
  case 'prompts': {
@@ -357,12 +401,65 @@ export async function runCommand(command, positional, flags, positionals = posit
357
401
  query.set('time_range', params.time_range);
358
402
  if (params.environment)
359
403
  query.set('environment', params.environment);
404
+ if (params.world_state)
405
+ query.set('world_state', params.world_state);
406
+ if (params.outcome && params.outcome !== 'any')
407
+ query.set('outcome', params.outcome);
360
408
  if (params.limit)
361
409
  query.set('limit', params.limit.toString());
362
410
  if (params.offset !== undefined)
363
411
  query.set('offset', params.offset.toString());
364
412
  const queryString = query.toString() ? `?${query.toString()}` : '';
365
413
  const data = await callDataAPI(`/conversations${queryString}`);
414
+ if (params.include_world_state === true) {
415
+ const enriched = asRecord(data) ?? {};
416
+ const list = Array.isArray(enriched.conversations)
417
+ ? enriched.conversations
418
+ : [];
419
+ const withState = await attachWorldState(list);
420
+ context.output.writeData({ ...enriched, conversations: withState });
421
+ }
422
+ else {
423
+ context.output.writeData(data);
424
+ }
425
+ break;
426
+ }
427
+ case 'search': {
428
+ // Positional query mirrors world-state/context/cluster-conversations:
429
+ // reject an empty/whitespace query with the shared CLI input error before
430
+ // it reaches the schema (which also guards via q.min(1)).
431
+ if (!positional || positional.trim() === '') {
432
+ throw new CliInputError('<query> is required', 'Usage: moda search "<query>" [--mode=keyword|semantic|hybrid] [--user-id=ID] [--time-range=RANGE] [--limit=N]');
433
+ }
434
+ const args = flagsToArgs(flags, 'q', positional);
435
+ const params = SearchSchema.parse(args);
436
+ const query = new URLSearchParams();
437
+ query.set('q', params.q);
438
+ if (params.mode)
439
+ query.set('mode', params.mode);
440
+ if (params.user_id)
441
+ query.set('user_id', params.user_id);
442
+ if (params.time_range)
443
+ query.set('time_range', params.time_range);
444
+ if (params.limit)
445
+ query.set('limit', params.limit.toString());
446
+ const data = await callDataAPI(`/search?${query.toString()}`);
447
+ context.output.writeData(data);
448
+ break;
449
+ }
450
+ case 'world-state': {
451
+ if (!positional) {
452
+ throw new CliInputError('<conversation_id> is required', 'Usage: moda world-state <conversation_id> [--summary-only] [--event-limit=N]');
453
+ }
454
+ const args = flagsToArgs(flags, 'conversation_id', positional);
455
+ const params = WorldStateSchema.parse(args);
456
+ const query = new URLSearchParams();
457
+ if (params.summary_only === true)
458
+ query.set('summary_only', '1');
459
+ if (params.event_limit)
460
+ query.set('event_limit', params.event_limit.toString());
461
+ const queryString = query.toString() ? `?${query.toString()}` : '';
462
+ const data = await callDataAPI(`/conversations/${params.conversation_id}/world-state${queryString}`);
366
463
  context.output.writeData(data);
367
464
  break;
368
465
  }
@@ -784,7 +881,29 @@ export const commandRegistry = createCommandRegistry([
784
881
  legacyCommand({
785
882
  name: 'conversations',
786
883
  description: 'Search and filter conversations',
787
- examples: ['moda conversations --search="error" --limit=5'],
884
+ examples: [
885
+ 'moda conversations --search="error" --limit=5',
886
+ 'moda conversations --world-state="enterprise" --outcome=positive',
887
+ 'moda conversations --world-state="refund,enterprise" --include-world-state',
888
+ ],
889
+ ...dataApiDefaults,
890
+ }),
891
+ legacyCommand({
892
+ name: 'search',
893
+ description: 'Search conversation messages (keyword, semantic, or hybrid)',
894
+ examples: [
895
+ 'moda search "billing error"',
896
+ 'moda search "refund flow" --mode=semantic --time-range=7d --limit=10',
897
+ ],
898
+ ...dataApiDefaults,
899
+ }),
900
+ legacyCommand({
901
+ name: 'world-state',
902
+ description: "Get a conversation's world state (slots, threads, events)",
903
+ examples: [
904
+ 'moda world-state <conversation_id>',
905
+ 'moda world-state <conversation_id> --summary-only',
906
+ ],
788
907
  ...dataApiDefaults,
789
908
  }),
790
909
  legacyCommand({
package/dist/schemas.d.ts CHANGED
@@ -35,6 +35,9 @@ export declare const ConversationsSchema: z.ZodObject<{
35
35
  user_id: z.ZodOptional<z.ZodString>;
36
36
  time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "24h", "7d", "30d", "90d"]>>>;
37
37
  environment: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "development", "staging", "production"]>>>;
38
+ world_state: z.ZodOptional<z.ZodString>;
39
+ outcome: z.ZodOptional<z.ZodDefault<z.ZodEnum<["any", "positive", "negative"]>>>;
40
+ include_world_state: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
38
41
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
39
42
  offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
40
43
  }, "strip", z.ZodTypeAny, {
@@ -45,6 +48,9 @@ export declare const ConversationsSchema: z.ZodObject<{
45
48
  offset?: number | undefined;
46
49
  cluster_id?: string | undefined;
47
50
  user_id?: string | undefined;
51
+ world_state?: string | undefined;
52
+ outcome?: "any" | "positive" | "negative" | undefined;
53
+ include_world_state?: boolean | undefined;
48
54
  }, {
49
55
  search?: string | undefined;
50
56
  environment?: "staging" | "all" | "development" | "production" | undefined;
@@ -53,6 +59,23 @@ export declare const ConversationsSchema: z.ZodObject<{
53
59
  offset?: number | undefined;
54
60
  cluster_id?: string | undefined;
55
61
  user_id?: string | undefined;
62
+ world_state?: string | undefined;
63
+ outcome?: "any" | "positive" | "negative" | undefined;
64
+ include_world_state?: unknown;
65
+ }>;
66
+ /** moda world-state <conversationId> — per-conversation world-state drill-in. */
67
+ export declare const WorldStateSchema: z.ZodObject<{
68
+ conversation_id: z.ZodString;
69
+ summary_only: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
70
+ event_limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
71
+ }, "strip", z.ZodTypeAny, {
72
+ conversation_id: string;
73
+ summary_only?: boolean | undefined;
74
+ event_limit?: number | undefined;
75
+ }, {
76
+ conversation_id: string;
77
+ summary_only?: unknown;
78
+ event_limit?: number | undefined;
56
79
  }>;
57
80
  export declare const ContextSchema: z.ZodObject<{
58
81
  conversation_id: z.ZodString;
@@ -118,6 +141,30 @@ export declare const ToolFailureDetailSchema: z.ZodObject<{
118
141
  include_window?: unknown;
119
142
  subtype?: string | undefined;
120
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
+ }>;
121
168
  /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
122
169
  export declare const WorkflowsSchema: z.ZodObject<{
123
170
  time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
package/dist/schemas.js CHANGED
@@ -25,9 +25,23 @@ export const ConversationsSchema = z.object({
25
25
  user_id: z.string().optional(),
26
26
  time_range: z.enum(['all', '1h', '3d', '24h', '7d', '30d', '90d']).default('all').optional(),
27
27
  environment: z.enum(['all', 'development', 'staging', 'production']).default('all').optional(),
28
+ // Keyword(s) matched against conversation world-state content (segment slots
29
+ // + carried-in durable profile slots). Comma-separated terms are ANDed.
30
+ // max(200) mirrors the backend ConversationsQueryDto @MaxLength(200) so an
31
+ // over-long value fails locally instead of round-tripping a 400.
32
+ world_state: z.string().max(200).optional(),
33
+ // Filter by conversation outcome (trajectory + frustration).
34
+ outcome: z.enum(['any', 'positive', 'negative']).default('any').optional(),
35
+ include_world_state: boolFlag(),
28
36
  limit: z.number().min(1).max(100).default(20).optional(),
29
37
  offset: z.number().min(0).default(0).optional(),
30
38
  });
39
+ /** moda world-state <conversationId> — per-conversation world-state drill-in. */
40
+ export const WorldStateSchema = z.object({
41
+ conversation_id: z.string(),
42
+ summary_only: boolFlag(),
43
+ event_limit: z.number().min(1).max(5000).default(2000).optional(),
44
+ });
31
45
  export const ContextSchema = z.object({
32
46
  conversation_id: z.string(),
33
47
  msg_index: z.number().min(0).optional(),
@@ -62,6 +76,19 @@ export const ToolFailureDetailSchema = z.object({
62
76
  // `moda workflows` lists the catalog; `moda runs` lists executions.
63
77
  const RUN_OUTCOMES = ['running', 'succeeded', 'partial', 'failed', 'loop', 'abandoned', 'unknown'];
64
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
+ });
65
92
  /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
66
93
  export const WorkflowsSchema = z.object({
67
94
  time_range: z.enum(TIME_RANGES).default('all').optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {