@moda-ai/cli 1.6.0 → 1.8.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 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, 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,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
+ world-state <conversation_id> Get a conversation's world state (slots/threads/events)
140
141
  context <conversation_id> Get windowed conversation context
141
142
  frustrations Get user frustration detections
142
143
  tool-failures Get tool failure overview
@@ -183,6 +184,18 @@ Common flags:
183
184
  --no-tui Show raw analyst stream instead of dashboard
184
185
  --no-update-check Skip the daily new-version check (or MODA_CLI_UPDATE_CHECK=0)
185
186
 
187
+ Conversations flags:
188
+ --search=TEXT Substring match on the conversation summary
189
+ --world-state=KEYWORDS Match world-state content (slots + durable profile); comma = AND
190
+ --outcome=any|positive|negative Filter by conversation outcome (trajectory + frustration)
191
+ --include-world-state Attach each result's world-state summary
192
+ --user-id=ID Filter to a single user
193
+ --environment=ENV Filter by environment (all|development|staging|production)
194
+
195
+ World-state flags:
196
+ --summary-only Return segment summaries only (omit the event stream)
197
+ --event-limit=N Max world-state events to return (1-5000, default 2000)
198
+
186
199
  Runs flags:
187
200
  --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
188
201
  --class-id=ID Filter to a single workflow class (e.g. cron:helpscout-triage-hourly)
@@ -198,6 +211,8 @@ Init flags:
198
211
  --no-sdk-integrate Skip SDK integration (alias: --no-sdk-guide)
199
212
  --ci-cd Create GitHub Actions Moda validation workflow
200
213
  --no-ci-cd Skip CI/CD workflow creation
214
+ --autosync Create GitHub Actions workflow to sync prompts + skills to Moda on push to main
215
+ --no-autosync Skip autosync workflow creation
201
216
  --offline Run final validation offline (skip the online telemetry check)
202
217
 
203
218
  Examples:
@@ -228,9 +243,13 @@ Examples:
228
243
  moda harness scan --json
229
244
  moda harness explain --agent=agent_support
230
245
  moda init --ci-cd
246
+ moda init --autosync
231
247
  moda overview --days-back=30
232
248
  moda clusters --time-range=7d
233
249
  moda conversations --search="error" --limit=5
250
+ moda conversations --world-state="enterprise" --outcome=positive
251
+ moda conversations --world-state="refund,billing" --include-world-state
252
+ moda world-state <conversation_id> --summary-only
234
253
  moda frustrations --days-back=14 --limit=20
235
254
  moda context <conversation_id> --window=3
236
255
  moda workflows --time-range=7d
@@ -289,6 +308,25 @@ async function printWithOptionalWindows(enriched, listKey, includeWindow, window
289
308
  const withWindows = await attachWindows(list, windowSize);
290
309
  output.writeData({ ...enriched, [listKey]: withWindows });
291
310
  }
311
+ // Hydrate each conversation row with its world-state summary (slots + segment
312
+ // summaries, no event stream) via the drill-in endpoint. Mirrors attachWindows:
313
+ // best-effort per row, never throws.
314
+ async function attachWorldState(rows) {
315
+ return Promise.all(rows.map(async (row) => {
316
+ const conv_id = asString(row.conversation_id) ?? '';
317
+ if (!conv_id) {
318
+ return { ...row, world_state: null };
319
+ }
320
+ try {
321
+ const data = await callDataAPI(`/conversations/${conv_id}/world-state?summary_only=1`);
322
+ return { ...row, world_state: data };
323
+ }
324
+ catch (err) {
325
+ const message = err instanceof Error ? err.message : String(err);
326
+ return { ...row, world_state: { error: message } };
327
+ }
328
+ }));
329
+ }
292
330
  export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
