@moda-ai/cli 1.21.0 → 1.22.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/README.md CHANGED
@@ -58,6 +58,9 @@ moda tool-failures # Tool failure overview
58
58
  moda tool-failure-detail <tool_name> # Per-tool failure detail
59
59
  moda tool-failure-detail <tool_name> --include-window --window=1
60
60
 
61
+ moda feedback "cluster label looks wrong" # Help us improve: flag wrong/missing data
62
+ moda feedback "..." --category=missing_data --conversation-id=<id>
63
+
61
64
  # Prompt management
62
65
  moda prompts init # Create .moda/prompts.yml
63
66
  moda prompts status # Read-only local prompt status
@@ -14,7 +14,7 @@ export declare class ApiError extends Error {
14
14
  export declare class NetworkError extends Error {
15
15
  constructor(message: string);
16
16
  }
17
- export declare function callDataAPI(endpoint: string): Promise<unknown>;
17
+ export declare function callDataAPI(endpoint: string, init?: RequestInit, requestOptions?: ModaRequestOptions): Promise<unknown>;
18
18
  /**
19
19
  * Per-call transport overrides. Used by expensive, long-running, non-idempotent
20
20
  * endpoints (e.g. the agent `ask` POST) that need a longer timeout than the
@@ -31,10 +31,10 @@ export class NetworkError extends Error {
31
31
  function isRetryable(status) {
32
32
  return status === 429 || status >= 500;
33
33
  }
34
- export async function callDataAPI(endpoint) {
34
+ export async function callDataAPI(endpoint, init = { method: 'GET' }, requestOptions = {}) {
35
35
  apiRequestCount++;
36
36
  const url = `${resolveModaBaseUrl()}/api/v1/data${endpoint}`;
37
- return callModaAPI(url, { method: 'GET' });
37
+ return callModaAPI(url, init, {}, requestOptions);
38
38
  }
39
39
  export async function callControlAPI(endpoint, options = {}, profileOptions = {}, requestOptions = {}) {
40
40
  const url = `${resolveModaBaseUrl(profileOptions)}/api${endpoint}`;
package/dist/cli.d.ts CHANGED
@@ -21,6 +21,12 @@ export declare function flagsToArgs(flags: Record<string, string>, positionalKey
21
21
  * reusing this for a command with positionals.
22
22
  */
23
23
  export declare function buildResumeCommand(command: string, flags: Record<string, string>): string[];
24
+ /**
25
+ * Coarse caller identity for the feedback body's `agent` field. Same
26
+ * truthy-env semantics as the agent-environment detection in output.ts;
27
+ * never sourced from flags, because `--agent` is the output-mode flag.
28
+ */
29
+ export declare function detectCallerAgent(env: NodeJS.ProcessEnv): string;
24
30
  export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>, positionals?: string[], context?: CommandContext): Promise<number>;
25
31
  interface CommandDispatchResult {
26
32
  exitCode: number;
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, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from './schemas.js';
5
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, FeedbackSchema, FEEDBACK_CATEGORIES, FEEDBACK_SEVERITIES, } from './schemas.js';
6
6
  import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
7
7
  import { runPromptsCommand } from './prompts.js';
8
8
  import { runSkillsCommand } from './skills.js';
@@ -170,6 +170,7 @@ Commands:
170
170
  frustrations Get user frustration detections
171
171
  tool-failures Get tool failure overview
172
172
  tool-failure-detail <tool_name> Get per-tool failure detail
173
+ feedback "<note>" Flag wrong/missing data or CLI quirks to the Moda team
173
174
 
174
175
  Prompt management:
175
176
  prompts init Create .moda/prompts.yml
@@ -235,6 +236,16 @@ World-state flags:
235
236
  --summary-only Return segment summaries only (omit the event stream)
236
237
  --event-limit=N Max world-state events to return (1-5000, default 2000)
237
238
 
239
+ Feedback flags:
240
+ --category=CAT bad_cluster_label|mismatched_frustration|missing_data|noisy_data|
241
+ wrong_tool_failure|incorrect_loop|api_quirk|other (default other)
242
+ --severity=info|low|medium|high How bad it is (default low)
243
+ --conversation-id=ID Attach the conversation you were looking at
244
+ --cluster-id=ID Attach a cluster node id
245
+ --tool-name=NAME Attach a tool name
246
+ --run-id=ID Attach a run id
247
+ (set MODA_CLI_TIPS=0 to hide the feedback tip line)
248
+
238
249
  Init flags:
