@moda-ai/cli 1.10.2 → 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 +33 -19
- package/package.json +1 -1
- package/skills/moda-cli/SKILL.md +51 -7
package/dist/agent-schemas.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// - ProductionAskResult in ./production-intelligence.ts (production_ask.v0.1)
|
|
15
15
|
// - ModaIntelligenceV1 in ./intelligence-schema.ts (moda.intelligence.v1)
|
|
16
16
|
import { MODA_INTELLIGENCE_V1_JSON_SCHEMA } from './intelligence-schema.js';
|
|
17
|
+
import { AGENT_ERROR_CODES } from './output.js';
|
|
17
18
|
const agentSummarySchema = {
|
|
18
19
|
type: 'object',
|
|
19
20
|
additionalProperties: false,
|
|
@@ -83,15 +84,54 @@ const agentArtifactSchema = {
|
|
|
83
84
|
description: { type: 'string' },
|
|
84
85
|
},
|
|
85
86
|
};
|
|
87
|
+
const agentInputRequestSchema = {
|
|
88
|
+
type: 'object',
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
required: ['id', 'question', 'kind'],
|
|
91
|
+
properties: {
|
|
92
|
+
id: { type: 'string' },
|
|
93
|
+
question: { type: 'string' },
|
|
94
|
+
kind: { type: 'string', enum: ['select', 'confirm', 'text'] },
|
|
95
|
+
choices: {
|
|
96
|
+
type: 'array',
|
|
97
|
+
items: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
required: ['value', 'label', 'resume_flags'],
|
|
101
|
+
properties: {
|
|
102
|
+
value: { type: 'string' },
|
|
103
|
+
label: { type: 'string' },
|
|
104
|
+
resume_flags: { type: 'array', items: { type: 'string' } },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
resume_command: { type: 'array', items: { type: 'string' } },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const agentAuthSchema = {
|
|
112
|
+
type: 'object',
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
required: ['reason', 'login_command'],
|
|
115
|
+
properties: {
|
|
116
|
+
reason: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
enum: ['no_stored_credentials', 'token_expired', 'token_revoked', 'renew_failed'],
|
|
119
|
+
},
|
|
120
|
+
login_command: { type: 'array', items: { type: 'string' } },
|
|
121
|
+
resume_command: { type: 'array', items: { type: 'string' } },
|
|
122
|
+
},
|
|
123
|
+
};
|
|
86
124
|
const agentErrorSchema = {
|
|
87
125
|
type: 'object',
|
|
88
126
|
additionalProperties: false,
|
|
89
127
|
required: ['code', 'message', 'blocking'],
|
|
90
128
|
properties: {
|
|
91
|
-
code: { type: 'string' },
|
|
129
|
+
code: { type: 'string', enum: AGENT_ERROR_CODES },
|
|
92
130
|
message: { type: 'string' },
|
|
93
131
|
cause: { type: 'string' },
|
|
94
132
|
suggested_commands: { type: 'array', items: { type: 'string' } },
|
|
133
|
+
input_request: agentInputRequestSchema,
|
|
134
|
+
auth: agentAuthSchema,
|
|
95
135
|
blocking: { type: 'boolean' },
|
|
96
136
|
},
|
|
97
137
|
};
|
|
@@ -138,7 +178,7 @@ const agentEnvelopeSchema = {
|
|
|
138
178
|
const agentEventSchema = {
|
|
139
179
|
$id: 'moda.agent_event.v1',
|
|
140
180
|
title: 'AgentEvent',
|
|
141
|
-
description: 'Streaming NDJSON event emitted in agent-stream mode by the moda CLI.',
|
|
181
|
+
description: 'Streaming NDJSON event emitted in agent-stream mode by the moda CLI. The first event is started; run_id is constant; seq starts at 1 and increments without gaps; exactly one terminal event (completed, error, cancelled, needs_input, auth_required) ends every stream; heartbeat absence for more than 30 seconds is the stall signal.',
|
|
142
182
|
type: 'object',
|
|
143
183
|
additionalProperties: false,
|
|
144
184
|
required: ['schema_version', 'event', 'command', 'generated_at'],
|
|
@@ -146,10 +186,25 @@ const agentEventSchema = {
|
|
|
146
186
|
schema_version: { type: 'string', const: 'moda.agent_event.v1' },
|
|
147
187
|
event: {
|
|
148
188
|
type: 'string',
|
|
149
|
-
enum: [
|
|
189
|
+
enum: [
|
|
190
|
+
'started',
|
|
191
|
+
'progress',
|
|
192
|
+
'artifact',
|
|
193
|
+
'approval_required',
|
|
194
|
+
'completed',
|
|
195
|
+
'error',
|
|
196
|
+
'heartbeat',
|
|
197
|
+
'needs_input',
|
|
198
|
+
'auth_required',
|
|
199
|
+
'cancelled',
|
|
200
|
+
'warning',
|
|
201
|
+
],
|
|
150
202
|
},
|
|
151
203
|
command: { type: 'string' },
|
|
152
204
|
generated_at: { type: 'string', format: 'date-time' },
|
|
205
|
+
run_id: { type: 'string' },
|
|
206
|
+
seq: { type: 'number' },
|
|
207
|
+
event_version: { type: 'number' },
|
|
153
208
|
status: { type: 'string', enum: ['ok', 'degraded', 'error'] },
|
|
154
209
|
phase: { type: 'string' },
|
|
155
210
|
message: { type: 'string' },
|
|
@@ -157,6 +212,8 @@ const agentEventSchema = {
|
|
|
157
212
|
artifact: agentArtifactSchema,
|
|
158
213
|
result: agentEnvelopeSchema,
|
|
159
214
|
error: agentErrorSchema,
|
|
215
|
+
input_request: agentInputRequestSchema,
|
|
216
|
+
auth: agentAuthSchema,
|
|
160
217
|
},
|
|
161
218
|
};
|
|
162
219
|
const productionEvidenceRefSchema = {
|
package/dist/auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createServer } from 'node:http';
|
|
|
3
3
|
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
4
4
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
5
5
|
import { resolveModaBaseUrl } from './config.js';
|
|
6
|
+
import { CliAuthError } from './errors.js';
|
|
6
7
|
import { getModaPaths, loadProfileConfig, saveProfileConfig, secretFilePathFromRef, writeSecretJsonFile, } from './profiles.js';
|
|
7
8
|
const CALLBACK_PORT = 8432;
|
|
8
9
|
const AUTH_TIMEOUT_MS = 120_000;
|
|
@@ -310,7 +311,10 @@ export async function login(screen = 'login', options = {}) {
|
|
|
310
311
|
export async function authFetch(path, options = {}, authOptions = {}) {
|
|
311
312
|
const token = loadToken(authOptions);
|
|
312
313
|
if (!token) {
|
|
313
|
-
throw new
|
|
314
|
+
throw new CliAuthError('Not authenticated. Run "moda auth login" to log in.', {
|
|
315
|
+
reason: 'no_stored_credentials',
|
|
316
|
+
login_command: ['moda', 'auth', 'login'],
|
|
317
|
+
});
|
|
314
318
|
}
|
|
315
319
|
const headers = new Headers(options.headers);
|
|
316
320
|
headers.set('Authorization', `Bearer ${token}`);
|
package/dist/cli.js
CHANGED
|
@@ -12,16 +12,18 @@ import { runSkillsCommand } from './skills.js';
|
|
|
12
12
|
import { runHarnessCommand } from './harness.js';
|
|
13
13
|
import { runSyncCommand } from './sync.js';
|
|
14
14
|
import { runDoctorCommand, runStatusCommand } from './doctor.js';
|
|
15
|
-
import { CliInputError } from './errors.js';
|
|
15
|
+
import { CliAuthError, CliCancellationError, CliInputError, CliInputRequiredError, isApiLikeError, isNetworkLikeError } from './errors.js';
|
|
16
16
|
import { buildSearchAdoptionTelemetry, isTelemetryDisabled, SEARCH_MODES, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
|
|
17
17
|
import { isUpdateCheckDisabled, printUpdateNotice, startUpdateCheck, stripUpdateCheckFlag, } from './update-check.js';
|
|
18
18
|
import { createCommandContext } from './command-context.js';
|
|
19
19
|
import { createCommandRegistry } from './command-registry.js';
|
|
20
|
-
import {
|
|
20
|
+
import { createCliOutput, EXIT_CANCELLED, resolveOutputMode, safeWriteStderr, safeWriteStdout, } from './output.js';
|
|
21
21
|
import { renderJsonSchemas } from './agent-schemas.js';
|
|
22
22
|
import { runAuthCommand, runConfigCommand, runKeysCommand, runProfilesCommand } from './profile-commands.js';
|
|
23
23
|
import { runAgentCommand } from './agent.js';
|
|
24
24
|
import { runProductionCommand } from './production-intelligence.js';
|
|
25
|
+
import { exitCodeForError, finishRun } from './exit-funnel.js';
|
|
26
|
+
import { buildManifest, renderManifestHuman } from './manifest.js';
|
|
25
27
|
export { CliInputError } from './errors.js';
|
|
26
28
|
export function parseArgs(argv) {
|
|
27
29
|
const args = argv.slice(2);
|
|
@@ -106,6 +108,7 @@ function printHelp() {
|
|
|
106
108
|
Commands:
|
|
107
109
|
init Set up Moda in your project
|
|
108
110
|
status Show concise local Moda setup status
|
|
111
|
+
manifest Describe the CLI machine protocol and commands
|
|
109
112
|
doctor Diagnose local Moda setup and data flow
|
|
110
113
|
auth login Authenticate with Moda
|
|
111
114
|
auth logout Clear cached Moda auth
|
|
@@ -136,6 +139,7 @@ Commands:
|
|
|
136
139
|
sync prompts Sync prompt versions to Moda Cloud
|
|
137
140
|
doctor --json Emit structured diagnostic checks
|
|
138
141
|
status --json Emit concise setup state
|
|
142
|
+
manifest --json Emit the CLI machine protocol manifest
|
|
139
143
|
overview Harness health briefing with production signals
|
|
140
144
|
clusters Browse topic cluster hierarchy
|
|
141
145
|
cluster-conversations <node_id> List conversations in a cluster
|
|
@@ -228,6 +232,7 @@ Init flags:
|
|
|
228
232
|
Examples:
|
|
229
233
|
moda init
|
|
230
234
|
moda status
|
|
235
|
+
moda manifest --json
|
|
231
236
|
moda doctor --json
|
|
232
237
|
moda profiles list
|
|
233
238
|
moda profiles use staging
|
|
@@ -377,10 +382,7 @@ function recordConversationsTelemetry(context, data) {
|
|
|
377
382
|
export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
|
|
378
383
|
switch (command) {
|
|
379
384
|
case 'prompts': {
|
|
380
|
-
await runPromptsCommand(
|
|
381
|
-
profile: context.profile,
|
|
382
|
-
env: context.env,
|
|
383
|
-
});
|
|
385
|
+
await runPromptsCommand(context);
|
|
384
386
|
break;
|
|
385
387
|
}
|
|
386
388
|
case 'overview': {
|
|
@@ -713,7 +715,7 @@ export const commandRegistry = createCommandRegistry([
|
|
|
713
715
|
const result = await runInit(flagsToArgs(context.flags), {
|
|
714
716
|
onEvent: (event) => {
|
|
715
717
|
context.output.writeEvent({
|
|
716
|
-
event: 'progress',
|
|
718
|
+
event: event.status === 'warning' ? 'warning' : 'progress',
|
|
717
719
|
phase: event.phase,
|
|
718
720
|
message: event.message,
|
|
719
721
|
elapsed_ms: Date.now() - context.startedAt,
|
|
@@ -726,6 +728,30 @@ export const commandRegistry = createCommandRegistry([
|
|
|
726
728
|
return { exitCode: 0, apiKey: result.apiKey };
|
|
727
729
|
},
|
|
728
730
|
},
|
|
731
|
+
{
|
|
732
|
+
name: 'manifest',
|
|
733
|
+
description: 'Describe the CLI machine protocol, commands, events, and exit codes',
|
|
734
|
+
examples: ['moda manifest --json', 'moda --agent manifest'],
|
|
735
|
+
auth: 'none',
|
|
736
|
+
outputModes: ['human', 'json', 'agent'],
|
|
737
|
+
mutability: 'read',
|
|
738
|
+
validateConfigBeforeRun: false,
|
|
739
|
+
resetApiRequestCountBeforeRun: false,
|
|
740
|
+
telemetry: 'none',
|
|
741
|
+
handler: async (context) => {
|
|
742
|
+
const manifest = buildManifest(commandRegistry.all());
|
|
743
|
+
if (context.outputMode === 'human') {
|
|
744
|
+
if (!context.quiet)
|
|
745
|
+
safeWriteStdout(`${renderManifestHuman(manifest)}\n`);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
context.output.writeData(manifest, {
|
|
749
|
+
summary: 'Moda CLI machine protocol manifest.',
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return { exitCode: 0 };
|
|
753
|
+
},
|
|
754
|
+
},
|
|
729
755
|
{
|
|
730
756
|
name: 'auth',
|
|
731
757
|
description: 'Authenticate and inspect browser auth sessions',
|
|
@@ -1035,55 +1061,58 @@ export const commandRegistry = createCommandRegistry([
|
|
|
1035
1061
|
resetApiRequestCountBeforeRun: false,
|
|
1036
1062
|
telemetry: 'result',
|
|
1037
1063
|
handler: async (context) => ({
|
|
1038
|
-
exitCode: await runSkillsCommand(
|
|
1039
|
-
command: context.command,
|
|
1040
|
-
positional: context.positional,
|
|
1041
|
-
positionals: context.positionals,
|
|
1042
|
-
flags: context.flags,
|
|
1043
|
-
}),
|
|
1064
|
+
exitCode: await runSkillsCommand(context),
|
|
1044
1065
|
}),
|
|
1045
1066
|
},
|
|
1046
1067
|
]);
|
|
1047
1068
|
// --- Main ---
|
|
1048
1069
|
export function classifyCliError(error) {
|
|
1049
|
-
if (error instanceof z.ZodError ||
|
|
1070
|
+
if (error instanceof z.ZodError ||
|
|
1071
|
+
error instanceof CliInputError ||
|
|
1072
|
+
error instanceof CliInputRequiredError ||
|
|
1073
|
+
error instanceof CliAuthError) {
|
|
1074
|
+
// Auth/input-required are caller-actionable states (missing credentials,
|
|
1075
|
+
// needs a choice), not unknown failures. Bucket them with validation so
|
|
1076
|
+
// the missing-API-key case keeps its pre-hardening telemetry type.
|
|
1050
1077
|
return 'validation_error';
|
|
1051
1078
|
}
|
|
1052
|
-
if (error instanceof ApiError) {
|
|
1079
|
+
if (error instanceof ApiError || isApiLikeError(error)) {
|
|
1053
1080
|
return 'api_error';
|
|
1054
1081
|
}
|
|
1055
|
-
if (error
|
|
1056
|
-
(error.message.startsWith('Network error:') ||
|
|
1057
|
-
error.message.startsWith('Request timed out'))) {
|
|
1082
|
+
if (isNetworkLikeError(error)) {
|
|
1058
1083
|
return 'network_error';
|
|
1059
1084
|
}
|
|
1060
1085
|
return 'unknown_error';
|
|
1061
1086
|
}
|
|
1062
1087
|
export function printCliError(error) {
|
|
1063
1088
|
if (error instanceof CliInputError) {
|
|
1064
|
-
|
|
1089
|
+
safeWriteStderr(`Error: ${error.message}\n`);
|
|
1065
1090
|
if (error.usage)
|
|
1066
|
-
|
|
1091
|
+
safeWriteStderr(`${error.usage}\n`);
|
|
1067
1092
|
}
|
|
1068
1093
|
else if (error instanceof z.ZodError) {
|
|
1069
1094
|
const issues = error.errors.map((e) => {
|
|
1070
1095
|
const field = e.path.length ? e.path.join('.') : 'input';
|
|
1071
1096
|
return ` ${field}: ${e.message}`;
|
|
1072
1097
|
});
|
|
1073
|
-
|
|
1098
|
+
safeWriteStderr(`Validation error:\n${issues.join('\n')}\n`);
|
|
1074
1099
|
}
|
|
1075
1100
|
else if (error instanceof ApiError) {
|
|
1076
|
-
|
|
1101
|
+
safeWriteStderr(`API error (HTTP ${error.statusCode}): ${error.responseBody || error.message}\n`);
|
|
1077
1102
|
}
|
|
1078
1103
|
else if (error instanceof Error) {
|
|
1079
|
-
|
|
1104
|
+
safeWriteStderr(`Error: ${error.message}\n`);
|
|
1080
1105
|
}
|
|
1081
1106
|
else {
|
|
1082
|
-
|
|
1107
|
+
safeWriteStderr(`Error: ${String(error)}\n`);
|
|
1083
1108
|
}
|
|
1084
1109
|
}
|
|
1085
|
-
function printAgentCliError(command, error) {
|
|
1086
|
-
|
|
1110
|
+
function printAgentCliError(command, error, mode = 'agent') {
|
|
1111
|
+
const output = createCliOutput({
|
|
1112
|
+
command: command || 'unknown',
|
|
1113
|
+
mode,
|
|
1114
|
+
});
|
|
1115
|
+
output.writeError(error);
|
|
1087
1116
|
}
|
|
1088
1117
|
export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
1089
1118
|
const { command, flags } = parsed;
|
|
@@ -1114,7 +1143,7 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1114
1143
|
env: process.env,
|
|
1115
1144
|
});
|
|
1116
1145
|
if (mode === 'agent' || mode === 'agent-stream') {
|
|
1117
|
-
printAgentCliError(command, error);
|
|
1146
|
+
printAgentCliError(command, error, mode);
|
|
1118
1147
|
}
|
|
1119
1148
|
else {
|
|
1120
1149
|
printCliError(error);
|
|
@@ -1129,12 +1158,6 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1129
1158
|
if (!updateCheckDisabled) {
|
|
1130
1159
|
void startUpdateCheck();
|
|
1131
1160
|
}
|
|
1132
|
-
if (definition.validateConfigBeforeRun) {
|
|
1133
|
-
validateConfig();
|
|
1134
|
-
}
|
|
1135
|
-
if (definition.resetApiRequestCountBeforeRun) {
|
|
1136
|
-
resetApiRequestCount();
|
|
1137
|
-
}
|
|
1138
1161
|
const context = createCommandContext(parsed, {
|
|
1139
1162
|
commandName: definition.name,
|
|
1140
1163
|
commandFlags,
|
|
@@ -1148,6 +1171,43 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1148
1171
|
env: process.env,
|
|
1149
1172
|
}),
|
|
1150
1173
|
});
|
|
1174
|
+
setActiveContext(context);
|
|
1175
|
+
if (definition.validateConfigBeforeRun) {
|
|
1176
|
+
try {
|
|
1177
|
+
validateConfig();
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
if (!telemetryDisabled && definition.telemetry === 'result-and-error') {
|
|
1181
|
+
void sendCliUsageTelemetry({
|
|
1182
|
+
apiKey: resolveApiKey(),
|
|
1183
|
+
command: definition.name,
|
|
1184
|
+
flags: commandFlags,
|
|
1185
|
+
status: 'error',
|
|
1186
|
+
errorType: classifyCliError(error),
|
|
1187
|
+
exitCode: exitCodeForError(error),
|
|
1188
|
+
durationMs: Date.now() - startedAt,
|
|
1189
|
+
apiRequestCount: getApiRequestCount(),
|
|
1190
|
+
adoption: buildSearchAdoptionTelemetry({
|
|
1191
|
+
command: definition.name,
|
|
1192
|
+
positional: parsed.positional,
|
|
1193
|
+
flags: commandFlags,
|
|
1194
|
+
outputMode: context.outputMode,
|
|
1195
|
+
recorded: context.getTelemetryMeta(),
|
|
1196
|
+
skillVersion: process.env.MODA_SKILL_VERSION,
|
|
1197
|
+
}),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
if (context.outputMode === 'agent' || context.outputMode === 'agent-stream' || context.outputMode === 'json') {
|
|
1201
|
+
return finishRun(context, { kind: 'error', error });
|
|
1202
|
+
}
|
|
1203
|
+
printCliError(error);
|
|
1204
|
+
context.output.close();
|
|
1205
|
+
return exitCodeForError(error);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (definition.resetApiRequestCountBeforeRun) {
|
|
1209
|
+
resetApiRequestCount();
|
|
1210
|
+
}
|
|
1151
1211
|
try {
|
|
1152
1212
|
const result = await definition.handler(context);
|
|
1153
1213
|
if (!telemetryDisabled && definition.telemetry !== 'none') {
|
|
@@ -1172,7 +1232,7 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1172
1232
|
if (!updateCheckDisabled) {
|
|
1173
1233
|
printUpdateNotice();
|
|
1174
1234
|
}
|
|
1175
|
-
return result.exitCode;
|
|
1235
|
+
return finishRun(context, { kind: 'completed', exitCode: result.exitCode });
|
|
1176
1236
|
}
|
|
1177
1237
|
catch (error) {
|
|
1178
1238
|
if (!telemetryDisabled && definition.telemetry === 'result-and-error') {
|
|
@@ -1182,7 +1242,7 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1182
1242
|
flags: commandFlags,
|
|
1183
1243
|
status: 'error',
|
|
1184
1244
|
errorType: classifyCliError(error),
|
|
1185
|
-
exitCode:
|
|
1245
|
+
exitCode: exitCodeForError(error),
|
|
1186
1246
|
durationMs: Date.now() - startedAt,
|
|
1187
1247
|
apiRequestCount: getApiRequestCount(),
|
|
1188
1248
|
adoption: buildSearchAdoptionTelemetry({
|
|
@@ -1195,8 +1255,8 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1195
1255
|
}),
|
|
1196
1256
|
});
|
|
1197
1257
|
}
|
|
1198
|
-
if (context.outputMode === 'agent' || context.outputMode === 'agent-stream') {
|
|
1199
|
-
|
|
1258
|
+
if (context.outputMode === 'agent' || context.outputMode === 'agent-stream' || context.outputMode === 'json') {
|
|
1259
|
+
return finishRun(context, { kind: 'error', error });
|
|
1200
1260
|
}
|
|
1201
1261
|
else {
|
|
1202
1262
|
printCliError(error);
|
|
@@ -1204,9 +1264,34 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1204
1264
|
if (!updateCheckDisabled) {
|
|
1205
1265
|
printUpdateNotice();
|
|
1206
1266
|
}
|
|
1207
|
-
|
|
1267
|
+
context.output.close();
|
|
1268
|
+
return exitCodeForError(error);
|
|
1208
1269
|
}
|
|
1209
1270
|
}
|
|
1271
|
+
let activeContext;
|
|
1272
|
+
let signalCount = 0;
|
|
1273
|
+
function setActiveContext(context) {
|
|
1274
|
+
activeContext = context;
|
|
1275
|
+
}
|
|
1276
|
+
function installSignalHandlers() {
|
|
1277
|
+
const handleSignal = (signal) => {
|
|
1278
|
+
signalCount += 1;
|
|
1279
|
+
if (signalCount > 1) {
|
|
1280
|
+
process.exit(EXIT_CANCELLED);
|
|
1281
|
+
}
|
|
1282
|
+
const reason = `Command cancelled by ${signal}.`;
|
|
1283
|
+
if (activeContext) {
|
|
1284
|
+
void finishRun(activeContext, { kind: 'cancelled', reason }).finally(() => {
|
|
1285
|
+
process.exit(EXIT_CANCELLED);
|
|
1286
|
+
});
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
printCliError(new CliCancellationError(reason));
|
|
1290
|
+
process.exit(EXIT_CANCELLED);
|
|
1291
|
+
};
|
|
1292
|
+
process.once('SIGINT', handleSignal);
|
|
1293
|
+
process.once('SIGTERM', handleSignal);
|
|
1294
|
+
}
|
|
1210
1295
|
async function main() {
|
|
1211
1296
|
// Set exitCode (don't process.exit) so any in-flight telemetry request
|
|
1212
1297
|
// can resolve before Node drains its event loop and exits naturally.
|
|
@@ -1218,6 +1303,7 @@ import { realpathSync } from 'fs';
|
|
|
1218
1303
|
const isMain = process.argv[1] &&
|
|
1219
1304
|
fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
|
|
1220
1305
|
if (isMain) {
|
|
1306
|
+
installSignalHandlers();
|
|
1221
1307
|
main().catch((error) => {
|
|
1222
1308
|
printCliError(error);
|
|
1223
1309
|
process.exitCode = 1;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { DEFAULT_INGEST_URL, PROD_BASE_URL, getLegacyConfigPath, loadLegacyCliConfig, resolveApiKeyFromActiveProfile, resolveBaseUrl, resolveIngestUrl as resolveProfileIngestUrl, resolveTenantIdFromActiveProfile, syncLegacyConfigToProfile, writeLegacyCliConfig, } from './profiles.js';
|
|
3
|
+
import { CliAuthError } from './errors.js';
|
|
3
4
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
5
|
export const VERSION = pkg.version;
|
|
5
6
|
// Canonical host is moda.dev. The www host 308-redirects to the apex, and
|
|
@@ -65,9 +66,10 @@ export function resolveModaBaseUrl(options = {}) {
|
|
|
65
66
|
}
|
|
66
67
|
export function validateConfig() {
|
|
67
68
|
if (!resolveApiKey()) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
throw new CliAuthError('No Moda API key found. Set MODA_API_KEY or run "moda init".', {
|
|
70
|
+
reason: 'no_stored_credentials',
|
|
71
|
+
login_command: ['moda', 'init'],
|
|
72
|
+
}, 'Get your API key from https://moda.dev/settings');
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
// Kept for callers/tests that need the legacy path during migration.
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,4 +2,36 @@ export declare class CliInputError extends Error {
|
|
|
2
2
|
readonly usage?: string | undefined;
|
|
3
3
|
constructor(message: string, usage?: string | undefined);
|
|
4
4
|
}
|
|
5
|
+
export interface AgentInputChoice {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
resume_flags: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface AgentInputRequest {
|
|
11
|
+
id: string;
|
|
12
|
+
question: string;
|
|
13
|
+
kind: 'select' | 'confirm' | 'text';
|
|
14
|
+
choices?: AgentInputChoice[];
|
|
15
|
+
resume_command?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface AgentAuthRequest {
|
|
18
|
+
reason: 'no_stored_credentials' | 'token_expired' | 'token_revoked' | 'renew_failed';
|
|
19
|
+
login_command: string[];
|
|
20
|
+
resume_command?: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare class CliInputRequiredError extends Error {
|
|
23
|
+
readonly input_request: AgentInputRequest;
|
|
24
|
+
constructor(message: string, input_request: AgentInputRequest, usage?: string);
|
|
25
|
+
readonly usage?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class CliAuthError extends Error {
|
|
28
|
+
readonly auth: AgentAuthRequest;
|
|
29
|
+
constructor(message: string, auth: AgentAuthRequest, usage?: string);
|
|
30
|
+
readonly usage?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class CliCancellationError extends Error {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
export declare function isNetworkLikeError(error: unknown): boolean;
|
|
36
|
+
export declare function isApiLikeError(error: unknown): boolean;
|
|
5
37
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
CHANGED
|
@@ -6,4 +6,44 @@ export class CliInputError extends Error {
|
|
|
6
6
|
this.name = 'CliInputError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
export class CliInputRequiredError extends Error {
|
|
10
|
+
input_request;
|
|
11
|
+
constructor(message, input_request, usage) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.input_request = input_request;
|
|
14
|
+
this.name = 'CliInputRequiredError';
|
|
15
|
+
if (usage)
|
|
16
|
+
this.usage = usage;
|
|
17
|
+
}
|
|
18
|
+
usage;
|
|
19
|
+
}
|
|
20
|
+
export class CliAuthError extends Error {
|
|
21
|
+
auth;
|
|
22
|
+
constructor(message, auth, usage) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.auth = auth;
|
|
25
|
+
this.name = 'CliAuthError';
|
|
26
|
+
if (usage)
|
|
27
|
+
this.usage = usage;
|
|
28
|
+
}
|
|
29
|
+
usage;
|
|
30
|
+
}
|
|
31
|
+
export class CliCancellationError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'CliCancellationError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function isNetworkLikeError(error) {
|
|
38
|
+
return error instanceof Error &&
|
|
39
|
+
(error.message.startsWith('Network error:') ||
|
|
40
|
+
error.message.startsWith('Request timed out') ||
|
|
41
|
+
error.name === 'AbortError' ||
|
|
42
|
+
/(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|socket hang up)/i.test(error.message));
|
|
43
|
+
}
|
|
44
|
+
export function isApiLikeError(error) {
|
|
45
|
+
return error instanceof Error &&
|
|
46
|
+
(error.name === 'ApiError' ||
|
|
47
|
+
('statusCode' in error && typeof error.statusCode === 'number'));
|
|
48
|
+
}
|
|
9
49
|
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CommandContext } from './command-context.js';
|
|
2
|
+
export type RunOutcome = {
|
|
3
|
+
kind: 'completed';
|
|
4
|
+
exitCode?: number;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'error';
|
|
7
|
+
error: unknown;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'cancelled';
|
|
10
|
+
reason?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function finishRun(context: CommandContext, outcome: RunOutcome): Promise<number>;
|
|
13
|
+
export declare function exitCodeForError(error: unknown): number;
|
|
14
|
+
//# sourceMappingURL=exit-funnel.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CliAuthError, CliCancellationError, CliInputRequiredError, } from './errors.js';
|
|
2
|
+
import { EXIT_AUTH_REQUIRED, EXIT_CANCELLED, EXIT_ERROR, EXIT_INPUT_REQUIRED, EXIT_OK, wasOutputCancelled, } from './output.js';
|
|
3
|
+
const finishedContexts = new WeakSet();
|
|
4
|
+
export async function finishRun(context, outcome) {
|
|
5
|
+
if (finishedContexts.has(context)) {
|
|
6
|
+
return exitCodeForOutcome(outcome);
|
|
7
|
+
}
|
|
8
|
+
finishedContexts.add(context);
|
|
9
|
+
if (outcome.kind === 'completed') {
|
|
10
|
+
// Guarantee the stream contract structurally: exactly one terminal event
|
|
11
|
+
// ends every stream. A handler that returned without writing one gets a
|
|
12
|
+
// synthetic `completed` (a no-op in non-stream modes and when a terminal
|
|
13
|
+
// event was already emitted).
|
|
14
|
+
if (!context.output.hasTerminalOutput()) {
|
|
15
|
+
context.output.writeEvent({ event: 'completed', status: 'ok' });
|
|
16
|
+
}
|
|
17
|
+
context.output.close();
|
|
18
|
+
if (wasOutputCancelled())
|
|
19
|
+
return EXIT_CANCELLED;
|
|
20
|
+
return outcome.exitCode ?? EXIT_OK;
|
|
21
|
+
}
|
|
22
|
+
if (outcome.kind === 'cancelled') {
|
|
23
|
+
context.output.writeCancelled(outcome.reason ?? 'Command cancelled.');
|
|
24
|
+
context.output.close();
|
|
25
|
+
return EXIT_CANCELLED;
|
|
26
|
+
}
|
|
27
|
+
context.output.writeError(outcome.error);
|
|
28
|
+
context.output.close();
|
|
29
|
+
if (wasOutputCancelled())
|
|
30
|
+
return EXIT_CANCELLED;
|
|
31
|
+
return exitCodeForError(outcome.error);
|
|
32
|
+
}
|
|
33
|
+
export function exitCodeForError(error) {
|
|
34
|
+
if (error instanceof CliAuthError)
|
|
35
|
+
return EXIT_AUTH_REQUIRED;
|
|
36
|
+
if (error instanceof CliInputRequiredError)
|
|
37
|
+
return EXIT_INPUT_REQUIRED;
|
|
38
|
+
if (error instanceof CliCancellationError)
|
|
39
|
+
return EXIT_CANCELLED;
|
|
40
|
+
return EXIT_ERROR;
|
|
41
|
+
}
|
|
42
|
+
function exitCodeForOutcome(outcome) {
|
|
43
|
+
if (outcome.kind === 'completed')
|
|
44
|
+
return outcome.exitCode ?? EXIT_OK;
|
|
45
|
+
if (outcome.kind === 'cancelled')
|
|
46
|
+
return EXIT_CANCELLED;
|
|
47
|
+
return exitCodeForError(outcome.error);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=exit-funnel.js.map
|
package/dist/harness.js
CHANGED
|
@@ -7,7 +7,7 @@ import { confirm, select } from '@inquirer/prompts';
|
|
|
7
7
|
import { callControlAPI } from './api-client.js';
|
|
8
8
|
import { AnalystTui, shouldUseAnalystTui } from './analyst-tui.js';
|
|
9
9
|
import { resolveApiKey, resolveTenantId } from './config.js';
|
|
10
|
-
import { CliInputError } from './errors.js';
|
|
10
|
+
import { CliInputError, CliInputRequiredError } from './errors.js';
|
|
11
11
|
import { getActiveProfile } from './profiles.js';
|
|
12
12
|
import { approveHarnessReport, createHarnessReport, graphForReport, hashHarnessReport, hashStableJson as hashHarnessReportStableJson, HARNESS_REPORT_PATH, loadHarnessReportForSync, normalizeHarnessReport, readHarnessReport, resolveHarnessReportPath, validateHarnessReport, writeHarnessReportArtifacts, } from './harness-report.js';
|
|
13
13
|
import { renderHarnessAnalystChoice, renderHarnessAnalystLaunch, renderHarnessAnalystPreflight, renderHarnessAnalyzeHumanResult, renderHarnessProgress, } from './tui.js';
|
|
@@ -1623,7 +1623,7 @@ function buildCommandScanPlan(context, rootDir) {
|
|
|
1623
1623
|
const rel = normalizePath(relative(context.cwd, rootDir)) || '.';
|
|
1624
1624
|
const scope = explicitPath && rel !== '.' ? 'narrow' : 'broad';
|
|
1625
1625
|
const machineReadable = context.outputMode !== 'human';
|
|
1626
|
-
const approvalRequired = scope === 'broad' && !context.yes
|
|
1626
|
+
const approvalRequired = context.positional === 'analyze' && scope === 'broad' && !context.yes;
|
|
1627
1627
|
return {
|
|
1628
1628
|
scope,
|
|
1629
1629
|
root: rel,
|
|
@@ -1632,10 +1632,10 @@ function buildCommandScanPlan(context, rootDir) {
|
|
|
1632
1632
|
granted: !approvalRequired,
|
|
1633
1633
|
reason: approvalRequired
|
|
1634
1634
|
? 'broad scan requires confirmation'
|
|
1635
|
-
:
|
|
1636
|
-
? '
|
|
1637
|
-
:
|
|
1638
|
-
? '
|
|
1635
|
+
: context.yes
|
|
1636
|
+
? '--yes'
|
|
1637
|
+
: machineReadable
|
|
1638
|
+
? 'machine-readable output'
|
|
1639
1639
|
: scope === 'narrow'
|
|
1640
1640
|
? 'narrow path'
|
|
1641
1641
|
: 'non-interactive terminal',
|
|
@@ -1646,6 +1646,18 @@ function buildCommandScanPlan(context, rootDir) {
|
|
|
1646
1646
|
async function approveScanIfNeeded(scanPlan, options = {}) {
|
|
1647
1647
|
if (!scanPlan.approval.required)
|
|
1648
1648
|
return;
|
|
1649
|
+
if (options.context && (options.context.outputMode !== 'human' || process.stdin.isTTY !== true)) {
|
|
1650
|
+
throw new CliInputRequiredError(options.message ?? 'Scan this project to build a Moda harness manifest?', {
|
|
1651
|
+
id: 'harness_scan_approval',
|
|
1652
|
+
question: options.message ?? 'Scan this project to build a Moda harness manifest?',
|
|
1653
|
+
kind: 'confirm',
|
|
1654
|
+
choices: [
|
|
1655
|
+
{ value: 'yes', label: 'Approve broad scan', resume_flags: ['--yes'] },
|
|
1656
|
+
{ value: 'no', label: 'Decline broad scan', resume_flags: ['--path', scanPlan.root === '.' ? './src' : scanPlan.root] },
|
|
1657
|
+
],
|
|
1658
|
+
resume_command: ['moda', 'harness', 'analyze', '--yes'],
|
|
1659
|
+
}, 'Re-run with `--yes` to approve the broad local read plan, or pass --path=<dir> for a narrower scan.');
|
|
1660
|
+
}
|
|
1649
1661
|
if (options.context?.outputMode === 'human') {
|
|
1650
1662
|
writeHumanBlock(options.context, renderHarnessAnalystPreflight({
|
|
1651
1663
|
root: scanPlan.root,
|