@moda-ai/cli 1.20.1 → 1.22.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.
@@ -0,0 +1,135 @@
1
+ import { z } from 'zod';
2
+ export declare const SDK_RESULT_SCHEMA_VERSION = "moda.sdk_integration_result.v1";
3
+ export declare const SDK_RESULT_JSON_BEGIN = "MODA_SDK_RESULT_JSON_BEGIN";
4
+ export declare const SDK_RESULT_JSON_END = "MODA_SDK_RESULT_JSON_END";
5
+ export declare const SDK_RESULT_FILE = ".moda/sdk-integration-result.json";
6
+ export declare const SdkAgentResultSchema: z.ZodObject<{
7
+ schema_version: z.ZodPipeline<z.ZodString, z.ZodLiteral<"moda.sdk_integration_result.v1">>;
8
+ status: z.ZodEffects<z.ZodEnum<["integrated", "needs_human_input", "repository_checks_failed", "smoke_execution_failed", "nothing_to_integrate", "failed"]>, "failed" | "integrated" | "needs_human_input" | "repository_checks_failed" | "smoke_execution_failed" | "nothing_to_integrate", unknown>;
9
+ summary: z.ZodEffects<z.ZodString, string, unknown>;
10
+ skills_consulted: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodEffects<z.ZodString, string, unknown>, "many">, string[], unknown>>;
11
+ skills_used: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodEffects<z.ZodString, string, unknown>, "many">, string[], unknown>>;
12
+ packages_installed: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodEffects<z.ZodString, string, unknown>, "many">, string[], unknown>>;
13
+ files_changed: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodEffects<z.ZodString, string, unknown>, "many">, string[], unknown>>;
14
+ call_sites_instrumented: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodEffects<z.ZodEffects<z.ZodString, string, unknown>, {
15
+ file: string;
16
+ }, unknown>, z.ZodObject<{
17
+ file: z.ZodEffects<z.ZodString, string, unknown>;
18
+ detail: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ file: string;
21
+ detail?: string | undefined;
22
+ }, {
23
+ file?: unknown;
24
+ detail?: unknown;
25
+ }>]>, "many">, ({
26
+ file: string;
27
+ } | {
28
+ file: string;
29
+ detail?: string | undefined;
30
+ })[], unknown>>;
31
+ commands_run: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodObject<{
32
+ command: z.ZodEffects<z.ZodString, string, unknown>;
33
+ exit_code: z.ZodEffects<z.ZodUnion<[z.ZodNumber, z.ZodNull]>, number | null, unknown>;
34
+ kind: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
35
+ }, "strip", z.ZodTypeAny, {
36
+ command: string;
37
+ exit_code: number | null;
38
+ kind?: string | undefined;
39
+ }, {
40
+ command?: unknown;
41
+ kind?: unknown;
42
+ exit_code?: unknown;
43
+ }>, "many">, {
44
+ command: string;
45
+ exit_code: number | null;
46
+ kind?: string | undefined;
47
+ }[], unknown>>;
48
+ smoke: z.ZodDefault<z.ZodEffects<z.ZodNullable<z.ZodObject<{
49
+ method: z.ZodEffects<z.ZodString, string, unknown>;
50
+ command: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, unknown>;
51
+ verification_id: z.ZodEffects<z.ZodString, string, unknown>;
52
+ }, "strip", z.ZodTypeAny, {
53
+ method: string;
54
+ verification_id: string;
55
+ command?: string | undefined;
56
+ }, {
57
+ command?: unknown;
58
+ method?: unknown;
59
+ verification_id?: unknown;
60
+ }>>, {
61
+ method: string;
62
+ verification_id: string;
63
+ command?: string | undefined;
64
+ } | null, unknown>>;
65
+ remaining_actions: z.ZodDefault<z.ZodEffects<z.ZodArray<z.ZodEffects<z.ZodString, string, unknown>, "many">, string[], unknown>>;
66
+ failure: z.ZodDefault<z.ZodEffects<z.ZodNullable<z.ZodObject<{
67
+ class: z.ZodEffects<z.ZodString, string, unknown>;
68
+ detail: z.ZodEffects<z.ZodString, string, unknown>;
69
+ }, "strip", z.ZodTypeAny, {
70
+ detail: string;
71
+ class: string;
72
+ }, {
73
+ detail?: unknown;
74
+ class?: unknown;
75
+ }>>, {
76
+ detail: string;
77
+ class: string;
78
+ } | null, unknown>>;
79
+ }, "strip", z.ZodTypeAny, {
80
+ schema_version: "moda.sdk_integration_result.v1";
81
+ summary: string;
82
+ status: "failed" | "integrated" | "needs_human_input" | "repository_checks_failed" | "smoke_execution_failed" | "nothing_to_integrate";
83
+ skills_consulted: string[];
84
+ skills_used: string[];
85
+ packages_installed: string[];
86
+ files_changed: string[];
87
+ call_sites_instrumented: ({
88
+ file: string;
89
+ } | {
90
+ file: string;
91
+ detail?: string | undefined;
92
+ })[];
93
+ commands_run: {
94
+ command: string;
95
+ exit_code: number | null;
96
+ kind?: string | undefined;
97
+ }[];
98
+ smoke: {
99
+ method: string;
100
+ verification_id: string;
101
+ command?: string | undefined;
102
+ } | null;
103
+ remaining_actions: string[];
104
+ failure: {
105
+ detail: string;
106
+ class: string;
107
+ } | null;
108
+ }, {
109
+ schema_version: string;
110
+ summary?: unknown;
111
+ status?: unknown;
112
+ skills_consulted?: unknown;
113
+ skills_used?: unknown;
114
+ packages_installed?: unknown;
115
+ files_changed?: unknown;
116
+ call_sites_instrumented?: unknown;
117
+ commands_run?: unknown;
118
+ smoke?: unknown;
119
+ remaining_actions?: unknown;
120
+ failure?: unknown;
121
+ }>;
122
+ export type SdkAgentResult = z.infer<typeof SdkAgentResultSchema>;
123
+ export type ExtractSdkAgentResult = {
124
+ result: SdkAgentResult;
125
+ source: 'file' | 'markers';
126
+ } | {
127
+ error: string;
128
+ detail?: string;
129
+ };
130
+ export declare function extractSdkAgentResult(opts: {
131
+ cwd: string;
132
+ stdout: string;
133
+ stderr: string;
134
+ }): ExtractSdkAgentResult;
135
+ //# sourceMappingURL=sdk-protocol.d.ts.map
@@ -0,0 +1,152 @@
1
+ // src/init/sdk-protocol.ts — Versioned result protocol used by the coding
2
+ // agent that performs SDK integration.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { z } from 'zod';
6
+ export const SDK_RESULT_SCHEMA_VERSION = 'moda.sdk_integration_result.v1';
7
+ export const SDK_RESULT_JSON_BEGIN = 'MODA_SDK_RESULT_JSON_BEGIN';
8
+ export const SDK_RESULT_JSON_END = 'MODA_SDK_RESULT_JSON_END';
9
+ export const SDK_RESULT_FILE = '.moda/sdk-integration-result.json';
10
+ const nonemptyString = z.preprocess((value) => {
11
+ if (typeof value === 'string')
12
+ return value.trim();
13
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
14
+ return String(value);
15
+ }
16
+ return value;
17
+ }, z.string().min(1));
18
+ const optionalString = z.preprocess((value) => {
19
+ if (value === undefined)
20
+ return undefined;
21
+ if (typeof value === 'string') {
22
+ const trimmed = value.trim();
23
+ return trimmed || undefined;
24
+ }
25
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
26
+ return String(value);
27
+ }
28
+ return undefined;
29
+ }, z.string().min(1).optional());
30
+ function tolerantArray(itemSchema) {
31
+ return z.preprocess((value) => {
32
+ if (!Array.isArray(value))
33
+ return [];
34
+ const accepted = [];
35
+ for (const item of value) {
36
+ const parsed = itemSchema.safeParse(item);
37
+ if (parsed.success)
38
+ accepted.push(parsed.data);
39
+ }
40
+ return accepted;
41
+ }, z.array(itemSchema)).default([]);
42
+ }
43
+ const callSiteSchema = z.union([
44
+ nonemptyString.transform((file) => ({ file })),
45
+ z.object({
46
+ file: nonemptyString,
47
+ detail: optionalString,
48
+ }),
49
+ ]);
50
+ const exitCodeSchema = z.preprocess((value) => {
51
+ if (value === null)
52
+ return null;
53
+ if (typeof value === 'string' && value.trim() !== '') {
54
+ const numeric = Number(value);
55
+ return Number.isFinite(numeric) ? numeric : value;
56
+ }
57
+ return value;
58
+ }, z.union([z.number().finite(), z.null()]));
59
+ const commandSchema = z.object({
60
+ command: nonemptyString,
61
+ exit_code: exitCodeSchema,
62
+ kind: optionalString,
63
+ });
64
+ const smokeSchema = z.object({
65
+ method: nonemptyString,
66
+ command: optionalString,
67
+ verification_id: nonemptyString,
68
+ });
69
+ const failureSchema = z.object({
70
+ class: nonemptyString,
71
+ detail: nonemptyString,
72
+ });
73
+ export const SdkAgentResultSchema = z.object({
74
+ schema_version: z.string().trim().pipe(z.literal(SDK_RESULT_SCHEMA_VERSION)),
75
+ status: z.preprocess((value) => typeof value === 'string' ? value.trim() : value, z.enum([
76
+ 'integrated',
77
+ 'needs_human_input',
78
+ 'repository_checks_failed',
79
+ 'smoke_execution_failed',
80
+ 'nothing_to_integrate',
81
+ 'failed',
82
+ ])),
83
+ summary: nonemptyString,
84
+ skills_consulted: tolerantArray(nonemptyString),
85
+ skills_used: tolerantArray(nonemptyString),
86
+ packages_installed: tolerantArray(nonemptyString),
87
+ files_changed: tolerantArray(nonemptyString),
88
+ call_sites_instrumented: tolerantArray(callSiteSchema),
89
+ commands_run: tolerantArray(commandSchema),
90
+ smoke: z.preprocess((value) => value ?? null, smokeSchema.nullable()).default(null),
91
+ remaining_actions: tolerantArray(nonemptyString),
92
+ failure: z.preprocess((value) => value ?? null, failureSchema.nullable()).default(null),
93
+ });
94
+ export function extractSdkAgentResult(opts) {
95
+ const diagnostics = [];
96
+ const resultPath = join(opts.cwd, SDK_RESULT_FILE);
97
+ if (existsSync(resultPath)) {
98
+ try {
99
+ const parsed = parseCandidate(readFileSync(resultPath, 'utf8'));
100
+ if (parsed.result)
101
+ return { result: parsed.result, source: 'file' };
102
+ diagnostics.push(`result file: ${parsed.detail}`);
103
+ }
104
+ catch (error) {
105
+ diagnostics.push(`result file: ${errorMessage(error)}`);
106
+ }
107
+ }
108
+ const output = [opts.stdout, opts.stderr].filter(Boolean).join('\n');
109
+ const candidates = markerCandidates(output);
110
+ for (const [index, candidate] of candidates.entries()) {
111
+ const parsed = parseCandidate(candidate);
112
+ if (parsed.result)
113
+ return { result: parsed.result, source: 'markers' };
114
+ diagnostics.push(`marker block ${index + 1}: ${parsed.detail}`);
115
+ }
116
+ if (candidates.length === 0) {
117
+ diagnostics.push(`no ${SDK_RESULT_JSON_BEGIN}/${SDK_RESULT_JSON_END} marker block found`);
118
+ }
119
+ return {
120
+ error: 'SDK integration result protocol violation.',
121
+ detail: diagnostics.join('; '),
122
+ };
123
+ }
124
+ function markerCandidates(output) {
125
+ const pattern = new RegExp(`${SDK_RESULT_JSON_BEGIN}\\s*([\\s\\S]*?)\\s*${SDK_RESULT_JSON_END}`, 'g');
126
+ const candidates = [];
127
+ for (const match of output.matchAll(pattern)) {
128
+ if (match[1])
129
+ candidates.push(match[1]);
130
+ }
131
+ return candidates;
132
+ }
133
+ function parseCandidate(candidate) {
134
+ let value;
135
+ try {
136
+ value = JSON.parse(candidate.trim());
137
+ }
138
+ catch (error) {
139
+ return { detail: `invalid JSON (${errorMessage(error)})` };
140
+ }
141
+ const parsed = SdkAgentResultSchema.safeParse(value);
142
+ if (parsed.success)
143
+ return { result: parsed.data };
144
+ const detail = parsed.error.issues
145
+ .map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
146
+ .join(', ');
147
+ return { detail: `validation failed (${detail})` };
148
+ }
149
+ function errorMessage(error) {
150
+ return error instanceof Error ? error.message : String(error);
151
+ }
152
+ //# sourceMappingURL=sdk-protocol.js.map
@@ -0,0 +1,27 @@
1
+ export interface VerificationEvidence {
2
+ observed: boolean;
3
+ verification_id: string;
4
+ total_spans: number;
5
+ llm_spans: number;
6
+ tool_spans: number;
7
+ trace_ids: string[];
8
+ orphan_count: number;
9
+ duplicate_count: number;
10
+ ingest_rows: number;
11
+ elapsed_ms: number;
12
+ polls: number;
13
+ last_error?: string;
14
+ }
15
+ export declare function generateVerificationId(): string;
16
+ export declare function isModaVerificationId(value: string): boolean;
17
+ export declare function verifyExactTrace(opts: {
18
+ verificationId: string;
19
+ timeoutMs?: number;
20
+ pollIntervalMs?: number;
21
+ fetchAudit?: (id: string) => Promise<unknown>;
22
+ sleep?: (ms: number) => Promise<void>;
23
+ now?: () => number;
24
+ }): Promise<VerificationEvidence>;
25
+ /** Fetch audit data for SDK verification using an explicit provisioned API key. */
26
+ export declare function fetchAuditWithApiKey(id: string, apiKey: string, baseUrl: string): Promise<unknown>;
27
+ //# sourceMappingURL=sdk-verification.d.ts.map
@@ -0,0 +1,123 @@
1
+ // src/init/sdk-verification.ts — Poll the Data API for an exact, freshly
2
+ // generated conversation id and require evidence of an observed LLM span.
3
+ import { randomBytes } from 'node:crypto';
4
+ const DEFAULT_TIMEOUT_MS = 90_000;
5
+ const DEFAULT_POLL_INTERVAL_MS = 3_000;
6
+ const MIN_TIMEOUT_MS = 10_000;
7
+ const MAX_TIMEOUT_MS = 600_000;
8
+ export function generateVerificationId() {
9
+ return `moda-verify-${randomBytes(16).toString('hex')}`;
10
+ }
11
+ export function isModaVerificationId(value) {
12
+ return /^moda-verify-[0-9a-f]{32}$/.test(value);
13
+ }
14
+ export async function verifyExactTrace(opts) {
15
+ const now = opts.now ?? Date.now;
16
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
17
+ // Verification MUST poll with the provisioned key (fetchAuditWithApiKey) so an
18
+ // ambient MODA_API_KEY pointing at another tenant cannot skew the result.
19
+ // Fail explicitly rather than silently falling back to ambient credentials.
20
+ const fetchAudit = opts.fetchAudit;
21
+ const timeoutMs = opts.timeoutMs === undefined || !Number.isFinite(opts.timeoutMs)
22
+ ? resolveTimeoutFromEnv()
23
+ : Math.max(0, opts.timeoutMs);
24
+ const requestedPollInterval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
25
+ const pollIntervalMs = Number.isFinite(requestedPollInterval)
26
+ ? Math.max(1, requestedPollInterval)
27
+ : DEFAULT_POLL_INTERVAL_MS;
28
+ const startedAt = now();
29
+ const deadline = startedAt + Math.max(0, timeoutMs);
30
+ let polls = 0;
31
+ let lastError;
32
+ let counts = {
33
+ total_spans: 0,
34
+ llm_spans: 0,
35
+ tool_spans: 0,
36
+ trace_ids: [],
37
+ orphan_count: 0,
38
+ duplicate_count: 0,
39
+ ingest_rows: 0,
40
+ };
41
+ if (!fetchAudit) {
42
+ lastError = 'verification requires an explicit audit fetcher bound to the provisioned API key';
43
+ return buildEvidence(false);
44
+ }
45
+ while (true) {
46
+ polls++;
47
+ try {
48
+ const response = await fetchAudit(opts.verificationId);
49
+ const parsed = readAuditCounts(response);
50
+ if (parsed) {
51
+ counts = parsed;
52
+ if (counts.total_spans >= 1 && counts.llm_spans >= 1) {
53
+ return buildEvidence(true);
54
+ }
55
+ }
56
+ }
57
+ catch (error) {
58
+ lastError = error instanceof Error ? error.message : String(error);
59
+ }
60
+ const currentTime = now();
61
+ if (currentTime >= deadline)
62
+ return buildEvidence(false);
63
+ await sleep(Math.min(pollIntervalMs, deadline - currentTime));
64
+ }
65
+ function buildEvidence(observed) {
66
+ return {
67
+ observed,
68
+ verification_id: opts.verificationId,
69
+ ...counts,
70
+ elapsed_ms: Math.max(0, now() - startedAt),
71
+ polls,
72
+ ...(lastError ? { last_error: lastError } : {}),
73
+ };
74
+ }
75
+ }
76
+ function resolveTimeoutFromEnv() {
77
+ const raw = process.env.MODA_SDK_VERIFY_TIMEOUT_MS;
78
+ if (!raw)
79
+ return DEFAULT_TIMEOUT_MS;
80
+ const parsed = Number(raw);
81
+ if (!Number.isFinite(parsed))
82
+ return DEFAULT_TIMEOUT_MS;
83
+ return Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.trunc(parsed)));
84
+ }
85
+ function readAuditCounts(value) {
86
+ if (!isRecord(value) || !isRecord(value.summary))
87
+ return undefined;
88
+ const summary = value.summary;
89
+ if (!isNonNegativeNumber(summary.total_spans) || !isRecord(summary.by_type))
90
+ return undefined;
91
+ return {
92
+ total_spans: summary.total_spans,
93
+ llm_spans: isNonNegativeNumber(summary.by_type.llm) ? summary.by_type.llm : 0,
94
+ tool_spans: isNonNegativeNumber(summary.by_type.tool) ? summary.by_type.tool : 0,
95
+ trace_ids: Array.isArray(summary.trace_ids)
96
+ ? summary.trace_ids.filter((item) => typeof item === 'string')
97
+ : [],
98
+ orphan_count: isNonNegativeNumber(summary.orphan_count) ? summary.orphan_count : 0,
99
+ duplicate_count: isNonNegativeNumber(summary.duplicate_count) ? summary.duplicate_count : 0,
100
+ ingest_rows: isNonNegativeNumber(summary.ingest_rows) ? summary.ingest_rows : 0,
101
+ };
102
+ }
103
+ function isRecord(value) {
104
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
105
+ }
106
+ function isNonNegativeNumber(value) {
107
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
108
+ }
109
+ /** Fetch audit data for SDK verification using an explicit provisioned API key. */
110
+ export async function fetchAuditWithApiKey(id, apiKey, baseUrl) {
111
+ const url = `${baseUrl}/api/v1/data/audit/${encodeURIComponent(id)}?kind=conversation`;
112
+ const response = await fetch(url, {
113
+ method: 'GET',
114
+ headers: { 'x-api-key': apiKey },
115
+ signal: AbortSignal.timeout(10_000),
116
+ });
117
+ const body = await response.text();
118
+ if (!response.ok) {
119
+ throw new Error(`Audit fetch failed (${response.status}): ${body.slice(0, 200)}`);
120
+ }
121
+ return JSON.parse(body);
122
+ }
123
+ //# sourceMappingURL=sdk-verification.js.map
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.20.1",
3
+ "version": "1.22.0",
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-12T01:43:29.789Z",
4
- "cli_version": "1.20.1",
3
+ "bundled_at": "2026-07-14T06:44:35.511Z",
4
+ "cli_version": "1.22.0",
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 |
@@ -1,19 +0,0 @@
1
- import type { SdkGuideContext } from './sdk-guide.js';
2
- type Language = 'node' | 'python';
3
- type Framework = 'vercel-ai-sdk' | 'langchain' | 'claude-agent-sdk';
4
- type Provider = 'openai' | 'anthropic';
5
- interface ResolutionRow {
6
- language: Language;
7
- framework?: Framework;
8
- provider?: Provider;
9
- skillId: string;
10
- }
11
- /** The §2.3 resolution table, encoded as data. Order matters. */
12
- export declare const RESOLUTION_TABLE: readonly ResolutionRow[];
13
- /**
14
- * Resolve the detected context to a bundled skill id, or null when no
15
- * language was detected (callers fall back to the slim generic guide).
16
- */
17
- export declare function resolveIntegrationSkillId(context: SdkGuideContext): string | null;
18
- export {};
19
- //# sourceMappingURL=skill-resolver.d.ts.map