@moda-ai/cli 1.18.0 → 1.18.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
@@ -177,6 +177,7 @@ Commands:
177
177
  prompts sync --watch Upload again when prompt files change
178
178
  prompts promote <key> Move a dev/staging/prod label
179
179
  prompts ab Run a judged prompt A/B replay experiment
180
+ prompts propose Revise a prompt from a run's failures (--gate to auto-A/B, --promote-on-win)
180
181
 
181
182
  Skills:
182
183
  skills gen Kick off tenant-wide skill generation
@@ -1,5 +1,8 @@
1
1
  import type { CommandContext } from './command-context.js';
2
2
  import type { ProfileIOOptions } from './profiles.js';
3
+ export declare const DEFAULT_POLL_INTERVAL_MS = 15000;
4
+ export declare const DEFAULT_WAIT_TIMEOUT_MS: number;
5
+ export declare const TERMINAL_RUN_STATUSES: Set<string>;
3
6
  export interface PromptArmPayload {
4
7
  content: string;
5
8
  prompt_version_id?: string;
@@ -15,4 +18,54 @@ export interface PromptAbResult {
15
18
  cases: Array<Record<string, unknown>>;
16
19
  }
17
20
  export declare function runPromptAb(flags: Record<string, string>, profileOptions: ProfileIOOptions, context: CommandContext): Promise<void>;
21
+ export interface ComparisonPayload {
22
+ run: {
23
+ runId: string;
24
+ status: string;
25
+ caseCount: number;
26
+ prodPassCount: number;
27
+ proposedPassCount: number;
28
+ prodPassRate: number;
29
+ proposedPassRate: number;
30
+ notes: string;
31
+ } | null;
32
+ cases: Array<{
33
+ caseId: string;
34
+ prodPass: boolean | null;
35
+ proposedPass: boolean | null;
36
+ prodScore: number | null;
37
+ proposedScore: number | null;
38
+ }>;
39
+ }
40
+ export declare function resolvePromptArmSpec(source: string, label: string, opts?: {
41
+ preferKey?: boolean;
42
+ }): PromptArmPayload;
43
+ export declare function fetchLatestRun(tenantId: string, replaySetId: string, runId: string, profileOptions: ProfileIOOptions): Promise<ComparisonPayload>;
44
+ /**
45
+ * Strict outcome of a completed comparison for gating: 'candidate' only when the
46
+ * proposed arm passed strictly more cases than prod on a completed run. A tie,
47
+ * regression, skip, error, or missing run is never a candidate win.
48
+ */
49
+ export declare function candidateOutcome(payload: ComparisonPayload): 'candidate' | 'baseline' | 'tie' | 'no-run';
50
+ export declare function formatVerdict(payload: ComparisonPayload): string;
51
+ /**
52
+ * Enqueue a prod-vs-proposed replay comparison and poll `/runs/latest` (pinned to
53
+ * OUR run id) until it reaches a terminal status or the deadline. Shared by
54
+ * `prompts ab` and the `prompts propose --gate` auto-gate. Returns the terminal
55
+ * comparison payload; throws on enqueue/poll timeout.
56
+ */
57
+ export declare function enqueueAndPollComparison(args: {
58
+ tenantId: string;
59
+ replaySetId: string;
60
+ promptArms: {
61
+ prod: PromptArmPayload;
62
+ proposed: PromptArmPayload;
63
+ };
64
+ seedsPerCase?: number;
65
+ assistantModel?: string;
66
+ promotePrimary?: boolean;
67
+ timeoutMs?: number;
68
+ pollIntervalMs?: number;
69
+ onWaiting?: (status: string) => void;
70
+ }, profileOptions: ProfileIOOptions): Promise<ComparisonPayload>;
18
71
  //# sourceMappingURL=prompts-ab.d.ts.map
@@ -4,9 +4,9 @@ import { callControlAPI, callDataAPI } from './api-client.js';
4
4
  import { resolveTenantId, validateConfig } from './config.js';
5
5
  import { discoverPrompts, parsePromptFileForAb, readPromptLock, runPromptSync, } from './prompts.js';
6
6
  const DEFAULT_SEEDS_PER_CASE = 3;
7
- const DEFAULT_POLL_INTERVAL_MS = 15_000;
8
- const DEFAULT_WAIT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
9
- const TERMINAL_RUN_STATUSES = new Set(['completed', 'skipped', 'error']);
7
+ export const DEFAULT_POLL_INTERVAL_MS = 15_000;
8
+ export const DEFAULT_WAIT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
9
+ export const TERMINAL_RUN_STATUSES = new Set(['completed', 'skipped', 'error']);
10
10
  export async function runPromptAb(flags, profileOptions, context) {
11
11
  validateConfig();
12
12
  const tenantId = flags['tenant-id'] || resolveTenantId(profileOptions);
@@ -81,23 +81,40 @@ export async function runPromptAb(flags, profileOptions, context) {
81
81
  }
82
82
  context.output.writeData(result);
83
83
  }
84
- function resolvePromptArmSpec(source, label) {
84
+ export function resolvePromptArmSpec(source, label, opts = {}) {
85
85
  const trimmed = source.trim();
86
86
  if (!trimmed) {
87
87
  throw new Error(`Empty prompt source for ${label}`);
88
88
  }
89
+ const fromDiscovery = () => {
90
+ const discovered = discoverPrompts();
91
+ const match = discovered.find((prompt) => prompt.key === trimmed)
92
+ ?? discovered.find((prompt) => prompt.sourcePath === trimmed);
93
+ return match ? { key: match.key, content: match.content } : null;
94
+ };
89
95
  const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
90
96
  let key;
91
97
  let content;
92
- if (existsSync(absPath)) {
98
+ // When the caller knows `source` is a registry key (e.g. `propose --gate`'s
99
+ // `--prompt-key`), resolve by discovery first so a same-named file/dir in the
100
+ // cwd can't shadow the intended prompt; fall back to path resolution otherwise.
101
+ const preferred = opts.preferKey ? fromDiscovery() : null;
102
+ if (preferred) {
103
+ key = preferred.key;
104
+ content = preferred.content;
105
+ }
106
+ else if (opts.preferKey) {
107
+ // Key-first resolution must NOT fall through to path resolution — otherwise a
108
+ // cwd file named like the key would still shadow an unresolved registry key.
109
+ throw new Error(`Could not resolve prompt key '${trimmed}' — no discovered prompt matched this key`);
110
+ }
111
+ else if (existsSync(absPath)) {
93
112
  const parsed = parsePromptFileForAb(absPath);
94
113
  key = parsed.key;
95
114
  content = parsed.content;
96
115
  }
97
116
  else {
98
- const discovered = discoverPrompts();
99
- const match = discovered.find((prompt) => prompt.key === trimmed)
100
- ?? discovered.find((prompt) => prompt.sourcePath === trimmed);
117
+ const match = fromDiscovery();
101
118
  if (!match) {
102
119
  throw new Error(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
103
120
  }
@@ -176,13 +193,29 @@ async function loadScenarioFromConversation(conversationId) {
176
193
  }
177
194
  return `Replay conversation ${conversationId}: help the user accomplish their original goal using the available tools.`;
178
195
  }
179
- async function fetchLatestRun(tenantId, replaySetId, runId, profileOptions) {
196
+ export async function fetchLatestRun(tenantId, replaySetId, runId, profileOptions) {
180
197
  // Pin the poll to OUR run id — otherwise a concurrent comparison on the same
181
198
  // set would make `/runs/latest` return the newer run forever and this command
182
199
  // would stall until timeout even after its own run finished.
183
200
  return callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/runs/latest?runId=${encodeURIComponent(runId)}`, { method: 'GET' }, profileOptions, { timeoutMs: 60_000, retries: 2 });
184
201
  }
185
- function formatVerdict(payload) {
202
+ /**
203
+ * Strict outcome of a completed comparison for gating: 'candidate' only when the
204
+ * proposed arm passed strictly more cases than prod on a completed run. A tie,
205
+ * regression, skip, error, or missing run is never a candidate win.
206
+ */
207
+ export function candidateOutcome(payload) {
208
+ const run = payload.run;
209
+ if (!run || run.status !== 'completed')
210
+ return 'no-run';
211
+ const delta = run.proposedPassCount - run.prodPassCount;
212
+ if (delta > 0)
213
+ return 'candidate';
214
+ if (delta < 0)
215
+ return 'baseline';
216
+ return 'tie';
217
+ }
218
+ export function formatVerdict(payload) {
186
219
  const run = payload.run;
187
220
  if (!run)
188
221
  return 'no run';
@@ -224,6 +257,43 @@ function formatPass(value) {
224
257
  function pct(rate) {
225
258
  return `${Math.round(Math.max(0, Math.min(1, rate)) * 100)}%`;
226
259
  }
260
+ /**
261
+ * Enqueue a prod-vs-proposed replay comparison and poll `/runs/latest` (pinned to
262
+ * OUR run id) until it reaches a terminal status or the deadline. Shared by
263
+ * `prompts ab` and the `prompts propose --gate` auto-gate. Returns the terminal
264
+ * comparison payload; throws on enqueue/poll timeout.
265
+ */
266
+ export async function enqueueAndPollComparison(args, profileOptions) {
267
+ const enqueue = (await callControlAPI(`/tenants/${encodeURIComponent(args.tenantId)}/replay-sets/${encodeURIComponent(args.replaySetId)}/run`, {
268
+ method: 'POST',
269
+ body: JSON.stringify({
270
+ promptArms: args.promptArms,
271
+ seedsPerCase: args.seedsPerCase ?? DEFAULT_SEEDS_PER_CASE,
272
+ assistantModel: args.assistantModel || undefined,
273
+ promotePrimary: args.promotePrimary ?? false,
274
+ }),
275
+ }, profileOptions, { timeoutMs: 120_000, retries: 1 }));
276
+ const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
277
+ const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
278
+ const deadline = Date.now() + timeoutMs;
279
+ let latest = null;
280
+ while (Date.now() < deadline) {
281
+ latest = await fetchLatestRun(args.tenantId, args.replaySetId, enqueue.runId, profileOptions);
282
+ const status = latest.run?.status ?? '';
283
+ const runId = latest.run?.runId ?? '';
284
+ if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
285
+ return latest;
286
+ }
287
+ args.onWaiting?.(status || 'pending');
288
+ await sleep(pollIntervalMs);
289
+ }
290
+ if (!latest?.run
291
+ || latest.run.runId !== enqueue.runId
292
+ || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
293
+ throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
294
+ }
295
+ return latest;
296
+ }
227
297
  function parsePositiveInt(raw, fallback, max) {
228
298
  if (!raw)
229
299
  return fallback;
@@ -2,9 +2,14 @@ import type { CommandContext } from './command-context.js';
2
2
  import type { ProfileIOOptions } from './profiles.js';
3
3
  /**
4
4
  * `moda prompts propose` — turn a `prompts ab` run's failures into a revised
5
- * candidate registered as a new (unlabeled) prompt version. Does NOT promote:
6
- * the operator gates the candidate with a fresh `prompts ab` (ideally on the
7
- * printed holdout cases the revision never saw) and promotes only if it wins.
5
+ * candidate registered as a new (unlabeled) prompt version. Does NOT promote by
6
+ * default: the operator gates the candidate with a fresh `prompts ab` (ideally on
7
+ * the printed holdout cases the revision never saw) and promotes only if it wins.
8
+ *
9
+ * `--gate` chains that gate automatically: it A/Bs the fresh candidate (proposed
10
+ * arm) vs the on-disk baseline (prod arm) over the same replay set and prints the
11
+ * verdict. `--promote-on-win` then promotes the candidate to `prod` ONLY on a
12
+ * strict candidate win. `--model=<slug>` overrides the revision model.
8
13
  */
9
14
  export declare function runPromptPropose(positionals: string[], flags: Record<string, string>, profileOptions: ProfileIOOptions, context: CommandContext): Promise<void>;
10
15
  //# sourceMappingURL=prompts-propose.d.ts.map
@@ -1,14 +1,20 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
  import { callControlAPI } from './api-client.js';
4
- import { validateConfig } from './config.js';
4
+ import { resolveTenantId, validateConfig } from './config.js';
5
+ import { candidateOutcome, enqueueAndPollComparison, formatVerdict, resolvePromptArmSpec, } from './prompts-ab.js';
5
6
  // Propose is a single OpenRouter revision behind the backend; give it room.
6
7
  const PROPOSE_TIMEOUT_MS = 300_000;
7
8
  /**
8
9
  * `moda prompts propose` — turn a `prompts ab` run's failures into a revised
9
- * candidate registered as a new (unlabeled) prompt version. Does NOT promote:
10
- * the operator gates the candidate with a fresh `prompts ab` (ideally on the
11
- * printed holdout cases the revision never saw) and promotes only if it wins.
10
+ * candidate registered as a new (unlabeled) prompt version. Does NOT promote by
11
+ * default: the operator gates the candidate with a fresh `prompts ab` (ideally on
12
+ * the printed holdout cases the revision never saw) and promotes only if it wins.
13
+ *
14
+ * `--gate` chains that gate automatically: it A/Bs the fresh candidate (proposed
15
+ * arm) vs the on-disk baseline (prod arm) over the same replay set and prints the
16
+ * verdict. `--promote-on-win` then promotes the candidate to `prod` ONLY on a
17
+ * strict candidate win. `--model=<slug>` overrides the revision model.
12
18
  */
13
19
  export async function runPromptPropose(positionals, flags, profileOptions, context) {
14
20
  validateConfig();
@@ -16,21 +22,124 @@ export async function runPromptPropose(positionals, flags, profileOptions, conte
16
22
  const fromRunId = flags['from-run'] || flags['from-run-id'] || flags['run-id'];
17
23
  const replaySetId = flags['set-id'] || flags['replay-set-id'];
18
24
  if (!promptKey || !fromRunId || !replaySetId) {
19
- throw new Error('Usage: moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=8] [--out=path.prompt.md]');
25
+ throw new Error('Usage: moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=8] [--model=<slug>] [--out=path.prompt.md] [--gate] [--promote-on-win]');
20
26
  }
21
27
  const maxDossiers = parsePositiveInt(flags['max-dossiers'], 8, 16);
22
- const result = (await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, { method: 'POST', body: JSON.stringify({ fromRunId, replaySetId, maxDossiers }) }, profileOptions, { timeoutMs: PROPOSE_TIMEOUT_MS, retries: 0 }));
28
+ const model = (flags.model ?? flags['revise-model'] ?? '').trim();
29
+ const result = (await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, {
30
+ method: 'POST',
31
+ body: JSON.stringify({ fromRunId, replaySetId, maxDossiers, model: model || undefined }),
32
+ }, profileOptions, { timeoutMs: PROPOSE_TIMEOUT_MS, retries: 0 }));
23
33
  let outPath;
24
34
  if (result.status === 'proposed' && result.content && flags.out) {
25
35
  outPath = isAbsolute(flags.out) ? flags.out : resolve(process.cwd(), flags.out);
26
36
  writeFileSync(outPath, result.content, 'utf8');
27
37
  }
38
+ // The gate needs the candidate content in-hand for the proposed arm.
39
+ const wantGate = flags.gate === 'true';
40
+ const promoteOnWin = flags['promote-on-win'] === 'true';
41
+ let gate;
42
+ if (wantGate) {
43
+ if (result.status !== 'proposed' || !result.content) {
44
+ gate = {
45
+ ran: false,
46
+ scope: 'full-set',
47
+ scopeNote: `no candidate to gate (${result.status})`,
48
+ verdict: 'skipped',
49
+ outcome: 'no-run',
50
+ runId: null,
51
+ promoted: false,
52
+ };
53
+ }
54
+ else {
55
+ gate = await runGate({
56
+ result,
57
+ promptKey,
58
+ replaySetId,
59
+ flags,
60
+ promoteOnWin,
61
+ profileOptions,
62
+ context,
63
+ });
64
+ }
65
+ }
66
+ if (context.outputMode === 'human') {
67
+ printHuman(result, outPath, gate);
68
+ }
69
+ context.output.writeData({
70
+ ...result,
71
+ ...(outPath ? { outPath } : {}),
72
+ ...(gate ? { gate } : {}),
73
+ });
74
+ }
75
+ async function runGate(args) {
76
+ const { result, promptKey, replaySetId, flags, promoteOnWin, profileOptions, context } = args;
77
+ const tenantId = flags['tenant-id'] || resolveTenantId(profileOptions);
78
+ if (!tenantId) {
79
+ throw new Error('Missing tenant id for --gate. Run `moda init` or pass --tenant-id=<id>.');
80
+ }
81
+ // prod arm = the on-disk/registry baseline. `promptKey` is always a registry
82
+ // key here, so resolve key-first (`preferKey`) — otherwise a same-named file or
83
+ // dir in the cwd could shadow it and gate the candidate against wrong content.
84
+ // proposed arm = the freshly proposed candidate content returned above.
85
+ const prodArm = resolvePromptArmSpec(promptKey, 'baseline', { preferKey: true });
86
+ const proposedArm = {
87
+ content: result.content ?? '',
88
+ prompt_version_id: result.versionId ?? '',
89
+ prompt_id: '',
90
+ prompt_label: 'candidate',
91
+ };
92
+ const seedsPerCase = parsePositiveInt(flags.seeds ?? flags['seeds-per-case'], 3, 10);
93
+ const assistantModel = (flags['assistant-model'] ?? '').trim();
94
+ const timeoutMs = parsePositiveInt(flags['gate-timeout'] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000);
95
+ const pollIntervalMs = parsePositiveInt(flags['poll-interval'], 15_000, 120_000);
96
+ // The /run endpoint scores the WHOLE replay set — there is no per-case subset
97
+ // knob — so the gate cannot be scoped to just the holdout ids the revision
98
+ // never saw. Say so plainly so a win isn't read as overfitting-proof.
99
+ const scopeNote = result.holdoutCaseIds.length
100
+ ? `gated over the FULL replay set (${result.holdoutCaseIds.length} holdout case(s) could not be isolated — the run endpoint scores every case)`
101
+ : 'gated over the FULL replay set';
28
102
  if (context.outputMode === 'human') {
29
- printHuman(result, outPath);
103
+ process.stderr.write(`\nGating candidate for '${promptKey}' — ${scopeNote}...\n`);
30
104
  }
31
- context.output.writeData(outPath ? { ...result, outPath } : result);
105
+ const payload = await enqueueAndPollComparison({
106
+ tenantId,
107
+ replaySetId,
108
+ promptArms: { prod: prodArm, proposed: proposedArm },
109
+ seedsPerCase,
110
+ assistantModel: assistantModel || undefined,
111
+ promotePrimary: false,
112
+ timeoutMs,
113
+ pollIntervalMs,
114
+ onWaiting: context.outputMode === 'human'
115
+ ? (status) => process.stderr.write(`Waiting for gate run (${status})...\n`)
116
+ : undefined,
117
+ }, profileOptions);
118
+ const outcome = candidateOutcome(payload);
119
+ const gate = {
120
+ ran: true,
121
+ scope: 'full-set',
122
+ scopeNote,
123
+ verdict: formatVerdict(payload),
124
+ outcome,
125
+ runId: payload.run?.runId ?? null,
126
+ promoted: false,
127
+ };
128
+ if (promoteOnWin) {
129
+ if (outcome === 'candidate' && result.versionId) {
130
+ await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/labels`, { method: 'POST', body: JSON.stringify({ label: 'prod', versionId: result.versionId }) }, profileOptions);
131
+ gate.promoted = true;
132
+ }
133
+ else {
134
+ gate.promoteSkippedReason =
135
+ outcome === 'candidate'
136
+ ? 'no candidate versionId to promote'
137
+ : `candidate did not strictly win (${outcome})`;
138
+ }
139
+ }
140
+ return gate;
32
141
  }
33
- function printHuman(result, outPath) {
142
+ function printHuman(result, outPath, gate) {
34
143
  const w = (s) => process.stderr.write(s + '\n');
35
144
  if (result.status !== 'proposed') {
36
145
  w(`No candidate produced (${result.status}): ${result.note}`);
@@ -46,7 +155,33 @@ function printHuman(result, outPath) {
46
155
  w(` risks: ${result.risks.map((r) => `- ${r}`).join('\n ')}`);
47
156
  if (outPath)
48
157
  w(` written to: ${outPath}`);
158
+ if (result.lowN) {
159
+ w('');
160
+ w(` ⚠ low sample: ${result.lowN}`);
161
+ }
49
162
  w('');
163
+ if (gate?.ran) {
164
+ w('Gate (automatic A/B of candidate vs baseline):');
165
+ w(` scope: ${gate.scopeNote}`);
166
+ w(` verdict: ${gate.verdict}`);
167
+ if (gate.runId)
168
+ w(` run id: ${gate.runId}`);
169
+ if (gate.promoted) {
170
+ w(` promoted: candidate won — promoted version ${result.versionId} to label 'prod'`);
171
+ }
172
+ else if (gate.promoteSkippedReason) {
173
+ w(` promote: skipped (${gate.promoteSkippedReason})`);
174
+ }
175
+ w('');
176
+ if (!gate.promoted) {
177
+ w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
178
+ }
179
+ return;
180
+ }
181
+ if (gate && !gate.ran) {
182
+ w(`Gate not run: ${gate.scopeNote}`);
183
+ w('');
184
+ }
50
185
  w('Next — gate on holdout cases the revision never saw, then promote if it wins:');
51
186
  if (result.holdoutCaseIds.length) {
52
187
  w(` holdout cases (${result.holdoutCaseIds.length}): ${result.holdoutCaseIds.join(', ')}`);
@@ -65,6 +200,7 @@ function printHuman(result, outPath) {
65
200
  w(` # re-run with --out=<path.prompt.md> to materialize the candidate, then:`);
66
201
  w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=<path.prompt.md> --auto-generate`);
67
202
  }
203
+ w(` # or: moda prompts propose ... --gate --promote-on-win (auto-A/B, promote on a strict win)`);
68
204
  w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
69
205
  }
70
206
  function parsePositiveInt(raw, fallback, max) {
package/dist/schemas.d.ts CHANGED
@@ -43,24 +43,24 @@ export declare const ConversationsSchema: z.ZodObject<{
43
43
  }, "strip", z.ZodTypeAny, {
44
44
  search?: string | undefined;
45
45
  environment?: "staging" | "all" | "development" | "production" | undefined;
46
+ outcome?: "any" | "positive" | "negative" | undefined;
46
47
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
47
48
  limit?: number | undefined;
48
49
  offset?: number | undefined;
49
50
  cluster_id?: string | undefined;
50
51
  user_id?: string | undefined;
51
52
  world_state?: string | undefined;
52
- outcome?: "any" | "positive" | "negative" | undefined;
53
53
  include_world_state?: boolean | undefined;
54
54
  }, {
55
55
  search?: string | undefined;
56
56
  environment?: "staging" | "all" | "development" | "production" | undefined;
57
+ outcome?: "any" | "positive" | "negative" | undefined;
57
58
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
58
59
  limit?: number | undefined;
59
60
  offset?: number | undefined;
60
61
  cluster_id?: string | undefined;
61
62
  user_id?: string | undefined;
62
63
  world_state?: string | undefined;
63
- outcome?: "any" | "positive" | "negative" | undefined;
64
64
  include_world_state?: unknown;
65
65
  }>;
66
66
  /** moda world-state <conversationId> — per-conversation world-state drill-in. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.18.0",
3
+ "version": "1.18.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-09T22:31:35.483Z",
4
- "cli_version": "1.18.0",
3
+ "bundled_at": "2026-07-10T23:46:28.191Z",
4
+ "cli_version": "1.18.1",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",