293
331
  switch (command) {
294
332
  case 'prompts': {
@@ -354,12 +392,42 @@ export async function runCommand(command, positional, flags, positionals = posit
354
392
  query.set('time_range', params.time_range);
355
393
  if (params.environment)
356
394
  query.set('environment', params.environment);
395
+ if (params.world_state)
396
+ query.set('world_state', params.world_state);
397
+ if (params.outcome && params.outcome !== 'any')
398
+ query.set('outcome', params.outcome);
357
399
  if (params.limit)
358
400
  query.set('limit', params.limit.toString());
359
401
  if (params.offset !== undefined)
360
402
  query.set('offset', params.offset.toString());
361
403
  const queryString = query.toString() ? `?${query.toString()}` : '';
362
404
  const data = await callDataAPI(`/conversations${queryString}`);
405
+ if (params.include_world_state === true) {
406
+ const enriched = asRecord(data) ?? {};
407
+ const list = Array.isArray(enriched.conversations)
408
+ ? enriched.conversations
409
+ : [];
410
+ const withState = await attachWorldState(list);
411
+ context.output.writeData({ ...enriched, conversations: withState });
412
+ }
413
+ else {
414
+ context.output.writeData(data);
415
+ }
416
+ break;
417
+ }
418
+ case 'world-state': {
419
+ if (!positional) {
420
+ throw new CliInputError('<conversation_id> is required', 'Usage: moda world-state <conversation_id> [--summary-only] [--event-limit=N]');
421
+ }
422
+ const args = flagsToArgs(flags, 'conversation_id', positional);
423
+ const params = WorldStateSchema.parse(args);
424
+ const query = new URLSearchParams();
425
+ if (params.summary_only === true)
426
+ query.set('summary_only', '1');
427
+ if (params.event_limit)
428
+ query.set('event_limit', params.event_limit.toString());
429
+ const queryString = query.toString() ? `?${query.toString()}` : '';
430
+ const data = await callDataAPI(`/conversations/${params.conversation_id}/world-state${queryString}`);
363
431
  context.output.writeData(data);
364
432
  break;
365
433
  }
@@ -581,7 +649,7 @@ export const commandRegistry = createCommandRegistry([
581
649
  {
582
650
  name: 'init',
583
651
  description: 'Set up Moda in your project',
584
- examples: ['moda init', 'moda init --sdk-integrate', 'moda init --ci-cd'],
652
+ examples: ['moda init', 'moda init --sdk-integrate', 'moda init --ci-cd', 'moda init --autosync'],
585
653
  auth: 'none',
586
654
  outputModes: ['human', 'json', 'agent'],
587
655
  mutability: 'write',
@@ -781,7 +849,20 @@ export const commandRegistry = createCommandRegistry([
781
849
  legacyCommand({
782
850
  name: 'conversations',
783
851
  description: 'Search and filter conversations',
784
- examples: ['moda conversations --search="error" --limit=5'],
852
+ examples: [
853
+ 'moda conversations --search="error" --limit=5',
854
+ 'moda conversations --world-state="enterprise" --outcome=positive',
855
+ 'moda conversations --world-state="refund,enterprise" --include-world-state',
856
+ ],
857
+ ...dataApiDefaults,
858
+ }),
859
+ legacyCommand({
860
+ name: 'world-state',
861
+ description: "Get a conversation's world state (slots, threads, events)",
862
+ examples: [
863
+ 'moda world-state <conversation_id>',
864
+ 'moda world-state <conversation_id> --summary-only',
865
+ ],
785
866
  ...dataApiDefaults,
786
867
  }),
787
868
  legacyCommand({
@@ -11,7 +11,7 @@ export interface InitPhaseResult {
11
11
  details?: string;
12
12
  }
13
13
  export interface InitSetupAction {
14
- category: 'sdk' | 'harness' | 'hooks' | 'prompts' | 'skills' | 'ci-cd' | 'validation';
14
+ category: 'sdk' | 'harness' | 'hooks' | 'prompts' | 'skills' | 'ci-cd' | 'autosync' | 'validation';
15
15
  title: string;
16
16
  status: 'planned' | 'applied' | 'skipped' | 'manual';
17
17
  approved: boolean;
@@ -13,6 +13,7 @@ import { runSkillSync } from '../skills.js';
13
13
  import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
14
14
  import { runSdkIntegration } from './sdk-integration.js';
15
15
  import { createCommandContext } from '../command-context.js';
16
+ import { VERSION } from '../config.js';
16
17
  import { CliInputError } from '../errors.js';
17
18
  import { runHarnessCommand } from '../harness.js';
18
19
  import { buildDoctorReport } from '../doctor.js';
@@ -136,6 +137,7 @@ export async function runInit(flags, options = {}) {
136
137
  installHooks: false,
137
138
  integrateSdk: await shouldIntegrateSdk(flags, yes),
138
139
  configureCiCd: await shouldConfigureCiCd(flags, yes),
140
+ configureAutosync: await shouldConfigureAutosync(flags, yes),
139
141
  });
140
142
  log(renderInitSetupPlan(setupPlan));
141
143
  emit('plan', 'Setup plan ready.', 'completed');
@@ -235,6 +237,12 @@ export async function runInit(flags, options = {}) {
235
237
  log(renderInitNote(`Wrote ${workflowPath}`));
236
238
  markApplied(setupPlan, 'ci-cd');
237
239
  }
240
+ const configureAutosync = setupPlan.find((action) => action.category === 'autosync')?.approved === true;
241
+ if (configureAutosync) {
242
+ const workflowPath = writeAutosyncWorkflow();
243
+ log(renderInitNote(`Wrote ${workflowPath}`));
244
+ markApplied(setupPlan, 'autosync');
245
+ }
238
246
  log(renderInitNote('Reusing approved Step 4 harness report; no second analyst run needed'));
239
247
  markApplied(setupPlan, 'harness');
240
248
  markApplied(setupPlan, 'hooks', ingestCodingAgentTraces ? undefined : 'Coding-agent trace capture is temporarily unavailable.');
@@ -337,6 +345,15 @@ function buildSetupPlan(options) {
337
345
  command: 'moda doctor --online --json',
338
346
  reason: options.configureCiCd ? undefined : 'No CI/CD files are changed until a project-specific plan is approved.',
339
347
  },
348
+ {
349
+ category: 'autosync',
350
+ title: 'Autosync prompts and skills to Moda on push to main',
351
+ status: options.configureAutosync ? 'planned' : 'manual',
352
+ approved: options.configureAutosync,
353
+ files: ['.github/workflows/moda-autosync.yml'],
354
+ command: 'moda prompts sync && moda skills sync',
355
+ reason: options.configureAutosync ? undefined : 'No autosync workflow is written until it is explicitly approved (--autosync).',
356
+ },
340
357
  {
341
358
  category: 'validation',
342
359
  title: 'Run Moda doctor online to confirm telemetry is reaching Moda',
@@ -364,6 +381,13 @@ function changedFiles(setupPlan) {
364
381
  .filter((action) => action.status === 'applied')
365
382
  .flatMap((action) => action.files))].filter((file) => !file.includes('.env'));
366
383
  }
384
+ // The CLI version generated workflows pin to. Pinning (rather than `@latest`,
385
+ // which resolves at CI run time) keeps a future compromised `@moda-ai/cli`
386
+ // publish from silently executing in adopters' CI with their MODA_API_KEY /
387
+ // MODA_TENANT_ID present — especially important for autosync, which *writes* to
388
+ // the Moda registry. The pin tracks the CLI that generated the file, so each
389
+ // `moda init` upgrade is an explicit, reviewable bump of the workflow.
390
+ const GENERATED_WORKFLOW_CLI = `@moda-ai/cli@${VERSION}`;
367
391
  function writeCiCdWorkflow(cwd = process.cwd()) {
368
392
  const relativePath = '.github/workflows/moda-validation.yml';
369
393
  const path = join(cwd, relativePath);
@@ -376,6 +400,11 @@ function writeCiCdWorkflow(cwd = process.cwd()) {
376
400
  ' push:',
377
401
  ' branches: [main]',
378
402
  '',
403
+ '# Least privilege: these jobs only read the checked-out repo and talk to',
404
+ '# Moda over the API key; they never write back to the repository.',
405
+ 'permissions:',
406
+ ' contents: read',
407
+ '',
379
408
  'jobs:',
380
409
  ' moda-doctor:',
381
410
  ' runs-on: ubuntu-latest',
@@ -388,7 +417,51 @@ function writeCiCdWorkflow(cwd = process.cwd()) {
388
417
  ' with:',
389
418
  ' node-version: 22',
390
419
  ' - name: Validate Moda setup',
391
- ' run: npx -y @moda-ai/cli@latest moda doctor --online --json',
420
+ ` run: npx -y ${GENERATED_WORKFLOW_CLI} moda doctor --online --json`,
421
+ '',
422
+ ].join('\n')}`);
423
+ return relativePath;
424
+ }
425
+ // Generates a push-to-main autosync workflow that keeps the Moda registry in
426
+ // sync with the repo: it pushes prompt and skill changes after they merge. It
427
+ // only runs on `main` (never on pull_request) so unmerged versions are never
428
+ // published. Keep its env in step with writeCiCdWorkflow().
429
+ function writeAutosyncWorkflow(cwd = process.cwd()) {
430
+ const relativePath = '.github/workflows/moda-autosync.yml';
431
+ const path = join(cwd, relativePath);
432
+ mkdirSync(dirname(path), { recursive: true });
433
+ writeFileSync(path, `${[
434
+ 'name: Moda Autosync',
435
+ '',
436
+ 'on:',
437
+ ' push:',
438
+ ' branches: [main]',
439
+ '',
440
+ '# Least privilege: this job only reads the checked-out repo and writes to',
441
+ '# Moda over the API key; it never writes back to the repository.',
442
+ 'permissions:',
443
+ ' contents: read',
444
+ '',
445
+ '# A single in-flight sync at a time; newer pushes supersede older runs.',
446
+ 'concurrency:',
447
+ ' group: moda-autosync',
448
+ ' cancel-in-progress: true',
449
+ '',
450
+ 'jobs:',
451
+ ' moda-sync:',
452
+ ' runs-on: ubuntu-latest',
453
+ ' env:',
454
+ ' MODA_API_KEY: ${{ secrets.MODA_API_KEY }}',
455
+ ' MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}',
456
+ ' steps:',
457
+ ' - uses: actions/checkout@v4',
458
+ ' - uses: actions/setup-node@v4',
459
+ ' with:',
460
+ ' node-version: 22',
461
+ ` - name: Sync prompts to Moda`,
462
+ ` run: npx -y ${GENERATED_WORKFLOW_CLI} moda prompts sync`,
463
+ ` - name: Sync skills to Moda`,
464
+ ` run: npx -y ${GENERATED_WORKFLOW_CLI} moda skills sync`,
392
465
  '',
393
466
  ].join('\n')}`);
394
467
  return relativePath;
@@ -451,6 +524,19 @@ async function shouldConfigureCiCd(flags, yes) {
451
524
  default: false,
452
525
  });
453
526
  }
527
+ async function shouldConfigureAutosync(flags, yes) {
528
+ const explicit = parseOptionalBoolean(flagValue(flags, 'autosync', 'auto-sync', 'auto_sync'));
529
+ if (explicit !== undefined)
530
+ return explicit;
531
+ if (isTruthyFlag(flagValue(flags, 'no-autosync', 'no_autosync', 'no-auto-sync', 'no_auto_sync')))
532
+ return false;
533
+ if (yes)
534
+ return false;
535
+ return confirm({
536
+ message: 'Create a GitHub Actions workflow to autosync prompts and skills to Moda on push to main?',
537
+ default: false,
538
+ });
539
+ }
454
540
  async function shouldIntegrateSdk(flags, yes) {
455
541
  const explicit = parseOptionalBoolean(flagValue(flags, 'sdk-integrate', 'sdk_integrate', 'sdk-guide', 'sdk_guide', 'sdk'));
456
542
  if (explicit !== undefined)
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;
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(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {