@moda-ai/cli 1.10.1 → 1.11.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/agent-schemas.js +60 -3
- package/dist/auth.js +5 -1
- package/dist/cli.js +124 -38
- package/dist/config.js +5 -3
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +40 -0
- package/dist/exit-funnel.d.ts +14 -0
- package/dist/exit-funnel.js +49 -0
- package/dist/harness.js +18 -6
- package/dist/init/index.js +57 -4
- package/dist/init/tenant.d.ts +6 -1
- package/dist/init/tenant.js +35 -11
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js +111 -0
- package/dist/output.d.ts +44 -4
- package/dist/output.js +337 -32
- package/dist/prompts.d.ts +2 -1
- package/dist/prompts.js +38 -13
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +81 -0
- package/dist/schemas.d.ts +2 -2
- package/dist/skills.d.ts +8 -2
- package/dist/skills.js +165 -14
- package/package.json +1 -1
- package/skills/moda-cli/SKILL.md +51 -7
package/dist/skills.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CommandContext } from './command-context.js';
|
|
2
2
|
import type { ProfileIOOptions } from './profiles.js';
|
|
3
3
|
interface PullSummary {
|
|
4
4
|
total: number;
|
|
@@ -46,6 +46,12 @@ export declare function runSkillSync(flags?: Record<string, string>, profileOpti
|
|
|
46
46
|
/**
|
|
47
47
|
* `moda skills <subcommand>` dispatcher.
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
type LegacySkillsArgs = {
|
|
50
|
+
command: string;
|
|
51
|
+
positional: string | undefined;
|
|
52
|
+
positionals: string[];
|
|
53
|
+
flags: Record<string, string>;
|
|
54
|
+
};
|
|
55
|
+
export declare function runSkillsCommand(input: CommandContext | LegacySkillsArgs): Promise<number>;
|
|
50
56
|
export {};
|
|
51
57
|
//# sourceMappingURL=skills.d.ts.map
|
package/dist/skills.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createHash } from 'node:crypto';
|
|
|
20
20
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
21
21
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from 'node:path';
|
|
22
22
|
import { callControlAPI } from './api-client.js';
|
|
23
|
+
import { createCommandContext } from './command-context.js';
|
|
23
24
|
import { resolveApiKey, resolveIngestUrl, resolveTenantId } from './config.js';
|
|
24
25
|
import { claudeSkillToCursorRule } from './init/rules.js';
|
|
25
26
|
import { detectAgents } from './init/mcp.js';
|
|
@@ -27,6 +28,7 @@ import { CliInputError } from './errors.js';
|
|
|
27
28
|
const SKILLS_REQUEST_TIMEOUT_MS = 15_000;
|
|
28
29
|
const VALID_STATUSES = new Set(['approved', 'proposed', 'all']);
|
|
29
30
|
const SKILLS_MANIFEST_PATH = join('.moda', 'skills.yml');
|
|
31
|
+
const SKILL_PROPOSALS_MANIFEST_PATH = join('.moda', 'skill-proposals.yml');
|
|
30
32
|
/**
|
|
31
33
|
* Fetch a tenant's generated skills from the ingest worker. Uses the persisted
|
|
32
34
|
* API key + ingest URL (`~/.moda/config.json`), mirroring snapshot auth.
|
|
@@ -342,15 +344,110 @@ function getGitCommit(cwd) {
|
|
|
342
344
|
return undefined;
|
|
343
345
|
}
|
|
344
346
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
347
|
+
async function fetchSkillProposals(flags, profileOptions = {}) {
|
|
348
|
+
const params = new URLSearchParams();
|
|
349
|
+
if (flags['generation-run-id'])
|
|
350
|
+
params.set('generationRunId', flags['generation-run-id']);
|
|
351
|
+
const status = flags.status ?? 'ready_for_pr';
|
|
352
|
+
if (status !== 'all')
|
|
353
|
+
params.set('status', status);
|
|
354
|
+
if (flags['tenant-id'])
|
|
355
|
+
params.set('tenantId', flags['tenant-id']);
|
|
356
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
357
|
+
const response = (await callControlAPI(`/skills/proposals${suffix}`, { method: 'GET' }, profileOptions));
|
|
358
|
+
return Array.isArray(response.proposals) ? response.proposals : [];
|
|
359
|
+
}
|
|
360
|
+
async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
|
|
361
|
+
const cwd = options.cwd ?? process.cwd();
|
|
362
|
+
const params = new URLSearchParams();
|
|
363
|
+
if (flags['tenant-id'])
|
|
364
|
+
params.set('tenantId', flags['tenant-id']);
|
|
365
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
366
|
+
const proposal = (await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: 'GET' }, profileOptions));
|
|
367
|
+
if (!proposal.skillMd?.trim()) {
|
|
368
|
+
throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
|
|
369
|
+
}
|
|
370
|
+
const safeId = safeSkillId(proposal.skillId);
|
|
371
|
+
const relativePath = `.claude/skills/${safeId}/SKILL.md`;
|
|
372
|
+
const writeStatus = writeIfChanged(join(cwd, relativePath), proposal.skillMd);
|
|
373
|
+
if (writeStatus === 'failed') {
|
|
374
|
+
throw new Error(`Failed to write ${relativePath}`);
|
|
375
|
+
}
|
|
376
|
+
writeSkillProposalManifest(cwd, proposal, relativePath);
|
|
377
|
+
let acknowledged = true;
|
|
378
|
+
let ackError;
|
|
379
|
+
try {
|
|
380
|
+
await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
|
|
381
|
+
method: 'POST',
|
|
382
|
+
body: JSON.stringify({ appliedBy: 'moda-cli' }),
|
|
383
|
+
}, profileOptions);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
acknowledged = false;
|
|
387
|
+
ackError = err instanceof Error ? err.message : String(err);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
proposalId,
|
|
391
|
+
skillId: proposal.skillId,
|
|
392
|
+
path: relativePath,
|
|
393
|
+
status: writeStatus,
|
|
394
|
+
acknowledged,
|
|
395
|
+
ackError,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function safeSkillId(skillId) {
|
|
399
|
+
const safeId = basename(skillId || '');
|
|
400
|
+
if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
|
|
401
|
+
throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
|
|
402
|
+
}
|
|
403
|
+
return safeId;
|
|
404
|
+
}
|
|
405
|
+
function writeSkillProposalManifest(cwd, proposal, relativePath) {
|
|
406
|
+
const manifestPath = join(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
|
|
407
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
408
|
+
const existing = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : 'applied_proposals:\n';
|
|
409
|
+
const entry = [
|
|
410
|
+
` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
|
|
411
|
+
` skill_id: "${escapeYamlString(proposal.skillId)}"`,
|
|
412
|
+
` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
|
|
413
|
+
` skill_version: ${Number(proposal.skillVersion || 0)}`,
|
|
414
|
+
` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
|
|
415
|
+
` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
|
|
416
|
+
` topic_id: "${escapeYamlString(proposal.topicId)}"`,
|
|
417
|
+
` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
|
|
418
|
+
` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
|
|
419
|
+
` path: "${escapeYamlString(relativePath)}"`,
|
|
420
|
+
` applied_at: "${new Date().toISOString()}"`,
|
|
421
|
+
].join('\n');
|
|
422
|
+
const prefix = existing.endsWith('\n') ? existing : `${existing}\n`;
|
|
423
|
+
writeFileSync(manifestPath, `${prefix}${entry}\n`);
|
|
424
|
+
}
|
|
425
|
+
function escapeYamlString(value) {
|
|
426
|
+
return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
427
|
+
}
|
|
428
|
+
function formatProposalRate(value) {
|
|
429
|
+
return `${(Number(value || 0) * 100).toFixed(1)}%`;
|
|
430
|
+
}
|
|
431
|
+
export async function runSkillsCommand(input) {
|
|
432
|
+
const context = 'output' in input
|
|
433
|
+
? input
|
|
434
|
+
: createCommandContext({
|
|
435
|
+
command: input.command,
|
|
436
|
+
positional: input.positional,
|
|
437
|
+
positionals: input.positionals,
|
|
438
|
+
flags: input.flags,
|
|
439
|
+
}, {
|
|
440
|
+
outputMode: input.flags.agent === 'true' ? 'agent' : input.flags.json === 'true' ? 'json' : 'human',
|
|
441
|
+
});
|
|
442
|
+
const sub = context.positional;
|
|
443
|
+
const positionals = context.positionals ?? (sub ? [sub] : []);
|
|
444
|
+
const flags = context.flags;
|
|
350
445
|
if (sub === 'gen') {
|
|
351
|
-
const result = await generateSkills(
|
|
352
|
-
if (
|
|
353
|
-
|
|
446
|
+
const result = await generateSkills(flags);
|
|
447
|
+
if (context.outputMode !== 'human') {
|
|
448
|
+
context.output.writeData(result, {
|
|
449
|
+
status: Number(result.sessions_failed ?? 0) > 0 ? 'error' : 'success',
|
|
450
|
+
});
|
|
354
451
|
}
|
|
355
452
|
else if (result.accepted) {
|
|
356
453
|
console.log([
|
|
@@ -371,19 +468,69 @@ export async function runSkillsCommand(args) {
|
|
|
371
468
|
}
|
|
372
469
|
return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
|
|
373
470
|
}
|
|
471
|
+
if (sub === 'proposals' && positionals[1] === 'list') {
|
|
472
|
+
const proposals = await fetchSkillProposals(flags, { profile: context.profile });
|
|
473
|
+
if (flags.json === 'true') {
|
|
474
|
+
console.log(JSON.stringify({ proposals }));
|
|
475
|
+
}
|
|
476
|
+
else if (proposals.length === 0) {
|
|
477
|
+
console.log('No skill proposals found.');
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
for (const proposal of proposals) {
|
|
481
|
+
console.log([
|
|
482
|
+
proposal.proposalId,
|
|
483
|
+
proposal.proposalStatus,
|
|
484
|
+
`${proposal.skillId}@v${proposal.skillVersion}`,
|
|
485
|
+
`delta=${formatProposalRate(proposal.deltaPassRate)}`,
|
|
486
|
+
`baseline=${formatProposalRate(proposal.baselinePassRate)}`,
|
|
487
|
+
`candidate=${formatProposalRate(proposal.candidatePassRate)}`,
|
|
488
|
+
proposal.name,
|
|
489
|
+
].filter(Boolean).join('\t'));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
if (sub === 'proposal' && positionals[1] === 'apply') {
|
|
495
|
+
const proposalId = positionals[2] || flags['proposal-id'];
|
|
496
|
+
if (!proposalId) {
|
|
497
|
+
throw new CliInputError('A proposal id is required', 'Usage: moda skills proposal apply <proposal-id>');
|
|
498
|
+
}
|
|
499
|
+
const result = await applySkillProposal(proposalId, flags, { profile: context.profile });
|
|
500
|
+
if (flags.json === 'true') {
|
|
501
|
+
console.log(JSON.stringify(result));
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
console.log([
|
|
505
|
+
`${result.status} ${result.path}`,
|
|
506
|
+
result.acknowledged ? 'acknowledged: true' : 'acknowledged: false',
|
|
507
|
+
].join('\n'));
|
|
508
|
+
}
|
|
509
|
+
if (!result.acknowledged) {
|
|
510
|
+
// The local SKILL.md was written, but the server still lists this
|
|
511
|
+
// proposal as ready_for_pr. Warn (to stderr so JSON stdout stays clean)
|
|
512
|
+
// and exit non-zero so the failed acknowledgement isn't silently treated
|
|
513
|
+
// as success — re-running `apply` re-attempts the acknowledgement.
|
|
514
|
+
console.error(`Warning: wrote ${result.path} but failed to acknowledge proposal ${proposalId} on the server` +
|
|
515
|
+
(result.ackError ? ` (${result.ackError})` : '') +
|
|
516
|
+
'. The proposal is still listed as ready — re-run `moda skills proposal apply` to acknowledge it.');
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
374
521
|
if (sub === 'sync') {
|
|
375
|
-
const result = await runSkillSync(
|
|
376
|
-
|
|
522
|
+
const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
|
|
523
|
+
context.output.writeData({ synced: result.synced, dryRun: result.dryRun });
|
|
377
524
|
return 0;
|
|
378
525
|
}
|
|
379
526
|
if (sub !== 'pull') {
|
|
380
|
-
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] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
|
|
527
|
+
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 proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
|
|
381
528
|
}
|
|
382
|
-
const status = (
|
|
529
|
+
const status = (flags.status ?? 'approved').toLowerCase();
|
|
383
530
|
if (!VALID_STATUSES.has(status)) {
|
|
384
531
|
throw new CliInputError(`Invalid --status '${status}'`, `Must be one of: ${[...VALID_STATUSES].join(', ')}`);
|
|
385
532
|
}
|
|
386
|
-
const summary = await pullSkills({ status });
|
|
533
|
+
const summary = await pullSkills({ status, silent: context.outputMode !== 'human' });
|
|
387
534
|
const parts = [
|
|
388
535
|
summary.pruned && `${summary.pruned} pruned`,
|
|
389
536
|
summary.wrote && `${summary.wrote} written`,
|
|
@@ -391,9 +538,12 @@ export async function runSkillsCommand(args) {
|
|
|
391
538
|
summary.unchanged && `${summary.unchanged} unchanged`,
|
|
392
539
|
summary.failed && `${summary.failed} failed`,
|
|
393
540
|
].filter(Boolean);
|
|
394
|
-
if (summary.total > 0) {
|
|
541
|
+
if (context.outputMode === 'human' && summary.total > 0) {
|
|
395
542
|
console.log(` Done — ${parts.join(', ') || 'no changes'}`);
|
|
396
543
|
}
|
|
544
|
+
if (context.outputMode !== 'human') {
|
|
545
|
+
context.output.writeData({ status, summary });
|
|
546
|
+
}
|
|
397
547
|
return summary.failed > 0 ? 1 : 0;
|
|
398
548
|
}
|
|
399
549
|
async function generateSkills(flags) {
|
|
@@ -418,6 +568,7 @@ async function generateSkills(flags) {
|
|
|
418
568
|
start_at: flags['start-at'] || undefined,
|
|
419
569
|
end_at: flags['end-at'] || undefined,
|
|
420
570
|
clustering_run_id: flags['clustering-run-id'] || undefined,
|
|
571
|
+
replay_set_id: flags['replay-set-id'] || flags['eval-set-id'] || undefined,
|
|
421
572
|
run_clustering: flags['no-clustering'] !== 'true',
|
|
422
573
|
source: normalizeSkillSource(flags.source),
|
|
423
574
|
clustering_timeout_seconds: parsePositiveInt(flags['clustering-timeout-seconds'], 0),
|
package/package.json
CHANGED
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: moda-cli
|
|
3
|
-
version: 2.
|
|
3
|
+
version: 2.1.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, active loop detections, 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
|
|
|
@@ -80,10 +80,14 @@ export MODA_API_KEY="moda_sk_..." # from moda.dev/settings
|
|
|
80
80
|
|
|
81
81
|
## Agent protocol
|
|
82
82
|
|
|
83
|
-
`moda
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
Run `moda manifest --json` first when integrating with the CLI. It returns the
|
|
84
|
+
hand-maintained `moda.manifest.v1` contract: commands, output modes, exit codes,
|
|
85
|
+
event catalog, env vars, schema ids, and a glossary.
|
|
86
|
+
|
|
87
|
+
`moda` is agent-safe: it picks an output mode automatically, signals the
|
|
88
|
+
orchestrator's next action through exit codes, and emits schema-versioned JSON
|
|
89
|
+
on stdout in agent modes. Errors also ride stdout envelopes/events in agent
|
|
90
|
+
mode; stderr is only for human-mode prose/progress.
|
|
87
91
|
|
|
88
92
|
### Output mode auto-selection
|
|
89
93
|
|
|
@@ -111,8 +115,11 @@ Branch on the exit code; it is stable and cheaper than parsing the payload:
|
|
|
111
115
|
| Exit | Meaning | How to treat it |
|
|
112
116
|
|---|---|---|
|
|
113
117
|
| `0` | ok — answer is grounded in Moda Cloud intelligence | Trust it. |
|
|
118
|
+
| `1` | error — bad input, API/network failure, or unexpected failure | Read `errors[]`; retry only when the code is retryable. |
|
|
114
119
|
| `3` | degraded — Moda Cloud was unavailable; answer synthesized from local Data API evidence | Answer is present but lower-trust; verify before acting. |
|
|
115
|
-
| `
|
|
120
|
+
| `4` | auth required | Run `auth.login_command`, then re-run `auth.resume_command` if present. |
|
|
121
|
+
| `5` | input required | Pick a choice, append its `resume_flags`, and re-run. |
|
|
122
|
+
| `130` | cancelled | Treat as interrupted or closed pipe; do not retry blindly. |
|
|
116
123
|
|
|
117
124
|
### The `moda.agent.v1` envelope
|
|
118
125
|
|
|
@@ -126,10 +133,44 @@ Fields an agent should read:
|
|
|
126
133
|
- `next_commands[]` — suggested follow-up commands (each carries `mutability`
|
|
127
134
|
and `requires_approval`).
|
|
128
135
|
- `warnings[]`, `errors[]`.
|
|
136
|
+
- `meta.run_id` — correlates a non-stream envelope with stream events.
|
|
129
137
|
- `data` — the raw command payload. For `ask`, `data.source` is `cloud` or
|
|
130
138
|
`local_fallback` and `data.degraded` is a boolean — the canonical in-payload
|
|
131
139
|
way to detect degradation (in addition to exit code 3).
|
|
132
140
|
|
|
141
|
+
`errors[].code` is one of `input_error`, `api_error`, `network_error`,
|
|
142
|
+
`auth_required`, `input_required`, `cancelled`, `command_error`,
|
|
143
|
+
`unknown_error`, or a documented domain code such as `cloud_unavailable`.
|
|
144
|
+
|
|
145
|
+
### The `moda.agent_event.v1` stream
|
|
146
|
+
|
|
147
|
+
Use `--agent --stream` for commands whose progress matters (`init`, `harness
|
|
148
|
+
analyze`, and any command where liveness matters). The stream is NDJSON, one
|
|
149
|
+
event per stdout line.
|
|
150
|
+
|
|
151
|
+
Ordering guarantees:
|
|
152
|
+
|
|
153
|
+
- First event of every agent-stream run is `started`.
|
|
154
|
+
- `run_id` is constant within a run; `seq` starts at `1` and increments without
|
|
155
|
+
gaps.
|
|
156
|
+
- Exactly one terminal event ends every stream: `completed`, `error`,
|
|
157
|
+
`cancelled`, `needs_input`, or `auth_required` — on every exit path including
|
|
158
|
+
thrown errors, SIGINT, and refused input. Absence of a terminal event = the CLI
|
|
159
|
+
crashed.
|
|
160
|
+
- `heartbeat` fires every 10s (`MODA_HEARTBEAT_MS`, `0` disables) whenever no
|
|
161
|
+
other event is emitted. No heartbeat for more than 30s during a run is the
|
|
162
|
+
canonical stall signal.
|
|
163
|
+
- Secrets are redacted at the emit boundary; never expect full `moda_sk_`,
|
|
164
|
+
`moda_ct_`, JWT, bearer token, or `api_key` values in events.
|
|
165
|
+
|
|
166
|
+
Interaction is exit-and-reinvoke, not stdin:
|
|
167
|
+
|
|
168
|
+
- `needs_input` carries `input_request: { id, question, kind, choices,
|
|
169
|
+
resume_command }` and exits `5`. Choose a value and re-run with that choice's
|
|
170
|
+
`resume_flags`.
|
|
171
|
+
- `auth_required` carries `auth: { reason, login_command, resume_command }` and
|
|
172
|
+
exits `4`. Run the argv array in `login_command`, then resume.
|
|
173
|
+
|
|
133
174
|
### Detecting a degraded answer
|
|
134
175
|
|
|
135
176
|
Treat an answer as degraded if **any** of these hold:
|
|
@@ -141,7 +182,8 @@ Treat an answer as degraded if **any** of these hold:
|
|
|
141
182
|
### Schema introspection
|
|
142
183
|
|
|
143
184
|
```bash
|
|
144
|
-
moda --json
|
|
185
|
+
moda manifest --json # discover commands/events/exit codes/glossary
|
|
186
|
+
moda --json-schemas # print JSON Schemas for the output contract
|
|
145
187
|
```
|
|
146
188
|
|
|
147
189
|
### Recommended agent usage
|
|
@@ -151,6 +193,8 @@ answer=$(moda ask "what should I fix first?")
|
|
|
151
193
|
case $? in
|
|
152
194
|
0) printf 'grounded answer:\n%s\n' "$answer" ;;
|
|
153
195
|
3) printf 'DEGRADED (local fallback — verify before acting):\n%s\n' "$answer" ;;
|
|
196
|
+
4) echo "$answer" | jq -r '.errors[0].message'; exit 4 ;;
|
|
197
|
+
5) echo "$answer" | jq -r '.errors[0].message'; exit 5 ;;
|
|
154
198
|
*) echo "moda ask failed:"; echo "$answer" | jq -r '.errors[].message'; exit 1 ;;
|
|
155
199
|
esac
|
|
156
200
|
|