239
250
  --yes Accept all defaults (non-interactive)
240
251
  --analyst=auto|claude|codex|cursor|local-scan
@@ -310,6 +321,7 @@ Examples:
310
321
  moda search "refund flow" --mode=semantic --time-range=7d --limit=10
311
322
  moda world-state <conversation_id> --summary-only
312
323
  moda frustrations --days-back=14 --limit=20
324
+ moda feedback "cluster label looks wrong" --category=bad_cluster_label --cluster-id=<node_id>
313
325
  moda context <conversation_id> --window=3
314
326
  moda audit <conversation_id|trace_id>
315
327
  moda audit <trace_id> --kind=trace --json
@@ -421,6 +433,21 @@ function recordConversationsTelemetry(context, data) {
421
433
  has_more: hasMore === true ? 'yes' : hasMore === false ? 'no' : 'na',
422
434
  });
423
435
  }
436
+ /**
437
+ * Coarse caller identity for the feedback body's `agent` field. Same
438
+ * truthy-env semantics as the agent-environment detection in output.ts;
439
+ * never sourced from flags, because `--agent` is the output-mode flag.
440
+ */
441
+ export function detectCallerAgent(env) {
442
+ const truthy = (value) => value != null && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
443
+ if (truthy(env.CLAUDECODE))
444
+ return 'claude-code';
445
+ if (truthy(env.CURSOR))
446
+ return 'cursor';
447
+ if (truthy(env.MODA_AGENT) || truthy(env.CI))
448
+ return 'agent';
449
+ return 'human';
450
+ }
424
451
  export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
425
452
  switch (command) {
426
453
  case 'prompts': {
@@ -624,6 +651,54 @@ export async function runCommand(command, positional, flags, positionals = posit
624
651
  await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1, context.output);
625
652
  break;
626
653
  }
654
+ case 'feedback': {
655
+ // Join every positional so an unquoted multi-word note is kept whole
656
+ // instead of silently truncating to the first word.
657
+ const note = positionals.join(' ').trim();
658
+ if (note === '') {
659
+ throw new CliInputError('<note> is required', `Usage: moda feedback "<note>" [--category=${FEEDBACK_CATEGORIES.join('|')}] [--severity=${FEEDBACK_SEVERITIES.join('|')}] [--conversation-id=ID] [--cluster-id=ID] [--tool-name=NAME] [--run-id=ID]`);
660
+ }
661
+ // Raw flag strings, not flagsToArgs: its numeric coercion would turn a
662
+ // purely numeric id ("--conversation-id=873459") into a number that
663
+ // FeedbackSchema's z.string() rejects.
664
+ const params = FeedbackSchema.parse({
665
+ note,
666
+ ...(flags.category !== undefined ? { category: flags.category } : {}),
667
+ ...(flags.severity !== undefined ? { severity: flags.severity } : {}),
668
+ ...(flags['conversation-id'] !== undefined ? { conversation_id: flags['conversation-id'] } : {}),
669
+ ...(flags['cluster-id'] !== undefined ? { cluster_id: flags['cluster-id'] } : {}),
670
+ ...(flags['tool-name'] !== undefined ? { tool_name: flags['tool-name'] } : {}),
671
+ ...(flags['run-id'] !== undefined ? { run_id: flags['run-id'] } : {}),
672
+ });
673
+ const refs = {};
674
+ if (params.conversation_id)
675
+ refs.conversation_id = params.conversation_id;
676
+ if (params.cluster_id)
677
+ refs.cluster_id = params.cluster_id;
678
+ if (params.tool_name)
679
+ refs.tool_name = params.tool_name;
680
+ if (params.run_id)
681
+ refs.run_id = params.run_id;
682
+ const category = params.category ?? 'other';
683
+ const severity = params.severity ?? 'low';
684
+ // retries: 0. The backend mints a fresh feedback_id per request and
685
+ // moda.agent_feedback has no dedup layer, so a retried POST after a
686
+ // lost response would insert duplicate rows.
687
+ const data = await callDataAPI('/feedback', {
688
+ method: 'POST',
689
+ body: JSON.stringify({
690
+ category,
691
+ severity,
692
+ source: 'cli',
693
+ agent: detectCallerAgent(process.env),
694
+ cli_version: VERSION,
695
+ note: params.note,
696
+ ...(Object.keys(refs).length > 0 ? { refs } : {}),
697
+ }),
698
+ }, { retries: 0 });
699
+ context.output.writeData({ submitted: true, category, severity, ...(asRecord(data) ?? {}) }, { summary: 'Feedback submitted. Thank you for helping us improve!' });
700
+ break;
701
+ }
627
702
  default:
628
703
  throw new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
629
704
  }
