@moda-ai/cli 1.55.10 → 1.55.11
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-n0ch2j90.js +459 -0
- package/dist/cli-11fevr8y.js +811 -0
- package/dist/{cli-0qnghzg4.js → cli-429zt7g4.js} +1 -65
- package/dist/cli-4qe6n8am.js +345 -0
- package/dist/cli-97fnztpy.js +140 -0
- package/dist/{cli-d3c95kks.js → cli-fq7kk77e.js} +18 -1733
- package/dist/cli-fwec3m42.js +946 -0
- package/dist/cli-g7ferymy.js +496 -0
- package/dist/cli-k5qqj2bp.js +70 -0
- package/dist/cli-m16taf4z.js +30 -0
- package/dist/{cli-v27q5dvh.js → cli-m857n028.js} +189 -970
- package/dist/{cli-7s48d6ty.js → cli-n9gexgma.js} +2 -2
- package/dist/{cli-4f0p8erk.js → cli-t4jjhn3g.js} +5 -7
- package/dist/cli-tmqd1f0m.js +159 -0
- package/dist/cli.js +1409 -6991
- package/dist/doctor-spvy2zb3.js +23 -0
- package/dist/fix-ax6bbg37.js +1444 -0
- package/dist/{harness-github-actions-wpkr7jjr.js → harness-github-actions-mtksqp3c.js} +7 -2
- package/dist/{harness-0tnqw849.js → harness-jbqpbp6n.js} +7 -2
- package/dist/{index-jqexf7kn.js → index-2d0q94sv.js} +31 -20
- package/dist/production-intelligence-1tf98n79.js +1802 -0
- package/dist/profile-commands-f6599bcx.js +481 -0
- package/dist/prompt-source-mcp.js +7 -2
- package/dist/prompts-y3xd9heh.js +40 -0
- package/dist/{provision-5pkn25hh.js → provision-cjpsswa9.js} +7 -5
- package/dist/registry-1dvxsekd.js +1201 -0
- package/dist/skills-1k0hbkaf.js +19 -0
- package/dist/sync-by81xqk4.js +54 -0
- package/package.json +6 -6
- package/skills/integration/index.json +2 -2
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAuthSessionValid,
|
|
3
|
+
loadAuthSession
|
|
4
|
+
} from "./cli-t4jjhn3g.js";
|
|
5
|
+
import {
|
|
6
|
+
inspectPromptManifest
|
|
7
|
+
} from "./cli-fq7kk77e.js";
|
|
8
|
+
import {
|
|
9
|
+
readHarnessSyncState,
|
|
10
|
+
renderDoctorHuman,
|
|
11
|
+
renderStatusHuman,
|
|
12
|
+
scanHarness
|
|
13
|
+
} from "./cli-m857n028.js";
|
|
14
|
+
import {
|
|
15
|
+
callDataAPI
|
|
16
|
+
} from "./cli-tmqd1f0m.js";
|
|
17
|
+
import {
|
|
18
|
+
ensureSecretFilePermissions,
|
|
19
|
+
getActiveProfile,
|
|
20
|
+
getModaPaths,
|
|
21
|
+
isLoopbackBaseUrl,
|
|
22
|
+
loadProfileConfig,
|
|
23
|
+
recordApiRequest,
|
|
24
|
+
resolveApiKey,
|
|
25
|
+
resolveIngestUrl,
|
|
26
|
+
resolveModaBaseUrl,
|
|
27
|
+
resolveSecretReference,
|
|
28
|
+
resolveTenantId,
|
|
29
|
+
secretFilePathFromRef
|
|
30
|
+
} from "./cli-429zt7g4.js";
|
|
31
|
+
|
|
32
|
+
// src/doctor.ts
|
|
33
|
+
import { createHash } from "node:crypto";
|
|
34
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
async function runDoctorCommand(context) {
|
|
37
|
+
const report = await buildDoctorReport(context, {
|
|
38
|
+
offline: context.flags.offline === "true"
|
|
39
|
+
});
|
|
40
|
+
if (context.outputMode === "human") {
|
|
41
|
+
if (!context.quiet)
|
|
42
|
+
console.log(renderDoctorHuman(report));
|
|
43
|
+
return { exitCode: 0 };
|
|
44
|
+
}
|
|
45
|
+
context.output.writeData(report, {
|
|
46
|
+
status: report.summary.fail > 0 ? "error" : "success",
|
|
47
|
+
...doctorAgentWriteOptions(report, "doctor")
|
|
48
|
+
});
|
|
49
|
+
return { exitCode: 0 };
|
|
50
|
+
}
|
|
51
|
+
async function runStatusCommand(context) {
|
|
52
|
+
const report = await buildDoctorReport(context, {
|
|
53
|
+
offline: context.flags.offline === "true" || context.flags.online !== "true"
|
|
54
|
+
});
|
|
55
|
+
const topBroken = firstBrokenCheck(report.checks);
|
|
56
|
+
const statusPayload = {
|
|
57
|
+
schemaVersion: "status.v0.1",
|
|
58
|
+
generatedAt: report.generatedAt,
|
|
59
|
+
status: report.summary.status,
|
|
60
|
+
activeProfile: report.profile.active,
|
|
61
|
+
auth: {
|
|
62
|
+
apiKey: resolveApiKey() ? "configured" : "missing"
|
|
63
|
+
},
|
|
64
|
+
workspace: {
|
|
65
|
+
tenantId: resolveTenantId() ?? null,
|
|
66
|
+
baseUrl: resolveModaBaseUrl(),
|
|
67
|
+
ingestUrl: resolveIngestUrl()
|
|
68
|
+
},
|
|
69
|
+
dataFlow: checkStatus(report.checks, "cloud_data_flow"),
|
|
70
|
+
harness: checkStatus(report.checks, "harness_manifest_current"),
|
|
71
|
+
harnessSync: checkStatus(report.checks, "harness_sync_state_present"),
|
|
72
|
+
harnessReport: "skipped",
|
|
73
|
+
shadowSpark: "skipped",
|
|
74
|
+
prompts: checkStatus(report.checks, "prompts_discovered"),
|
|
75
|
+
skills: checkStatus(report.checks, "skills_discovered"),
|
|
76
|
+
topBrokenCheck: topBroken ? {
|
|
77
|
+
id: topBroken.id,
|
|
78
|
+
status: topBroken.status,
|
|
79
|
+
severity: topBroken.severity,
|
|
80
|
+
nextCommand: topBroken.nextCommand
|
|
81
|
+
} : null,
|
|
82
|
+
nextCommands: report.nextCommands.slice(0, 3)
|
|
83
|
+
};
|
|
84
|
+
if (context.outputMode === "human") {
|
|
85
|
+
if (!context.quiet)
|
|
86
|
+
console.log(renderStatusHuman(statusPayload));
|
|
87
|
+
return { exitCode: 0 };
|
|
88
|
+
}
|
|
89
|
+
context.output.writeData(statusPayload, {
|
|
90
|
+
status: report.summary.fail > 0 ? "error" : "success",
|
|
91
|
+
...doctorAgentWriteOptions(report, "status")
|
|
92
|
+
});
|
|
93
|
+
return { exitCode: 0 };
|
|
94
|
+
}
|
|
95
|
+
async function buildDoctorReport(context, options) {
|
|
96
|
+
const checks = [];
|
|
97
|
+
const profileConfig = loadProfileConfig({ env: context.env });
|
|
98
|
+
const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
|
|
99
|
+
const paths = getModaPaths({ env: context.env });
|
|
100
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
101
|
+
const apiKey = resolveApiKey(profileOptions);
|
|
102
|
+
const tenantId = resolveTenantId(profileOptions);
|
|
103
|
+
const baseUrl = resolveModaBaseUrl(profileOptions);
|
|
104
|
+
const ingestUrl = resolveIngestUrl(profileOptions);
|
|
105
|
+
checks.push({
|
|
106
|
+
id: "active_profile_resolves",
|
|
107
|
+
group: "config",
|
|
108
|
+
title: "Active profile resolves",
|
|
109
|
+
status: activeProfile ? "pass" : "fail",
|
|
110
|
+
severity: activeProfile ? "info" : "critical",
|
|
111
|
+
evidence: [
|
|
112
|
+
{ label: "active_profile", value: profileConfig.active_profile },
|
|
113
|
+
{ label: "environment", value: activeProfile.environment }
|
|
114
|
+
],
|
|
115
|
+
remediation: activeProfile ? undefined : "Create or select a Moda profile.",
|
|
116
|
+
nextCommand: activeProfile ? undefined : "moda profiles list"
|
|
117
|
+
});
|
|
118
|
+
checks.push({
|
|
119
|
+
id: "api_key_resolves",
|
|
120
|
+
group: "config",
|
|
121
|
+
title: "API key resolves",
|
|
122
|
+
status: apiKey ? "pass" : "fail",
|
|
123
|
+
severity: apiKey ? "info" : "critical",
|
|
124
|
+
evidence: [{ label: "source", value: apiKey ? "environment/profile/config" : "none" }],
|
|
125
|
+
remediation: apiKey ? undefined : "Authenticate and create a tenant API key.",
|
|
126
|
+
nextCommand: apiKey ? undefined : "moda init"
|
|
127
|
+
});
|
|
128
|
+
checks.push({
|
|
129
|
+
id: "tenant_resolves",
|
|
130
|
+
group: "config",
|
|
131
|
+
title: "Tenant/workspace resolves",
|
|
132
|
+
status: tenantId ? "pass" : "warning",
|
|
133
|
+
severity: tenantId ? "info" : "warning",
|
|
134
|
+
evidence: [{ label: "tenant_id", value: tenantId ? "configured" : "missing" }],
|
|
135
|
+
remediation: tenantId ? undefined : "Select a tenant during init or set MODA_TENANT_ID.",
|
|
136
|
+
nextCommand: tenantId ? undefined : "moda init"
|
|
137
|
+
});
|
|
138
|
+
checks.push(authTokenCheck(context, Boolean(apiKey)));
|
|
139
|
+
checks.push(configPermissionCheck(paths.legacyConfigPath));
|
|
140
|
+
if (activeProfile.api_key_ref) {
|
|
141
|
+
const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
|
|
142
|
+
if (secretPath)
|
|
143
|
+
checks.push(secretPermissionCheck(secretPath));
|
|
144
|
+
checks.push({
|
|
145
|
+
id: "profile_api_key_ref_resolves",
|
|
146
|
+
group: "config",
|
|
147
|
+
title: "Profile API key reference resolves",
|
|
148
|
+
status: resolveSecretReference(activeProfile.api_key_ref) ? "pass" : "warning",
|
|
149
|
+
severity: "warning",
|
|
150
|
+
evidence: [{ label: "api_key_ref", value: activeProfile.api_key_ref }],
|
|
151
|
+
remediation: "Create or update the profile API key reference.",
|
|
152
|
+
nextCommand: "moda profiles create"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
|
|
156
|
+
checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
|
|
157
|
+
checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
|
|
158
|
+
const manifestPath = join(context.cwd, ".moda", "harness.json");
|
|
159
|
+
const manifestExists = existsSync(manifestPath);
|
|
160
|
+
checks.push({
|
|
161
|
+
id: "harness_manifest_exists",
|
|
162
|
+
group: "harness",
|
|
163
|
+
title: "Harness manifest exists",
|
|
164
|
+
status: manifestExists ? "pass" : "warning",
|
|
165
|
+
severity: manifestExists ? "info" : "warning",
|
|
166
|
+
evidence: [{ label: "path", value: ".moda/harness.json" }],
|
|
167
|
+
remediation: manifestExists ? undefined : "Run `moda harness scan --summary` to scan the project and write a harness manifest.",
|
|
168
|
+
nextCommand: manifestExists ? undefined : "moda harness scan --summary"
|
|
169
|
+
});
|
|
170
|
+
checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
|
|
171
|
+
checks.push(harnessSyncStateCheck(context.cwd));
|
|
172
|
+
checks.push(promptsDiscoveredCheck(context.cwd));
|
|
173
|
+
checks.push(discoveryCheck("skills_discovered", "harness", "Skills are discovered", context.cwd, [
|
|
174
|
+
"skills",
|
|
175
|
+
".claude/skills",
|
|
176
|
+
".cursor/rules"
|
|
177
|
+
], "moda skills pull"));
|
|
178
|
+
checks.push(discoveryCheck("evals_discovered", "harness", "Evals/tests are discovered", context.cwd, [
|
|
179
|
+
"evals",
|
|
180
|
+
"tests",
|
|
181
|
+
"__tests__"
|
|
182
|
+
]));
|
|
183
|
+
checks.push(validationEventPathCheck(ingestUrl));
|
|
184
|
+
if (options.offline || !apiKey) {
|
|
185
|
+
checks.push(...offlineCloudChecks(options.offline));
|
|
186
|
+
} else {
|
|
187
|
+
checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
|
|
188
|
+
}
|
|
189
|
+
const nextCommands = nextCommandsForChecks(checks);
|
|
190
|
+
const summary = summarizeChecks(checks);
|
|
191
|
+
return {
|
|
192
|
+
schemaVersion: "doctor.v0.1",
|
|
193
|
+
generatedAt: new Date().toISOString(),
|
|
194
|
+
profile: {
|
|
195
|
+
active: activeProfile.name,
|
|
196
|
+
requested: context.profile
|
|
197
|
+
},
|
|
198
|
+
summary,
|
|
199
|
+
checks,
|
|
200
|
+
nextCommands,
|
|
201
|
+
fixPlan: checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => ({
|
|
202
|
+
checkId: check.id,
|
|
203
|
+
command: check.nextCommand,
|
|
204
|
+
action: check.remediation ?? "Review this diagnostic."
|
|
205
|
+
}))
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function authTokenCheck(context, hasApiKey) {
|
|
209
|
+
const session = loadAuthSession({
|
|
210
|
+
profile: context.profile,
|
|
211
|
+
env: context.env,
|
|
212
|
+
includeExpired: true
|
|
213
|
+
});
|
|
214
|
+
const valid = isAuthSessionValid(session);
|
|
215
|
+
if (!valid && hasApiKey) {
|
|
216
|
+
return {
|
|
217
|
+
id: "auth_token_valid",
|
|
218
|
+
group: "config",
|
|
219
|
+
title: "Browser auth token is valid",
|
|
220
|
+
status: "skipped",
|
|
221
|
+
severity: "info",
|
|
222
|
+
evidence: [{ label: "reason", value: "api key configured" }]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
id: "auth_token_valid",
|
|
227
|
+
group: "config",
|
|
228
|
+
title: "Browser auth token is valid",
|
|
229
|
+
status: valid ? "pass" : "warning",
|
|
230
|
+
severity: valid ? "info" : "warning",
|
|
231
|
+
evidence: [
|
|
232
|
+
{ label: "token", value: session ? "present" : "missing" },
|
|
233
|
+
...session ? [{ label: "expires_at", value: new Date(session.expires_at * 1000).toISOString() }] : []
|
|
234
|
+
],
|
|
235
|
+
remediation: valid ? undefined : "Refresh browser authentication.",
|
|
236
|
+
nextCommand: valid ? undefined : "moda init"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function configPermissionCheck(path) {
|
|
240
|
+
if (!existsSync(path)) {
|
|
241
|
+
return {
|
|
242
|
+
id: "config_file_permissions",
|
|
243
|
+
group: "config",
|
|
244
|
+
title: "Config file permissions",
|
|
245
|
+
status: "skipped",
|
|
246
|
+
severity: "info",
|
|
247
|
+
evidence: [{ label: "path", value: path }]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const permission = ensureSecretFilePermissions(path);
|
|
251
|
+
return {
|
|
252
|
+
id: "config_file_permissions",
|
|
253
|
+
group: "config",
|
|
254
|
+
title: "Config file permissions",
|
|
255
|
+
status: permission.secure ? "pass" : "fail",
|
|
256
|
+
severity: permission.secure ? "info" : "critical",
|
|
257
|
+
evidence: [
|
|
258
|
+
{ label: "path", value: path },
|
|
259
|
+
{ label: "mode", value: permission.mode ?? "unknown" }
|
|
260
|
+
],
|
|
261
|
+
remediation: permission.secure ? undefined : "Restrict config file permissions to the current user."
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function secretPermissionCheck(path) {
|
|
265
|
+
const permission = ensureSecretFilePermissions(path);
|
|
266
|
+
return {
|
|
267
|
+
id: "secret_file_permissions",
|
|
268
|
+
group: "config",
|
|
269
|
+
title: "Secret file permissions",
|
|
270
|
+
status: permission.secure ? "pass" : "fail",
|
|
271
|
+
severity: permission.secure ? "info" : "critical",
|
|
272
|
+
evidence: [
|
|
273
|
+
{ label: "path", value: path },
|
|
274
|
+
{ label: "mode", value: permission.mode ?? "unknown" }
|
|
275
|
+
],
|
|
276
|
+
remediation: permission.secure ? undefined : "Restrict secret file permissions to the current user."
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function urlCheck(id, group, title, value) {
|
|
280
|
+
let valid = false;
|
|
281
|
+
try {
|
|
282
|
+
const parsed = new URL(value);
|
|
283
|
+
valid = parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
284
|
+
} catch {
|
|
285
|
+
valid = false;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
id,
|
|
289
|
+
group,
|
|
290
|
+
title,
|
|
291
|
+
status: valid ? "pass" : "fail",
|
|
292
|
+
severity: valid ? "info" : "critical",
|
|
293
|
+
evidence: [{ label: "url", value }],
|
|
294
|
+
remediation: valid ? undefined : "Configure a valid http(s) URL.",
|
|
295
|
+
nextCommand: valid ? undefined : "moda profiles create"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function localBaseUrlCheck(environment, baseUrl) {
|
|
299
|
+
if (environment !== "local") {
|
|
300
|
+
return {
|
|
301
|
+
id: "local_base_url_frontend",
|
|
302
|
+
group: "config",
|
|
303
|
+
title: "Local base URL targets the frontend",
|
|
304
|
+
status: "skipped",
|
|
305
|
+
severity: "info",
|
|
306
|
+
evidence: [{ label: "environment", value: environment }]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let port = null;
|
|
310
|
+
let loopback = false;
|
|
311
|
+
try {
|
|
312
|
+
const parsed = new URL(baseUrl);
|
|
313
|
+
loopback = isLoopbackBaseUrl(baseUrl);
|
|
314
|
+
port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
315
|
+
} catch {
|
|
316
|
+
return {
|
|
317
|
+
id: "local_base_url_frontend",
|
|
318
|
+
group: "config",
|
|
319
|
+
title: "Local base URL targets the frontend",
|
|
320
|
+
status: "fail",
|
|
321
|
+
severity: "critical",
|
|
322
|
+
evidence: [{ label: "base_url", value: baseUrl }],
|
|
323
|
+
remediation: "Set the local profile base URL to the frontend origin (http://localhost:3000).",
|
|
324
|
+
nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000"
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const looksLikeBackend = loopback && (port === "3001" || port === "3002");
|
|
328
|
+
if (looksLikeBackend) {
|
|
329
|
+
return {
|
|
330
|
+
id: "local_base_url_frontend",
|
|
331
|
+
group: "config",
|
|
332
|
+
title: "Local base URL targets the frontend",
|
|
333
|
+
status: "warning",
|
|
334
|
+
severity: "warning",
|
|
335
|
+
evidence: [
|
|
336
|
+
{ label: "base_url", value: baseUrl },
|
|
337
|
+
{ label: "port", value: port ?? "unknown" }
|
|
338
|
+
],
|
|
339
|
+
remediation: "Local CLI auth and control APIs are served by the frontend. " + "Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.",
|
|
340
|
+
nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000 --use"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (!loopback) {
|
|
344
|
+
return {
|
|
345
|
+
id: "local_base_url_frontend",
|
|
346
|
+
group: "config",
|
|
347
|
+
title: "Local base URL targets the frontend",
|
|
348
|
+
status: "warning",
|
|
349
|
+
severity: "warning",
|
|
350
|
+
evidence: [{ label: "base_url", value: baseUrl }],
|
|
351
|
+
remediation: "Local profile should use a loopback frontend URL (http://localhost:3000). " + "Activate with `moda profiles use local` or set MODA_PROFILE=local.",
|
|
352
|
+
nextCommand: "moda profiles use local"
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
id: "local_base_url_frontend",
|
|
357
|
+
group: "config",
|
|
358
|
+
title: "Local base URL targets the frontend",
|
|
359
|
+
status: "pass",
|
|
360
|
+
severity: "info",
|
|
361
|
+
evidence: [{ label: "base_url", value: baseUrl }]
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
|
|
365
|
+
if (!manifestExists) {
|
|
366
|
+
return {
|
|
367
|
+
id: "harness_manifest_current",
|
|
368
|
+
group: "harness",
|
|
369
|
+
title: "Harness manifest matches scanner output",
|
|
370
|
+
status: "skipped",
|
|
371
|
+
severity: "info",
|
|
372
|
+
evidence: [{ label: "reason", value: "manifest missing" }],
|
|
373
|
+
nextCommand: "moda harness scan --summary"
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const current = scanHarness({
|
|
378
|
+
rootDir: cwd,
|
|
379
|
+
scannerVersion: "doctor",
|
|
380
|
+
tenantId,
|
|
381
|
+
tenantSource: tenantId ? "env_or_config" : undefined
|
|
382
|
+
});
|
|
383
|
+
const saved = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
384
|
+
const savedHash = stableGraphHash(saved);
|
|
385
|
+
const currentHash = stableGraphHash(current);
|
|
386
|
+
const matches = savedHash === currentHash;
|
|
387
|
+
return {
|
|
388
|
+
id: "harness_manifest_current",
|
|
389
|
+
group: "harness",
|
|
390
|
+
title: "Harness manifest matches scanner output",
|
|
391
|
+
status: matches ? "pass" : "warning",
|
|
392
|
+
severity: matches ? "info" : "warning",
|
|
393
|
+
evidence: [
|
|
394
|
+
{ label: "saved_hash", value: savedHash.slice(0, 12) },
|
|
395
|
+
{ label: "current_hash", value: currentHash.slice(0, 12) },
|
|
396
|
+
...matches ? [] : [{ label: "diff", value: "Manifest hash differs from fresh scanner output." }]
|
|
397
|
+
],
|
|
398
|
+
remediation: matches ? undefined : "Refresh the local harness manifest.",
|
|
399
|
+
nextCommand: matches ? undefined : "moda harness scan --summary"
|
|
400
|
+
};
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return {
|
|
403
|
+
id: "harness_manifest_current",
|
|
404
|
+
group: "harness",
|
|
405
|
+
title: "Harness manifest matches scanner output",
|
|
406
|
+
status: "fail",
|
|
407
|
+
severity: "critical",
|
|
408
|
+
evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
|
|
409
|
+
remediation: "Regenerate the harness manifest.",
|
|
410
|
+
nextCommand: "moda harness scan --summary"
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function harnessSyncStateCheck(cwd) {
|
|
415
|
+
const state = readHarnessSyncState(cwd);
|
|
416
|
+
if (!state) {
|
|
417
|
+
return {
|
|
418
|
+
id: "harness_sync_state_present",
|
|
419
|
+
group: "harness",
|
|
420
|
+
title: "Harness sync state exists",
|
|
421
|
+
status: "warning",
|
|
422
|
+
severity: "warning",
|
|
423
|
+
evidence: [{ label: "path", value: ".moda/sync-state.json" }],
|
|
424
|
+
remediation: "Sync the harness graph so Moda can correlate local state with cloud state.",
|
|
425
|
+
nextCommand: "moda harness sync --dry-run"
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
id: "harness_sync_state_present",
|
|
430
|
+
group: "harness",
|
|
431
|
+
title: "Harness sync state exists",
|
|
432
|
+
status: "pass",
|
|
433
|
+
severity: "info",
|
|
434
|
+
evidence: [
|
|
435
|
+
{ label: "updated_at", value: state.updatedAt },
|
|
436
|
+
{ label: "profile", value: state.profile },
|
|
437
|
+
{ label: "harness_id", value: state.harnessId },
|
|
438
|
+
{ label: "remote_version_id", value: state.remote.versionId || "none" }
|
|
439
|
+
]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function promptsDiscoveredCheck(cwd) {
|
|
443
|
+
const base = { id: "prompts_discovered", group: "harness", title: "Prompts are discovered" };
|
|
444
|
+
if (!existsSync(join(cwd, ".moda", "prompts.yml"))) {
|
|
445
|
+
return discoveryCheck(base.id, base.group, base.title, cwd, ["prompts", ".moda/prompts.yml"], "moda prompts init");
|
|
446
|
+
}
|
|
447
|
+
let inspection;
|
|
448
|
+
try {
|
|
449
|
+
inspection = inspectPromptManifest(cwd);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
return {
|
|
452
|
+
...base,
|
|
453
|
+
status: "fail",
|
|
454
|
+
severity: "warning",
|
|
455
|
+
evidence: [{ label: "manifest", value: ".moda/prompts.yml" }, { label: "error", value: error instanceof Error ? error.message : String(error) }],
|
|
456
|
+
remediation: "Fix .moda/prompts.yml (version, prompt_paths, sources).",
|
|
457
|
+
nextCommand: "moda prompts status"
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const evidence = [
|
|
461
|
+
{ label: "manifest", value: `.moda/prompts.yml (version ${inspection.manifestVersion})` },
|
|
462
|
+
{ label: "matched_files", value: String(inspection.matchedFiles) },
|
|
463
|
+
{ label: "sources", value: String(inspection.sources) },
|
|
464
|
+
...inspection.zeroMatchPatterns.length > 0 ? [{ label: "zero_match_patterns", value: inspection.zeroMatchPatterns.join(", ") }] : [],
|
|
465
|
+
...inspection.legacyOnlyFiles.length > 0 ? [{
|
|
466
|
+
label: "legacy_only_files",
|
|
467
|
+
value: `${inspection.legacyOnlyFiles.length} (${inspection.legacyOnlyFiles.slice(0, 3).join(", ")}${inspection.legacyOnlyFiles.length > 3 ? ", ..." : ""})`
|
|
468
|
+
}] : []
|
|
469
|
+
];
|
|
470
|
+
if (inspection.legacyOnlyFiles.length > 0) {
|
|
471
|
+
return {
|
|
472
|
+
...base,
|
|
473
|
+
status: "warning",
|
|
474
|
+
severity: "warning",
|
|
475
|
+
evidence,
|
|
476
|
+
remediation: "Prompt files found by older CLIs no longer match the manifest globs (they are exact paths from the repository root). Migrate the manifest to keep them tracked under their existing keys.",
|
|
477
|
+
nextCommand: "moda prompts migrate --dry-run"
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (inspection.matchedFiles === 0 && inspection.sources === 0) {
|
|
481
|
+
return {
|
|
482
|
+
...base,
|
|
483
|
+
status: "warning",
|
|
484
|
+
severity: "warning",
|
|
485
|
+
evidence,
|
|
486
|
+
remediation: "The prompt manifest matches no files and maps no sources, so sync registers nothing. Fix prompt_paths or preview source discovery.",
|
|
487
|
+
nextCommand: "moda prompts sync --dry-run"
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return { ...base, status: "pass", severity: "info", evidence };
|
|
491
|
+
}
|
|
492
|
+
function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
|
|
493
|
+
const found = relativePaths.filter((path) => existsSync(join(cwd, path)));
|
|
494
|
+
return {
|
|
495
|
+
id,
|
|
496
|
+
group,
|
|
497
|
+
title,
|
|
498
|
+
status: found.length > 0 ? "pass" : "warning",
|
|
499
|
+
severity: found.length > 0 ? "info" : "warning",
|
|
500
|
+
evidence: [{ label: "paths", value: found.length > 0 ? found.join(", ") : "none" }],
|
|
501
|
+
remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(", ")}.`,
|
|
502
|
+
nextCommand: found.length > 0 ? undefined : nextCommand
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function validationEventPathCheck(ingestUrl) {
|
|
506
|
+
return {
|
|
507
|
+
id: "validation_event_path_defined",
|
|
508
|
+
group: "cloud",
|
|
509
|
+
title: "Validation event path is defined",
|
|
510
|
+
status: "pass",
|
|
511
|
+
severity: "info",
|
|
512
|
+
evidence: [{ label: "path", value: `${ingestUrl.replace(/\/+$/, "")}/v1/otel/validation` }]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function offlineCloudChecks(isOffline) {
|
|
516
|
+
const status = isOffline ? "skipped" : "warning";
|
|
517
|
+
const evidence = [{ label: "mode", value: isOffline ? "offline" : "missing api key" }];
|
|
518
|
+
const remediation = isOffline ? "Rerun without --offline to check Moda Cloud." : "Configure an API key before checking cloud data flow.";
|
|
519
|
+
const nextCommand = isOffline ? "moda doctor --online" : "moda init";
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
id: "cloud_api_reachable",
|
|
523
|
+
group: "cloud",
|
|
524
|
+
title: "Moda Cloud API is reachable",
|
|
525
|
+
status,
|
|
526
|
+
severity: "warning",
|
|
527
|
+
evidence,
|
|
528
|
+
remediation,
|
|
529
|
+
nextCommand
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "ingest_endpoint_reachable",
|
|
533
|
+
group: "cloud",
|
|
534
|
+
title: "Moda ingest endpoint is reachable",
|
|
535
|
+
status,
|
|
536
|
+
severity: "warning",
|
|
537
|
+
evidence,
|
|
538
|
+
remediation,
|
|
539
|
+
nextCommand
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "recent_data_flow",
|
|
543
|
+
group: "cloud",
|
|
544
|
+
title: "Recent traces/logs/runs exist",
|
|
545
|
+
status,
|
|
546
|
+
severity: "warning",
|
|
547
|
+
evidence,
|
|
548
|
+
remediation,
|
|
549
|
+
nextCommand
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "cloud_data_flow",
|
|
553
|
+
group: "cloud",
|
|
554
|
+
title: "Moda Cloud data flow",
|
|
555
|
+
status,
|
|
556
|
+
severity: "warning",
|
|
557
|
+
evidence,
|
|
558
|
+
remediation,
|
|
559
|
+
nextCommand
|
|
560
|
+
}
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
async function cloudDataFlowChecks(ingestUrl, apiKey) {
|
|
564
|
+
let overview;
|
|
565
|
+
let cloudApiCheck;
|
|
566
|
+
try {
|
|
567
|
+
overview = await callDataAPI("/overview?days_back=1");
|
|
568
|
+
cloudApiCheck = {
|
|
569
|
+
id: "cloud_api_reachable",
|
|
570
|
+
group: "cloud",
|
|
571
|
+
title: "Moda Cloud API is reachable",
|
|
572
|
+
status: "pass",
|
|
573
|
+
severity: "info",
|
|
574
|
+
evidence: [{ label: "overview", value: "reachable" }]
|
|
575
|
+
};
|
|
576
|
+
} catch (err) {
|
|
577
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
578
|
+
return [
|
|
579
|
+
{
|
|
580
|
+
id: "cloud_api_reachable",
|
|
581
|
+
group: "cloud",
|
|
582
|
+
title: "Moda Cloud API is reachable",
|
|
583
|
+
status: "warning",
|
|
584
|
+
severity: "warning",
|
|
585
|
+
evidence: [{ label: "error", value: error }],
|
|
586
|
+
remediation: "Check API key and base URL.",
|
|
587
|
+
nextCommand: "moda doctor --offline"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
id: "ingest_endpoint_reachable",
|
|
591
|
+
group: "cloud",
|
|
592
|
+
title: "Moda ingest endpoint is reachable",
|
|
593
|
+
status: "skipped",
|
|
594
|
+
severity: "warning",
|
|
595
|
+
evidence: [{ label: "reason", value: "cloud API unreachable" }]
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
id: "recent_data_flow",
|
|
599
|
+
group: "cloud",
|
|
600
|
+
title: "Recent traces/logs/runs exist",
|
|
601
|
+
status: "skipped",
|
|
602
|
+
severity: "warning",
|
|
603
|
+
evidence: [{ label: "reason", value: "overview unavailable" }]
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
id: "cloud_data_flow",
|
|
607
|
+
group: "cloud",
|
|
608
|
+
title: "Moda Cloud data flow",
|
|
609
|
+
status: "warning",
|
|
610
|
+
severity: "warning",
|
|
611
|
+
evidence: [{ label: "error", value: error }],
|
|
612
|
+
remediation: "Check API key, base URL, and whether data has reached Moda.",
|
|
613
|
+
nextCommand: "moda doctor --offline"
|
|
614
|
+
}
|
|
615
|
+
];
|
|
616
|
+
}
|
|
617
|
+
const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
|
|
618
|
+
const recentCheck = recentDataFlowCheck(overview);
|
|
619
|
+
const cloudStatus = cloudApiCheck.status === "pass" && ingestCheck.status === "pass" && recentCheck.status === "pass" ? "pass" : "warning";
|
|
620
|
+
return [
|
|
621
|
+
cloudApiCheck,
|
|
622
|
+
ingestCheck,
|
|
623
|
+
recentCheck,
|
|
624
|
+
{
|
|
625
|
+
id: "cloud_data_flow",
|
|
626
|
+
group: "cloud",
|
|
627
|
+
title: "Moda Cloud data flow",
|
|
628
|
+
status: cloudStatus,
|
|
629
|
+
severity: cloudStatus === "pass" ? "info" : "warning",
|
|
630
|
+
evidence: [
|
|
631
|
+
{ label: "cloud_api", value: cloudApiCheck.status },
|
|
632
|
+
{ label: "ingest", value: ingestCheck.status },
|
|
633
|
+
{ label: "recent_data", value: recentCheck.status }
|
|
634
|
+
],
|
|
635
|
+
remediation: cloudStatus === "pass" ? undefined : "Check API key, ingest URL, and whether data has reached Moda.",
|
|
636
|
+
nextCommand: cloudStatus === "pass" ? undefined : "moda doctor --offline"
|
|
637
|
+
}
|
|
638
|
+
];
|
|
639
|
+
}
|
|
640
|
+
async function ingestReachabilityCheck(ingestUrl, apiKey) {
|
|
641
|
+
const url = `${ingestUrl.replace(/\/+$/, "")}/`;
|
|
642
|
+
try {
|
|
643
|
+
recordApiRequest();
|
|
644
|
+
const response = await fetch(url, {
|
|
645
|
+
method: "GET",
|
|
646
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
647
|
+
signal: AbortSignal.timeout(1e4)
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
id: "ingest_endpoint_reachable",
|
|
651
|
+
group: "cloud",
|
|
652
|
+
title: "Moda ingest endpoint is reachable",
|
|
653
|
+
status: "pass",
|
|
654
|
+
severity: "info",
|
|
655
|
+
evidence: [
|
|
656
|
+
{ label: "url", value: url },
|
|
657
|
+
{ label: "http_status", value: String(response.status) }
|
|
658
|
+
]
|
|
659
|
+
};
|
|
660
|
+
} catch (err) {
|
|
661
|
+
return {
|
|
662
|
+
id: "ingest_endpoint_reachable",
|
|
663
|
+
group: "cloud",
|
|
664
|
+
title: "Moda ingest endpoint is reachable",
|
|
665
|
+
status: "warning",
|
|
666
|
+
severity: "warning",
|
|
667
|
+
evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
|
|
668
|
+
remediation: "Check the configured ingest URL and network reachability.",
|
|
669
|
+
nextCommand: "moda profiles create"
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function recentDataFlowCheck(overview) {
|
|
674
|
+
const data = isRecord(overview) ? overview : {};
|
|
675
|
+
const conversations = numberAt(data, ["conversations", "total"]);
|
|
676
|
+
const toolFailures = numberAt(data, ["tool_failures", "total"]);
|
|
677
|
+
const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
|
|
678
|
+
const lastEventAt = lastActivityTimestamp(recentActivity);
|
|
679
|
+
const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
|
|
680
|
+
return {
|
|
681
|
+
id: "recent_data_flow",
|
|
682
|
+
group: "cloud",
|
|
683
|
+
title: "Recent traces/logs/runs exist",
|
|
684
|
+
status: hasData ? "pass" : "warning",
|
|
685
|
+
severity: hasData ? "info" : "warning",
|
|
686
|
+
evidence: [
|
|
687
|
+
{ label: "conversations", value: String(conversations) },
|
|
688
|
+
{ label: "tool_failures", value: String(toolFailures) },
|
|
689
|
+
{ label: "last_event_at", value: lastEventAt ?? "none" }
|
|
690
|
+
],
|
|
691
|
+
remediation: hasData ? undefined : "No recent Moda data was visible in the overview window.",
|
|
692
|
+
nextCommand: hasData ? undefined : "moda init"
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function numberAt(value, path) {
|
|
696
|
+
let cursor = value;
|
|
697
|
+
for (const key of path) {
|
|
698
|
+
if (!isRecord(cursor))
|
|
699
|
+
return 0;
|
|
700
|
+
cursor = cursor[key];
|
|
701
|
+
}
|
|
702
|
+
return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
|
|
703
|
+
}
|
|
704
|
+
function lastActivityTimestamp(items) {
|
|
705
|
+
const timestamps = items.map((item) => isRecord(item) && typeof item.timestamp === "string" ? item.timestamp : undefined).filter((item) => Boolean(item)).sort();
|
|
706
|
+
return timestamps.at(-1);
|
|
707
|
+
}
|
|
708
|
+
function summarizeChecks(checks) {
|
|
709
|
+
const fail = checks.filter((check) => check.status === "fail").length;
|
|
710
|
+
const warning = checks.filter((check) => check.status === "warning").length;
|
|
711
|
+
const skipped = checks.filter((check) => check.status === "skipped").length;
|
|
712
|
+
const pass = checks.filter((check) => check.status === "pass").length;
|
|
713
|
+
return {
|
|
714
|
+
status: fail > 0 ? "fail" : warning > 0 ? "warning" : "pass",
|
|
715
|
+
pass,
|
|
716
|
+
warning,
|
|
717
|
+
fail,
|
|
718
|
+
skipped
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function nextCommandsForChecks(checks) {
|
|
722
|
+
return [...new Set(checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => check.nextCommand).filter((command) => Boolean(command)))];
|
|
723
|
+
}
|
|
724
|
+
function firstBrokenCheck(checks) {
|
|
725
|
+
return checks.find((check) => check.status === "fail") ?? checks.find((check) => check.status === "warning");
|
|
726
|
+
}
|
|
727
|
+
function checkStatus(checks, id) {
|
|
728
|
+
return checks.find((check) => check.id === id)?.status ?? "skipped";
|
|
729
|
+
}
|
|
730
|
+
function doctorAgentWriteOptions(report, command) {
|
|
731
|
+
const broken = report.checks.filter((check) => check.status === "fail" || check.status === "warning");
|
|
732
|
+
return {
|
|
733
|
+
summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
|
|
734
|
+
findings: broken.slice(0, 16).map((check) => ({
|
|
735
|
+
id: check.id,
|
|
736
|
+
title: check.title,
|
|
737
|
+
status: check.status,
|
|
738
|
+
severity: check.severity,
|
|
739
|
+
description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
|
|
740
|
+
evidence_ref_ids: [`doctor:${check.id}`]
|
|
741
|
+
})),
|
|
742
|
+
evidenceRefs: report.checks.map((check) => ({
|
|
743
|
+
id: `doctor:${check.id}`,
|
|
744
|
+
kind: "doctor_check",
|
|
745
|
+
label: check.title,
|
|
746
|
+
status: check.status
|
|
747
|
+
})),
|
|
748
|
+
recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
|
|
749
|
+
id: `doctor_action_${index + 1}_${item.checkId}`,
|
|
750
|
+
title: item.action,
|
|
751
|
+
command: item.command,
|
|
752
|
+
rationale: `Doctor check ${item.checkId} is not passing.`,
|
|
753
|
+
mutability: item.command ? agentMutabilityForCommand(item.command) : "unknown",
|
|
754
|
+
requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true
|
|
755
|
+
})),
|
|
756
|
+
nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
|
|
757
|
+
command: nextCommand,
|
|
758
|
+
purpose: agentPurposeForCommand(nextCommand),
|
|
759
|
+
mutability: agentMutabilityForCommand(nextCommand),
|
|
760
|
+
requires_approval: agentCommandRequiresApproval(nextCommand)
|
|
761
|
+
})),
|
|
762
|
+
warnings: broken.filter((check) => check.status === "warning").slice(0, 16).map((check) => `${check.title}: ${check.remediation ?? check.status}`)
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function agentMutabilityForCommand(command) {
|
|
766
|
+
if (/\bsync\b/.test(command))
|
|
767
|
+
return "sync";
|
|
768
|
+
if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
|
|
769
|
+
return "write";
|
|
770
|
+
return "read";
|
|
771
|
+
}
|
|
772
|
+
function agentCommandRequiresApproval(command) {
|
|
773
|
+
return agentMutabilityForCommand(command) !== "read" && !command.includes("--dry-run");
|
|
774
|
+
}
|
|
775
|
+
function agentPurposeForCommand(command) {
|
|
776
|
+
if (command.includes("doctor"))
|
|
777
|
+
return "Re-run Moda diagnostics.";
|
|
778
|
+
if (command.includes("harness approve"))
|
|
779
|
+
return "Approve the validated harness report after review.";
|
|
780
|
+
if (command.includes("harness sync"))
|
|
781
|
+
return "Sync harness state to Moda Cloud.";
|
|
782
|
+
if (command.includes("harness scan"))
|
|
783
|
+
return "Refresh the local harness manifest.";
|
|
784
|
+
if (command.includes("init"))
|
|
785
|
+
return "Run Moda onboarding.";
|
|
786
|
+
return "Inspect or repair Moda setup.";
|
|
787
|
+
}
|
|
788
|
+
function stableGraphHash(graph) {
|
|
789
|
+
return createHash("sha256").update(JSON.stringify(sortJson({
|
|
790
|
+
...graph,
|
|
791
|
+
scan: {
|
|
792
|
+
...graph.scan,
|
|
793
|
+
createdAt: undefined,
|
|
794
|
+
scannerVersion: undefined,
|
|
795
|
+
skipped: undefined
|
|
796
|
+
}
|
|
797
|
+
}))).digest("hex");
|
|
798
|
+
}
|
|
799
|
+
function sortJson(value) {
|
|
800
|
+
if (Array.isArray(value))
|
|
801
|
+
return value.map((item) => sortJson(item));
|
|
802
|
+
if (value && typeof value === "object") {
|
|
803
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
|
|
804
|
+
}
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
807
|
+
function isRecord(value) {
|
|
808
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export { runDoctorCommand, runStatusCommand, buildDoctorReport, promptsDiscoveredCheck };
|