@@ -1036,6 +1111,16 @@ export const commandRegistry = createCommandRegistry([
1036
1111
  examples: ['moda tool-failure-detail search --include-window'],
1037
1112
  ...dataApiDefaults,
1038
1113
  }),
1114
+ legacyCommand({
1115
+ name: 'feedback',
1116
+ description: 'Flag wrong/missing data or CLI quirks to the Moda team',
1117
+ examples: [
1118
+ 'moda feedback "cluster label looks wrong" --category=bad_cluster_label --cluster-id=<node_id>',
1119
+ 'moda feedback "search finds nothing for a conversation I can open" --category=missing_data --conversation-id=<id>',
1120
+ ],
1121
+ ...dataApiDefaults,
1122
+ mutability: 'write',
1123
+ }),
1039
1124
  legacyCommand({
1040
1125
  name: 'prompts',
1041
1126
  description: 'Manage prompt versions and labels',
@@ -1111,6 +1196,28 @@ export function printCliError(error) {
1111
1196
  safeWriteStderr(`Error: ${String(error)}\n`);
1112
1197
  }
1113
1198
  }
1199
+ /**
1200
+ * Human-TTY twin of the envelope `meta.tip`: one dim stderr line after a
1201
+ * successful command. stderr only (stdout stays machine-readable), TTY only
1202
+ * (pipes/CI stay noise-free), and MODA_CLI_TIPS=0 opts out.
1203
+ */
1204
+ function printFeedbackTip(commandName, context) {
1205
+ if (context.outputMode !== 'human' || context.output.quiet)
1206
+ return;
1207
+ if (commandName === 'feedback')
1208
+ return;
1209
+ if (!process.stderr.isTTY)
1210
+ return;
1211
+ // Intentional strict `=== '0'` (NOT the permissive truthy() used by
1212
+ // detectCallerAgent): `MODA_CLI_TIPS=0` is the one documented opt-out form
1213
+ // (manifest env list + docs), so keep this exact-match — don't "fix" it to
1214
+ // match the other guards or it silently changes the published interface.
1215
+ if (process.env.MODA_CLI_TIPS === '0')
1216
+ return;
1217
+ const text = 'Tip: Use `moda feedback "<note>"` to help us improve!';
1218
+ const colorOk = !('NO_COLOR' in process.env) && process.env.TERM !== 'dumb';
1219
+ safeWriteStderr(`\n${colorOk ? `\x1b[2m${text}\x1b[0m` : text}\n`);
1220
+ }
1114
1221
  function printAgentCliError(command, error, mode = 'agent') {
1115
1222
  const output = createCliOutput({
1116
1223
  command: command || 'unknown',
@@ -1236,6 +1343,7 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1236
1343
  if (!updateCheckDisabled) {
1237
1344
  printUpdateNotice();
1238
1345
  }
1346
+ printFeedbackTip(definition.name, context);
1239
1347
  return finishRun(context, { kind: 'completed', exitCode: result.exitCode });
1240
1348
  }
1241
1349
  catch (error) {
package/dist/manifest.js CHANGED
@@ -32,6 +32,7 @@ export function buildManifest(commands) {
32
32
  'MODA_AGENT_MAX_OUTPUT_BYTES',
33
33
  'MODA_CLI_TELEMETRY',
34
34
  'MODA_CLI_UPDATE_CHECK',
35
+ 'MODA_CLI_TIPS',
35
36
  ],
36
37
  exit_codes: [
37
38
  { code: 0, name: 'ok', meaning: 'Command completed successfully.' },
package/dist/output.d.ts CHANGED
@@ -2,6 +2,14 @@ import { type AgentAuthRequest, type AgentInputRequest } from './errors.js';
2
2
  export type CliOutputMode = 'human' | 'json' | 'agent' | 'agent-stream';
3
3
  export declare const AGENT_SCHEMA_VERSION = "moda.agent.v1";
4
4
  export declare const AGENT_EVENT_SCHEMA_VERSION = "moda.agent_event.v1";
5
+ /**
6
+ * One-line feedback nudge stamped into every non-feedback success envelope's
7
+ * `meta.tip` (agents are the CLI's most common callers, and the envelope is
8
+ * where they actually look). Deliberately conditional wording so agents flag
9
+ * genuine observations instead of habitually running the command. The
10
+ * human-TTY twin is printed on stderr by the dispatcher in cli.ts.
11
+ */
12
+ export declare const FEEDBACK_TIP = "Help us improve! If a result in this response looks wrong or is missing data, flag it: moda feedback \"<describe what looked wrong>\" [--category=...] [--conversation-id=... | --cluster-id=... | --tool-name=...]. Only submit genuine observations.";
5
13
  /**
6
14
  * Stable process exit codes that an external agent can branch on:
7
15
  * 0 ok — grounded answer from Moda Cloud intelligence.
package/dist/output.js CHANGED
@@ -3,6 +3,14 @@ import { CliAccessError, CliAuthError, CliCancellationError, CliInputError, CliI
3
3
  import { redactDeep } from './redact.js';
4
4
  export const AGENT_SCHEMA_VERSION = 'moda.agent.v1';
5
5
  export const AGENT_EVENT_SCHEMA_VERSION = 'moda.agent_event.v1';
6
+ /**
7
+ * One-line feedback nudge stamped into every non-feedback success envelope's
8
+ * `meta.tip` (agents are the CLI's most common callers, and the envelope is
9
+ * where they actually look). Deliberately conditional wording so agents flag
10
+ * genuine observations instead of habitually running the command. The
11
+ * human-TTY twin is printed on stderr by the dispatcher in cli.ts.
12
+ */
13
+ export const FEEDBACK_TIP = 'Help us improve! If a result in this response looks wrong or is missing data, flag it: moda feedback "<describe what looked wrong>" [--category=...] [--conversation-id=... | --cluster-id=... | --tool-name=...]. Only submit genuine observations.';
6
14
  /**
7
15
  * Stable process exit codes that an external agent can branch on:
8
16
  * 0 ok — grounded answer from Moda Cloud intelligence.
@@ -214,6 +222,9 @@ export function createCliOutput(options) {
214
222
  meta: {
215
223
  ...(writeOptions.meta ?? {}),
216
224
  run_id: state.runId,
225
+ // No tip on the feedback command itself; it would point at the
226
+ // command that just ran.
227
+ ...(options.command === 'feedback' ? {} : { tip: FEEDBACK_TIP }),
217
228
  },
218
229
  summary: writeOptions.summary,
219
230
  findings: writeOptions.findings,
package/dist/schemas.d.ts CHANGED
@@ -188,4 +188,38 @@ export declare const SearchSchema: z.ZodObject<{
188
188
  user_id?: string | undefined;
189
189
  include_tool_io?: boolean | undefined;
190
190
  }>;
191
+ export declare const FEEDBACK_CATEGORIES: readonly ["bad_cluster_label", "mismatched_frustration", "missing_data", "noisy_data", "wrong_tool_failure", "incorrect_loop", "api_quirk", "other"];
192
+ export declare const FEEDBACK_SEVERITIES: readonly ["info", "low", "medium", "high"];
193
+ /**
194
+ * moda feedback "<note>": flag wrong/missing data or CLI quirks to the Moda
195
+ * team. The handler builds this input from raw flag strings (NOT flagsToArgs,
196
+ * whose numeric coercion would turn "--conversation-id=873459" into a number
197
+ * that z.string() rejects). max(4000) on note mirrors the backend
198
+ * @MaxLength(4000); the capped id fields become the POST body's `refs`.
199
+ */
200
+ export declare const FeedbackSchema: z.ZodObject<{
201
+ note: z.ZodString;
202
+ category: z.ZodOptional<z.ZodDefault<z.ZodEnum<["bad_cluster_label", "mismatched_frustration", "missing_data", "noisy_data", "wrong_tool_failure", "incorrect_loop", "api_quirk", "other"]>>>;
203
+ severity: z.ZodOptional<z.ZodDefault<z.ZodEnum<["info", "low", "medium", "high"]>>>;
204
+ conversation_id: z.ZodOptional<z.ZodString>;
205
+ cluster_id: z.ZodOptional<z.ZodString>;
206
+ tool_name: z.ZodOptional<z.ZodString>;
207
+ run_id: z.ZodOptional<z.ZodString>;
208
+ }, "strip", z.ZodTypeAny, {
209
+ note: string;
210
+ run_id?: string | undefined;
211
+ severity?: "high" | "medium" | "low" | "info" | undefined;
212
+ category?: "bad_cluster_label" | "mismatched_frustration" | "missing_data" | "noisy_data" | "wrong_tool_failure" | "incorrect_loop" | "api_quirk" | "other" | undefined;
213
+ conversation_id?: string | undefined;
214
+ tool_name?: string | undefined;
215
+ cluster_id?: string | undefined;
216
+ }, {
217
+ note: string;
218
+ run_id?: string | undefined;
219
+ severity?: "high" | "medium" | "low" | "info" | undefined;
220
+ category?: "bad_cluster_label" | "mismatched_frustration" | "missing_data" | "noisy_data" | "wrong_tool_failure" | "incorrect_loop" | "api_quirk" | "other" | undefined;
221
+ conversation_id?: string | undefined;
222
+ tool_name?: string | undefined;
223
+ cluster_id?: string | undefined;
224
+ }>;
191
225
  //# sourceMappingURL=schemas.d.ts.map
package/dist/schemas.js CHANGED
@@ -93,4 +93,34 @@ export const SearchSchema = z.object({
93
93
  limit: z.number().min(1).max(100).default(20).optional(),
94
94
  include_tool_io: z.boolean().default(false).optional(),
95
95
  });
96
+ // Enums mirror the backend FeedbackBodyDto (VALID_FEEDBACK_CATEGORIES /
97
+ // VALID_FEEDBACK_SEVERITIES) so a bad value fails locally instead of
98
+ // round-tripping a 400.
99
+ export const FEEDBACK_CATEGORIES = [
100
+ 'bad_cluster_label',
101
+ 'mismatched_frustration',
102
+ 'missing_data',
103
+ 'noisy_data',
104
+ 'wrong_tool_failure',
105
+ 'incorrect_loop',
106
+ 'api_quirk',
107
+ 'other',
108
+ ];
109
+ export const FEEDBACK_SEVERITIES = ['info', 'low', 'medium', 'high'];
110
+ /**
111
+ * moda feedback "<note>": flag wrong/missing data or CLI quirks to the Moda
112
+ * team. The handler builds this input from raw flag strings (NOT flagsToArgs,
113
+ * whose numeric coercion would turn "--conversation-id=873459" into a number
114
+ * that z.string() rejects). max(4000) on note mirrors the backend
115
+ * @MaxLength(4000); the capped id fields become the POST body's `refs`.
116
+ */
117
+ export const FeedbackSchema = z.object({
118
+ note: z.string().min(1).max(4000),
119
+ category: z.enum(FEEDBACK_CATEGORIES).default('other').optional(),
120
+ severity: z.enum(FEEDBACK_SEVERITIES).default('low').optional(),
121
+ conversation_id: z.string().min(1).max(256).optional(),
122
+ cluster_id: z.string().min(1).max(256).optional(),
123
+ tool_name: z.string().min(1).max(256).optional(),
124
+ run_id: z.string().min(1).max(256).optional(),
125
+ });
96
126
  //# sourceMappingURL=schemas.js.map
package/dist/skills.js CHANGED
@@ -653,7 +653,7 @@ export async function runSkillsCommand(input) {
653
653
  return 0;
654
654
  }
655
655
  if (sub !== 'pull') {
656
- throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills status [run-id] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
656
+ throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills status [run-id] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
657
657
  }
658
658
  const status = (flags.status ?? 'approved').toLowerCase();
659
659
  if (!VALID_STATUSES.has(status)) {
@@ -690,6 +690,11 @@ async function generateSkills(flags) {
690
690
  : `${resolveIngestUrl().replace(/\/+$/, '')}/v1/skills/generate`;
691
691
  const waitForCompletion = flags.wait === 'true' || flags['wait-for-completion'] === 'true';
692
692
  const forceReprocess = flags['force-reprocess'] === 'true';
693
+ const source = normalizeSkillSource(flags.source);
694
+ // Soft-deprecate: still accepted so old CLIs don't hard-fail; server returns empty.
695
+ if (source === 'coding_agent') {
696
+ console.error('Note: --source=coding-agent is deprecated for skills gen (SDK-only). The run will find no coding-agent sessions; use --source=all or --source=sdk.');
697
+ }
693
698
  const body = compactObject({
694
699
  tenant_id: tenantId || undefined,
695
700
  max_sessions: parsePositiveInt(flags['max-sessions'], 0),
@@ -699,7 +704,7 @@ async function generateSkills(flags) {
699
704
  clustering_run_id: flags['clustering-run-id'] || undefined,
700
705
  replay_set_id: flags['replay-set-id'] || flags['eval-set-id'] || undefined,
701
706
  run_clustering: flags['no-clustering'] !== 'true',
702
- source: normalizeSkillSource(flags.source),
707
+ source,
703
708
  clustering_timeout_seconds: parsePositiveInt(flags['clustering-timeout-seconds'], 7200),
704
709
  clustering_poll_seconds: parsePositiveInt(flags['clustering-poll-seconds'], 15),
705
710
  max_cluster_pct: parseOptionalFloat(flags['max-cluster-pct']),
@@ -745,6 +750,7 @@ function compactObject(input) {
745
750
  }
746
751
  function normalizeSkillSource(value) {
747
752
  const normalized = (value || 'all').toLowerCase().replace(/-/g, '_');
753
+ // coding_agent kept for soft-deprecation (empty server result + stderr note).
748
754
  if (['all', 'sdk', 'coding_agent'].includes(normalized))
749
755
  return normalized;
750
756
  return 'all';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.21.0",
3
+ "version": "1.22.1",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-07-12T21:12:37.084Z",
4
- "cli_version": "1.21.0",
3
+ "bundled_at": "2026-07-17T10:36:50.172Z",
4
+ "cli_version": "1.22.1",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: moda-cli
3
- version: 2.1.0
3
+ version: 2.2.0
4
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, 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, wants to find conversations or tool calls about a topic, or wants to investigate how their AI agent is performing.
5
5
  ---
6
6
 
@@ -680,6 +680,33 @@ done
680
680
  moda tool-failures | jq '.tools[] | {tool: .tool_name, failures: .failure_count}'
681
681
  ```
682
682
 
683
+ ## Feedback: help us improve
684
+
685
+ Successful agent envelopes carry a `meta.tip` reminding you of this. When a
686
+ response looks wrong (a cluster label that doesn't match its conversations,
687
+ a frustration whose causes don't match the transcript, an empty result that
688
+ should not be empty, an API quirk), flag it with `moda feedback`. The Moda
689
+ team reads these to fix data quality issues. Only submit genuine
690
+ observations; never run the command with placeholder text.
691
+
692
+ ```bash
693
+ moda feedback "cluster 'billing' is mostly refund conversations" \
694
+ --category=bad_cluster_label --cluster-id=<node_id>
695
+ moda feedback "search finds nothing for a conversation I can open" \
696
+ --category=missing_data --conversation-id=<id>
697
+ moda feedback "a cancelled call is counted as a tool failure" \
698
+ --category=wrong_tool_failure --tool-name=<tool>
699
+ ```
700
+
701
+ - `<note>` is required (max 4000 chars). Say what looked wrong and why.
702
+ - `--category`: `bad_cluster_label | mismatched_frustration | missing_data |
703
+ noisy_data | wrong_tool_failure | incorrect_loop | api_quirk | other`
704
+ (default `other`).
705
+ - `--severity`: `info | low | medium | high` (default `low`).
706
+ - Attach ids when you have them: `--conversation-id`, `--cluster-id`,
707
+ `--tool-name`, `--run-id`; they let the team reproduce what you saw.
708
+ - Never include secrets or full transcripts in the note.
709
+
683
710
  ## Validation — how to know it worked
684
711
 
685
712
  - Every successful command exits 0 and prints valid JSON to stdout.
@@ -748,6 +775,7 @@ npx: `npx -p @moda-ai/cli moda <command>`.
748
775
  | `moda frustrations` | User frustration detections with evidence |
749
776
  | `moda tool-failures` | Tool failure overview |
750
777
  | `moda tool-failure-detail <tool_name>` | Per-tool failure breakdown + examples |
778
+ | `moda feedback "<note>"` | Flag wrong/missing data or CLI quirks to the Moda team |
751
779
  | `moda prompts status` | Read-only local prompt status |
752
780
  | `moda prompts diff` | Read-only local prompt diff/status |
753
781
  | `moda prompts sync` | Upload changed prompt versions and update lockfile |