@moda-ai/cli 1.22.0 → 1.22.2

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.
Files changed (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -777
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. package/dist/workspace.js +0 -1294
package/dist/doctor.js DELETED
@@ -1,1182 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { spawnSync } from 'node:child_process';
3
- import { existsSync, readFileSync, statSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { callDataAPI } from './api-client.js';
6
- import { isAuthSessionValid, loadAuthSession } from './auth.js';
7
- import { resolveApiKey, resolveIngestUrl, resolveModaBaseUrl, resolveTenantId } from './config.js';
8
- import { readHarnessSyncState, scanHarness } from './harness.js';
9
- import { HARNESS_REPORT_APPROVAL_PATH, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, hashHarnessReport, readHarnessReport, validateHarnessReport, } from './harness-report.js';
10
- import { ensureSecretFilePermissions, getActiveProfile, getModaPaths, isLoopbackBaseUrl, loadProfileConfig, resolveSecretReference, secretFilePathFromRef, } from './profiles.js';
11
- import { renderDoctorHuman, renderStatusHuman } from './tui.js';
12
- export async function runDoctorCommand(context) {
13
- const report = await buildDoctorReport(context, {
14
- offline: context.flags.offline === 'true',
15
- });
16
- if (context.outputMode === 'human') {
17
- if (!context.quiet)
18
- console.log(renderDoctorHuman(report));
19
- return { exitCode: 0 };
20
- }
21
- context.output.writeData(report, {
22
- status: report.summary.fail > 0 ? 'error' : 'success',
23
- ...doctorAgentWriteOptions(report, 'doctor'),
24
- });
25
- return { exitCode: 0 };
26
- }
27
- export async function runStatusCommand(context) {
28
- const report = await buildDoctorReport(context, {
29
- offline: context.flags.offline === 'true' || context.flags.online !== 'true',
30
- });
31
- const topBroken = firstBrokenCheck(report.checks);
32
- const statusPayload = {
33
- schemaVersion: 'status.v0.1',
34
- generatedAt: report.generatedAt,
35
- status: report.summary.status,
36
- activeProfile: report.profile.active,
37
- auth: {
38
- apiKey: resolveApiKey() ? 'configured' : 'missing',
39
- },
40
- workspace: {
41
- tenantId: resolveTenantId() ?? null,
42
- baseUrl: resolveModaBaseUrl(),
43
- ingestUrl: resolveIngestUrl(),
44
- },
45
- dataFlow: checkStatus(report.checks, 'cloud_data_flow'),
46
- harness: checkStatus(report.checks, 'harness_manifest_current'),
47
- harnessSync: checkStatus(report.checks, 'harness_sync_state_present'),
48
- harnessReport: checkStatus(report.checks, 'harness_report_approved_synced'),
49
- shadowSpark: checkStatus(report.checks, 'shadow_spark_workspace_health'),
50
- prompts: checkStatus(report.checks, 'prompts_discovered'),
51
- skills: checkStatus(report.checks, 'skills_discovered'),
52
- topBrokenCheck: topBroken
53
- ? {
54
- id: topBroken.id,
55
- status: topBroken.status,
56
- severity: topBroken.severity,
57
- nextCommand: topBroken.nextCommand,
58
- }
59
- : null,
60
- nextCommands: report.nextCommands.slice(0, 3),
61
- };
62
- if (context.outputMode === 'human') {
63
- if (!context.quiet)
64
- console.log(renderStatusHuman(statusPayload));
65
- return { exitCode: 0 };
66
- }
67
- context.output.writeData(statusPayload, {
68
- status: report.summary.fail > 0 ? 'error' : 'success',
69
- ...doctorAgentWriteOptions(report, 'status'),
70
- });
71
- return { exitCode: 0 };
72
- }
73
- export async function buildDoctorReport(context, options) {
74
- const checks = [];
75
- const profileConfig = loadProfileConfig({ env: context.env });
76
- const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
77
- const paths = getModaPaths({ env: context.env });
78
- const profileOptions = { profile: context.profile, env: context.env };
79
- const apiKey = resolveApiKey(profileOptions);
80
- const tenantId = resolveTenantId(profileOptions);
81
- const baseUrl = resolveModaBaseUrl(profileOptions);
82
- const ingestUrl = resolveIngestUrl(profileOptions);
83
- checks.push({
84
- id: 'active_profile_resolves',
85
- group: 'config',
86
- title: 'Active profile resolves',
87
- status: activeProfile ? 'pass' : 'fail',
88
- severity: activeProfile ? 'info' : 'critical',
89
- evidence: [
90
- { label: 'active_profile', value: profileConfig.active_profile },
91
- { label: 'environment', value: activeProfile.environment },
92
- ],
93
- remediation: activeProfile ? undefined : 'Create or select a Moda profile.',
94
- nextCommand: activeProfile ? undefined : 'moda profiles list',
95
- });
96
- checks.push({
97
- id: 'api_key_resolves',
98
- group: 'config',
99
- title: 'API key resolves',
100
- status: apiKey ? 'pass' : 'fail',
101
- severity: apiKey ? 'info' : 'critical',
102
- evidence: [{ label: 'source', value: apiKey ? 'environment/profile/config' : 'none' }],
103
- remediation: apiKey ? undefined : 'Authenticate and create a tenant API key.',
104
- nextCommand: apiKey ? undefined : 'moda init',
105
- });
106
- checks.push({
107
- id: 'tenant_resolves',
108
- group: 'config',
109
- title: 'Tenant/workspace resolves',
110
- status: tenantId ? 'pass' : 'warning',
111
- severity: tenantId ? 'info' : 'warning',
112
- evidence: [{ label: 'tenant_id', value: tenantId ? 'configured' : 'missing' }],
113
- remediation: tenantId ? undefined : 'Select a tenant during init or set MODA_TENANT_ID.',
114
- nextCommand: tenantId ? undefined : 'moda init',
115
- });
116
- checks.push(authTokenCheck(context, Boolean(apiKey)));
117
- checks.push(configPermissionCheck(paths.legacyConfigPath));
118
- if (activeProfile.api_key_ref) {
119
- const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
120
- if (secretPath)
121
- checks.push(secretPermissionCheck(secretPath));
122
- checks.push({
123
- id: 'profile_api_key_ref_resolves',
124
- group: 'config',
125
- title: 'Profile API key reference resolves',
126
- status: resolveSecretReference(activeProfile.api_key_ref) ? 'pass' : 'warning',
127
- severity: 'warning',
128
- evidence: [{ label: 'api_key_ref', value: activeProfile.api_key_ref }],
129
- remediation: 'Create or update the profile API key reference.',
130
- nextCommand: 'moda profiles create',
131
- });
132
- }
133
- checks.push(urlCheck('base_url_valid', 'config', 'Base URL is valid', baseUrl));
134
- checks.push(urlCheck('ingest_url_valid', 'config', 'Ingest URL is valid', ingestUrl));
135
- checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
136
- const manifestPath = join(context.cwd, '.moda', 'harness.json');
137
- const manifestExists = existsSync(manifestPath);
138
- checks.push({
139
- id: 'harness_manifest_exists',
140
- group: 'harness',
141
- title: 'Harness manifest exists',
142
- status: manifestExists ? 'pass' : 'warning',
143
- severity: manifestExists ? 'info' : 'warning',
144
- evidence: [{ label: 'path', value: '.moda/harness.json' }],
145
- remediation: manifestExists ? undefined : 'Run `moda harness scan --summary` to scan the project and write a harness manifest.',
146
- nextCommand: manifestExists ? undefined : 'moda harness scan --summary',
147
- });
148
- checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
149
- checks.push(harnessSyncStateCheck(context.cwd));
150
- checks.push(harnessReportStateCheck(context.cwd));
151
- checks.push(discoveryCheck('prompts_discovered', 'harness', 'Prompts are discovered', context.cwd, [
152
- 'prompts',
153
- '.moda/prompts.yml',
154
- ], 'moda prompts init'));
155
- checks.push(discoveryCheck('skills_discovered', 'harness', 'Skills are discovered', context.cwd, [
156
- 'skills',
157
- '.claude/skills',
158
- '.cursor/rules',
159
- ], 'moda skills pull'));
160
- checks.push(discoveryCheck('evals_discovered', 'harness', 'Evals/tests are discovered', context.cwd, [
161
- 'evals',
162
- 'tests',
163
- '__tests__',
164
- ]));
165
- checks.push(sdkInitializationCheck(context.cwd));
166
- checks.push(validationEventPathCheck(ingestUrl));
167
- if (options.offline || !apiKey) {
168
- checks.push(...offlineCloudChecks(options.offline));
169
- }
170
- else {
171
- checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
172
- }
173
- const nextCommands = nextCommandsForChecks(checks);
174
- const summary = summarizeChecks(checks);
175
- return {
176
- schemaVersion: 'doctor.v0.1',
177
- generatedAt: new Date().toISOString(),
178
- profile: {
179
- active: activeProfile.name,
180
- requested: context.profile,
181
- },
182
- summary,
183
- checks,
184
- nextCommands,
185
- fixPlan: checks
186
- .filter((check) => check.status === 'fail' || check.status === 'warning')
187
- .map((check) => ({
188
- checkId: check.id,
189
- command: check.nextCommand,
190
- action: check.remediation ?? 'Review this diagnostic.',
191
- })),
192
- };
193
- }
194
- function authTokenCheck(context, hasApiKey) {
195
- const session = loadAuthSession({
196
- profile: context.profile,
197
- env: context.env,
198
- includeExpired: true,
199
- });
200
- const valid = isAuthSessionValid(session);
201
- if (!valid && hasApiKey) {
202
- return {
203
- id: 'auth_token_valid',
204
- group: 'config',
205
- title: 'Browser auth token is valid',
206
- status: 'skipped',
207
- severity: 'info',
208
- evidence: [{ label: 'reason', value: 'api key configured' }],
209
- };
210
- }
211
- return {
212
- id: 'auth_token_valid',
213
- group: 'config',
214
- title: 'Browser auth token is valid',
215
- status: valid ? 'pass' : 'warning',
216
- severity: valid ? 'info' : 'warning',
217
- evidence: [
218
- { label: 'token', value: session ? 'present' : 'missing' },
219
- ...(session ? [{ label: 'expires_at', value: new Date(session.expires_at * 1000).toISOString() }] : []),
220
- ],
221
- remediation: valid ? undefined : 'Refresh browser authentication.',
222
- nextCommand: valid ? undefined : 'moda init',
223
- };
224
- }
225
- function configPermissionCheck(path) {
226
- if (!existsSync(path)) {
227
- return {
228
- id: 'config_file_permissions',
229
- group: 'config',
230
- title: 'Config file permissions',
231
- status: 'skipped',
232
- severity: 'info',
233
- evidence: [{ label: 'path', value: path }],
234
- };
235
- }
236
- const permission = ensureSecretFilePermissions(path);
237
- return {
238
- id: 'config_file_permissions',
239
- group: 'config',
240
- title: 'Config file permissions',
241
- status: permission.secure ? 'pass' : 'fail',
242
- severity: permission.secure ? 'info' : 'critical',
243
- evidence: [
244
- { label: 'path', value: path },
245
- { label: 'mode', value: permission.mode ?? 'unknown' },
246
- ],
247
- remediation: permission.secure ? undefined : 'Restrict config file permissions to the current user.',
248
- };
249
- }
250
- function secretPermissionCheck(path) {
251
- const permission = ensureSecretFilePermissions(path);
252
- return {
253
- id: 'secret_file_permissions',
254
- group: 'config',
255
- title: 'Secret file permissions',
256
- status: permission.secure ? 'pass' : 'fail',
257
- severity: permission.secure ? 'info' : 'critical',
258
- evidence: [
259
- { label: 'path', value: path },
260
- { label: 'mode', value: permission.mode ?? 'unknown' },
261
- ],
262
- remediation: permission.secure ? undefined : 'Restrict secret file permissions to the current user.',
263
- };
264
- }
265
- function urlCheck(id, group, title, value) {
266
- let valid = false;
267
- try {
268
- const parsed = new URL(value);
269
- valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
270
- }
271
- catch {
272
- valid = false;
273
- }
274
- return {
275
- id,
276
- group,
277
- title,
278
- status: valid ? 'pass' : 'fail',
279
- severity: valid ? 'info' : 'critical',
280
- evidence: [{ label: 'url', value }],
281
- remediation: valid ? undefined : 'Configure a valid http(s) URL.',
282
- nextCommand: valid ? undefined : 'moda profiles create',
283
- };
284
- }
285
- /**
286
- * Local stacks mount `/auth/cli` and `/api/*` CLI/control proxies on the
287
- * frontend (`:3000` by default). Pointing `base_url` at the backend (`:3001`)
288
- * breaks browser login even though some Nest routes still answer there.
289
- */
290
- function localBaseUrlCheck(environment, baseUrl) {
291
- // Only the `local` profile targets a loopback worktree stack. `dev` defaults to
292
- // remote `https://moda.dev`, so including it here warns valid remote configs.
293
- if (environment !== 'local') {
294
- return {
295
- id: 'local_base_url_frontend',
296
- group: 'config',
297
- title: 'Local base URL targets the frontend',
298
- status: 'skipped',
299
- severity: 'info',
300
- evidence: [{ label: 'environment', value: environment }],
301
- };
302
- }
303
- let port = null;
304
- let loopback = false;
305
- try {
306
- const parsed = new URL(baseUrl);
307
- loopback = isLoopbackBaseUrl(baseUrl);
308
- port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80');
309
- }
310
- catch {
311
- return {
312
- id: 'local_base_url_frontend',
313
- group: 'config',
314
- title: 'Local base URL targets the frontend',
315
- status: 'fail',
316
- severity: 'critical',
317
- evidence: [{ label: 'base_url', value: baseUrl }],
318
- remediation: 'Set the local profile base URL to the frontend origin (http://localhost:3000).',
319
- nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000',
320
- };
321
- }
322
- const looksLikeBackend = loopback && (port === '3001' || port === '3002');
323
- if (looksLikeBackend) {
324
- return {
325
- id: 'local_base_url_frontend',
326
- group: 'config',
327
- title: 'Local base URL targets the frontend',
328
- status: 'warning',
329
- severity: 'warning',
330
- evidence: [
331
- { label: 'base_url', value: baseUrl },
332
- { label: 'port', value: port ?? 'unknown' },
333
- ],
334
- remediation: 'Local CLI auth and control APIs are served by the frontend. ' +
335
- 'Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.',
336
- nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000 --use',
337
- };
338
- }
339
- if (!loopback) {
340
- return {
341
- id: 'local_base_url_frontend',
342
- group: 'config',
343
- title: 'Local base URL targets the frontend',
344
- status: 'warning',
345
- severity: 'warning',
346
- evidence: [{ label: 'base_url', value: baseUrl }],
347
- remediation: 'Local profile should use a loopback frontend URL (http://localhost:3000). ' +
348
- 'Activate with `moda profiles use local` or set MODA_PROFILE=local.',
349
- nextCommand: 'moda profiles use local',
350
- };
351
- }
352
- return {
353
- id: 'local_base_url_frontend',
354
- group: 'config',
355
- title: 'Local base URL targets the frontend',
356
- status: 'pass',
357
- severity: 'info',
358
- evidence: [{ label: 'base_url', value: baseUrl }],
359
- };
360
- }
361
- function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
362
- if (!manifestExists) {
363
- return {
364
- id: 'harness_manifest_current',
365
- group: 'harness',
366
- title: 'Harness manifest matches scanner output',
367
- status: 'skipped',
368
- severity: 'info',
369
- evidence: [{ label: 'reason', value: 'manifest missing' }],
370
- nextCommand: 'moda harness scan --summary',
371
- };
372
- }
373
- try {
374
- const current = scanHarness({
375
- rootDir: cwd,
376
- scannerVersion: 'doctor',
377
- tenantId,
378
- tenantSource: tenantId ? 'env_or_config' : undefined,
379
- });
380
- const saved = JSON.parse(readFileSync(manifestPath, 'utf8'));
381
- const savedHash = stableGraphHash(saved);
382
- const currentHash = stableGraphHash(current);
383
- const matches = savedHash === currentHash;
384
- return {
385
- id: 'harness_manifest_current',
386
- group: 'harness',
387
- title: 'Harness manifest matches scanner output',
388
- status: matches ? 'pass' : 'warning',
389
- severity: matches ? 'info' : 'warning',
390
- evidence: [
391
- { label: 'saved_hash', value: savedHash.slice(0, 12) },
392
- { label: 'current_hash', value: currentHash.slice(0, 12) },
393
- ...(matches ? [] : [{ label: 'diff', value: 'Manifest hash differs from fresh scanner output.' }]),
394
- ],
395
- remediation: matches ? undefined : 'Refresh the local harness manifest.',
396
- nextCommand: matches ? undefined : 'moda harness scan --summary',
397
- };
398
- }
399
- catch (err) {
400
- return {
401
- id: 'harness_manifest_current',
402
- group: 'harness',
403
- title: 'Harness manifest matches scanner output',
404
- status: 'fail',
405
- severity: 'critical',
406
- evidence: [{ label: 'error', value: err instanceof Error ? err.message : String(err) }],
407
- remediation: 'Regenerate the harness manifest.',
408
- nextCommand: 'moda harness scan --summary',
409
- };
410
- }
411
- }
412
- function harnessSyncStateCheck(cwd) {
413
- const state = readHarnessSyncState(cwd);
414
- if (!state) {
415
- return {
416
- id: 'harness_sync_state_present',
417
- group: 'harness',
418
- title: 'Harness sync state exists',
419
- status: 'warning',
420
- severity: 'warning',
421
- evidence: [{ label: 'path', value: '.moda/sync-state.json' }],
422
- remediation: 'Sync the harness graph so Moda can correlate local state with cloud state.',
423
- nextCommand: 'moda harness sync --dry-run',
424
- };
425
- }
426
- return {
427
- id: 'harness_sync_state_present',
428
- group: 'harness',
429
- title: 'Harness sync state exists',
430
- status: 'pass',
431
- severity: 'info',
432
- evidence: [
433
- { label: 'updated_at', value: state.updatedAt },
434
- { label: 'profile', value: state.profile },
435
- { label: 'harness_id', value: state.harnessId },
436
- { label: 'remote_version_id', value: state.remote.versionId || 'none' },
437
- ],
438
- };
439
- }
440
- function harnessReportStateCheck(cwd) {
441
- const reportPath = join(cwd, HARNESS_REPORT_PATH);
442
- if (!existsSync(reportPath)) {
443
- return {
444
- id: 'harness_report_approved_synced',
445
- group: 'harness',
446
- title: 'Harness analyst report is approved and synced',
447
- status: 'warning',
448
- severity: 'warning',
449
- evidence: [{ label: 'path', value: HARNESS_REPORT_PATH }],
450
- remediation: 'Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.',
451
- nextCommand: 'moda harness analyze --summary',
452
- };
453
- }
454
- try {
455
- const report = readHarnessReport(reportPath);
456
- const validation = validateHarnessReport(report, cwd);
457
- const reportHash = hashHarnessReport(report);
458
- const evidence = [
459
- { label: 'path', value: HARNESS_REPORT_PATH },
460
- { label: 'report_hash', value: reportHash.slice(0, 12) },
461
- { label: 'validation', value: validation.status },
462
- ];
463
- if (validation.status !== 'pass') {
464
- return {
465
- id: 'harness_report_approved_synced',
466
- group: 'harness',
467
- title: 'Harness analyst report is approved and synced',
468
- status: 'fail',
469
- severity: 'critical',
470
- evidence: [
471
- ...evidence,
472
- { label: 'errors', value: validation.errors.slice(0, 3).join(' | ') || 'validation failed' },
473
- ],
474
- remediation: 'Regenerate or fix the harness report before approving or syncing.',
475
- nextCommand: 'moda harness analyze --summary',
476
- };
477
- }
478
- const approval = readHarnessReportApproval(cwd);
479
- const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION &&
480
- approval.reportId === report.reportId &&
481
- approval.reportHash === reportHash &&
482
- approval.approved === true;
483
- evidence.push({ label: 'approval', value: approved ? 'approved' : approval ? 'stale_or_invalid' : 'missing' });
484
- if (approval && !approved) {
485
- return {
486
- id: 'harness_report_approved_synced',
487
- group: 'harness',
488
- title: 'Harness analyst report is approved and synced',
489
- status: 'fail',
490
- severity: 'critical',
491
- evidence,
492
- remediation: 'Approval no longer matches the current report. Review and approve the report again.',
493
- nextCommand: 'moda harness approve --yes',
494
- };
495
- }
496
- if (!approved) {
497
- return {
498
- id: 'harness_report_approved_synced',
499
- group: 'harness',
500
- title: 'Harness analyst report is approved and synced',
501
- status: 'warning',
502
- severity: 'warning',
503
- evidence,
504
- remediation: 'Review and approve the validated harness report before syncing.',
505
- nextCommand: 'moda harness approve --yes',
506
- };
507
- }
508
- const state = readHarnessSyncState(cwd);
509
- const synced = state?.source === 'approved_report' && state.report?.reportHash === reportHash;
510
- evidence.push({ label: 'sync', value: synced ? 'synced' : 'not_synced_to_this_report' }, ...(state?.report?.reportHash ? [{ label: 'sync_report_hash', value: state.report.reportHash.slice(0, 12) }] : []));
511
- return {
512
- id: 'harness_report_approved_synced',
513
- group: 'harness',
514
- title: 'Harness analyst report is approved and synced',
515
- status: synced ? 'pass' : 'warning',
516
- severity: synced ? 'info' : 'warning',
517
- evidence,
518
- remediation: synced ? undefined : 'Sync the approved harness report so Moda cloud has the same harness graph.',
519
- nextCommand: synced ? undefined : 'moda harness sync --from-report .moda/harness-report.json',
520
- };
521
- }
522
- catch (err) {
523
- return {
524
- id: 'harness_report_approved_synced',
525
- group: 'harness',
526
- title: 'Harness analyst report is approved and synced',
527
- status: 'fail',
528
- severity: 'critical',
529
- evidence: [
530
- { label: 'path', value: HARNESS_REPORT_PATH },
531
- { label: 'error', value: err instanceof Error ? err.message : String(err) },
532
- ],
533
- remediation: 'Regenerate the harness report.',
534
- nextCommand: 'moda harness analyze --summary',
535
- };
536
- }
537
- }
538
- function readHarnessReportApproval(cwd) {
539
- const approvalPath = join(cwd, HARNESS_REPORT_APPROVAL_PATH);
540
- if (!existsSync(approvalPath))
541
- return null;
542
- try {
543
- return JSON.parse(readFileSync(approvalPath, 'utf8'));
544
- }
545
- catch {
546
- return {
547
- schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
548
- reportId: 'unreadable',
549
- reportHash: 'unreadable',
550
- approved: true,
551
- approvedAt: '',
552
- approvedBy: 'local-human',
553
- reportPath: HARNESS_REPORT_PATH,
554
- };
555
- }
556
- }
557
- function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
558
- const found = relativePaths.filter((path) => existsSync(join(cwd, path)));
559
- return {
560
- id,
561
- group,
562
- title,
563
- status: found.length > 0 ? 'pass' : 'warning',
564
- severity: found.length > 0 ? 'info' : 'warning',
565
- evidence: [{ label: 'paths', value: found.length > 0 ? found.join(', ') : 'none' }],
566
- remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(', ')}.`,
567
- nextCommand: found.length > 0 ? undefined : nextCommand,
568
- };
569
- }
570
- function sdkInitializationCheck(cwd) {
571
- const candidates = ['src', 'app', 'agent_harness'];
572
- let detected = false;
573
- for (const candidate of candidates) {
574
- const path = join(cwd, candidate);
575
- if (!existsSync(path))
576
- continue;
577
- try {
578
- const stat = statSync(path);
579
- detected ||= stat.isDirectory() || stat.isFile();
580
- }
581
- catch {
582
- // Ignore unreadable candidate paths; doctor reports higher-level gaps.
583
- }
584
- }
585
- return {
586
- id: 'sdk_initialization_detected',
587
- group: 'harness',
588
- title: 'SDK initialization is detectable',
589
- status: detected ? 'warning' : 'skipped',
590
- severity: 'warning',
591
- evidence: [{ label: 'status', value: detected ? 'code present, instrumentation not verified' : 'no source candidates' }],
592
- remediation: 'Use the generated integration guide to add Moda SDK instrumentation.',
593
- nextCommand: 'moda init',
594
- };
595
- }
596
- // Unused while coding-agent capture is disabled; kept for re-enablement.
597
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
598
- function shadowSparkWorkspaceCheck(cwd, env) {
599
- const repo = detectGitRepo(cwd);
600
- if (!repo) {
601
- return {
602
- id: 'shadow_spark_workspace_health',
603
- group: 'spark',
604
- title: 'Shadow Spark workspace health',
605
- status: 'skipped',
606
- severity: 'info',
607
- evidence: [{ label: 'reason', value: 'not inside a git repository' }],
608
- };
609
- }
610
- const workspaceId = createHash('sha256')
611
- .update(`${repo.toplevel}:${repo.originUrl || ''}`)
612
- .digest('hex')
613
- .slice(0, 16);
614
- const path = join(getModaPaths({ env }).home, '.moda', 'workspaces', workspaceId);
615
- const healthy = existsSync(join(path, '.git'));
616
- return {
617
- id: 'shadow_spark_workspace_health',
618
- group: 'spark',
619
- title: 'Shadow Spark workspace health',
620
- status: healthy ? 'pass' : 'warning',
621
- severity: healthy ? 'info' : 'warning',
622
- evidence: [
623
- { label: 'workspace_id', value: workspaceId },
624
- { label: 'path', value: path },
625
- { label: 'shadow_git', value: healthy ? 'present' : 'missing' },
626
- ],
627
- remediation: healthy ? undefined : 'Create a Shadow Spark snapshot.',
628
- nextCommand: healthy ? undefined : 'moda spark snapshot --json',
629
- };
630
- }
631
- // Unused while coding-agent capture is disabled; kept for re-enablement.
632
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
633
- function hookCheck(env) {
634
- const paths = getModaPaths({ env });
635
- const hookCandidates = [
636
- join(paths.home, '.claude', 'hooks', 'moda-snapshot.sh'),
637
- join(paths.home, '.codex', 'hooks.json'),
638
- join(paths.home, '.cursor', 'hooks.json'),
639
- ];
640
- const found = hookCandidates.filter((path) => existsSync(path));
641
- return {
642
- id: 'hooks_installed',
643
- group: 'hooks',
644
- title: 'Coding-agent hooks installed',
645
- status: found.length > 0 ? 'pass' : 'warning',
646
- severity: found.length > 0 ? 'info' : 'warning',
647
- evidence: [{ label: 'installed', value: found.length > 0 ? String(found.length) : 'none' }],
648
- remediation: found.length > 0 ? undefined : 'Install coding-agent trace hooks.',
649
- nextCommand: found.length > 0 ? undefined : 'moda hooks install --agent=all',
650
- };
651
- }
652
- // Unused while coding-agent capture is disabled; kept for re-enablement.
653
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
654
- function hookTelemetryChecks(env, ingestUrl) {
655
- const configs = agentTelemetryConfigs(env);
656
- return [
657
- hookFilesExecutableCheck(env),
658
- agentConfigJsonCheck(configs),
659
- agentConfigApiKeyCheck(configs),
660
- otlpEndpointCheck(configs, ingestUrl),
661
- agentConfigApiKeyStorageCheck(configs),
662
- ];
663
- }
664
- function agentTelemetryConfigs(env) {
665
- const paths = getModaPaths({ env });
666
- const configs = [
667
- {
668
- agent: 'claude',
669
- title: 'Claude Code',
670
- path: join(paths.home, '.claude', 'settings.json'),
671
- providerPath: 'claude',
672
- providerAttribute: 'claude_code',
673
- },
674
- {
675
- agent: 'codex',
676
- title: 'Codex',
677
- path: join(paths.home, '.codex', 'hooks.json'),
678
- providerPath: 'codex',
679
- providerAttribute: 'codex',
680
- },
681
- {
682
- agent: 'cursor',
683
- title: 'Cursor',
684
- path: join(paths.home, '.cursor', 'hooks.json'),
685
- providerPath: 'cursor',
686
- providerAttribute: 'cursor',
687
- },
688
- ];
689
- return configs.map((config) => {
690
- if (!existsSync(config.path))
691
- return { ...config, exists: false };
692
- try {
693
- const parsed = JSON.parse(readFileSync(config.path, 'utf8'));
694
- if (!isRecord(parsed)) {
695
- return { ...config, exists: true, error: 'JSON root is not an object.' };
696
- }
697
- return { ...config, exists: true, parsed };
698
- }
699
- catch (err) {
700
- return {
701
- ...config,
702
- exists: true,
703
- error: err instanceof Error ? err.message : String(err),
704
- };
705
- }
706
- });
707
- }
708
- function hookFilesExecutableCheck(env) {
709
- const paths = getModaPaths({ env });
710
- const candidates = [
711
- join(paths.home, '.claude', 'hooks', 'moda-snapshot.sh'),
712
- join(paths.home, '.moda', 'hooks', 'moda-snapshot.sh'),
713
- ];
714
- const found = candidates.filter((path) => existsSync(path));
715
- const notExecutable = found.filter((path) => {
716
- try {
717
- return (statSync(path).mode & 0o111) === 0;
718
- }
719
- catch {
720
- return true;
721
- }
722
- });
723
- return {
724
- id: 'hook_files_executable',
725
- group: 'hooks',
726
- title: 'Hook files are executable',
727
- status: found.length === 0 ? 'skipped' : notExecutable.length === 0 ? 'pass' : 'fail',
728
- severity: notExecutable.length === 0 ? 'info' : 'critical',
729
- evidence: [
730
- { label: 'found', value: found.length > 0 ? found.map(relativeHomePath(paths.home)).join(', ') : 'none' },
731
- ...(notExecutable.length > 0
732
- ? [{ label: 'not_executable', value: notExecutable.map(relativeHomePath(paths.home)).join(', ') }]
733
- : []),
734
- ],
735
- remediation: notExecutable.length > 0 ? 'Reinstall hooks so Moda can set executable permissions.' : undefined,
736
- nextCommand: notExecutable.length > 0 || found.length === 0 ? 'moda hooks install --agent=all' : undefined,
737
- };
738
- }
739
- function agentConfigJsonCheck(configs) {
740
- const existing = configs.filter((config) => config.exists);
741
- const invalid = existing.filter((config) => config.error);
742
- return {
743
- id: 'agent_config_json_valid',
744
- group: 'hooks',
745
- title: 'Agent config JSON is valid',
746
- status: existing.length === 0 ? 'skipped' : invalid.length === 0 ? 'pass' : 'fail',
747
- severity: invalid.length === 0 ? 'info' : 'critical',
748
- evidence: [
749
- { label: 'configs', value: existing.length > 0 ? existing.map((config) => config.agent).join(', ') : 'none' },
750
- ...(invalid.length > 0
751
- ? invalid.map((config) => ({ label: `${config.agent}_error`, value: config.error ?? 'invalid JSON' }))
752
- : []),
753
- ],
754
- remediation: invalid.length > 0 ? 'Fix invalid agent hook config JSON.' : undefined,
755
- nextCommand: invalid.length > 0 ? 'moda hooks install --agent=all' : undefined,
756
- };
757
- }
758
- function agentConfigApiKeyCheck(configs) {
759
- const parsed = configs.filter((config) => config.parsed);
760
- const missing = parsed.filter((config) => !telemetryHeader(config).replace(/^Authorization=Bearer\s*/, '').trim());
761
- return {
762
- id: 'agent_config_api_key_resolves',
763
- group: 'hooks',
764
- title: 'API key exists in agent telemetry config',
765
- status: parsed.length === 0 ? 'skipped' : missing.length === 0 ? 'pass' : 'warning',
766
- severity: missing.length === 0 ? 'info' : 'warning',
767
- evidence: [
768
- { label: 'configs', value: parsed.length > 0 ? parsed.map((config) => config.agent).join(', ') : 'none' },
769
- ...(missing.length > 0 ? [{ label: 'missing_or_empty', value: missing.map((config) => config.agent).join(', ') }] : []),
770
- ],
771
- remediation: missing.length > 0 ? 'Reinstall hooks after authenticating so telemetry has an API key.' : undefined,
772
- nextCommand: missing.length > 0 ? 'moda hooks install --agent=all' : undefined,
773
- };
774
- }
775
- function otlpEndpointCheck(configs, ingestUrl) {
776
- const parsed = configs.filter((config) => config.parsed);
777
- const expectedBase = ingestUrl.replace(/\/+$/, '');
778
- const mismatches = [];
779
- for (const config of parsed) {
780
- const env = telemetryEnv(config);
781
- const expected = {
782
- OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/logs`,
783
- OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/metrics`,
784
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/traces`,
785
- };
786
- for (const [key, value] of Object.entries(expected)) {
787
- if (env[key] !== value)
788
- mismatches.push(`${config.agent}:${key}`);
789
- }
790
- const attrs = String(env.OTEL_RESOURCE_ATTRIBUTES ?? '');
791
- if (!attrs.split(',').some((item) => item.trim() === `provider=${config.providerAttribute}`)) {
792
- mismatches.push(`${config.agent}:OTEL_RESOURCE_ATTRIBUTES`);
793
- }
794
- }
795
- return {
796
- id: 'otlp_endpoints_match_profile',
797
- group: 'hooks',
798
- title: 'OTLP endpoints match active ingest URL',
799
- status: parsed.length === 0 ? 'skipped' : mismatches.length === 0 ? 'pass' : 'warning',
800
- severity: mismatches.length === 0 ? 'info' : 'warning',
801
- evidence: [
802
- { label: 'ingest_url', value: expectedBase },
803
- { label: 'configs', value: parsed.length > 0 ? parsed.map((config) => config.agent).join(', ') : 'none' },
804
- ...(mismatches.length > 0 ? [{ label: 'mismatches', value: mismatches.slice(0, 8).join(', ') }] : []),
805
- ],
806
- remediation: mismatches.length > 0 ? 'Reinstall hooks so agent telemetry points at the active Moda ingest URL.' : undefined,
807
- nextCommand: mismatches.length > 0 ? 'moda hooks install --agent=all' : undefined,
808
- };
809
- }
810
- function agentConfigApiKeyStorageCheck(configs) {
811
- const withStoredKey = configs
812
- .filter((config) => config.parsed)
813
- .filter((config) => telemetryHeader(config).replace(/^Authorization=Bearer\s*/, '').trim().length > 0);
814
- return {
815
- id: 'agent_config_api_key_storage',
816
- group: 'hooks',
817
- title: 'API key storage in agent config',
818
- status: withStoredKey.length > 0 ? 'warning' : 'pass',
819
- severity: withStoredKey.length > 0 ? 'warning' : 'info',
820
- evidence: [{ label: 'configs_with_key', value: withStoredKey.length > 0 ? withStoredKey.map((config) => config.agent).join(', ') : 'none' }],
821
- remediation: withStoredKey.length > 0
822
- ? 'Agent configs contain OTLP auth headers; ensure file permissions are restricted and rotate keys if exposed.'
823
- : undefined,
824
- };
825
- }
826
- function validationEventPathCheck(ingestUrl) {
827
- return {
828
- id: 'validation_event_path_defined',
829
- group: 'cloud',
830
- title: 'Validation event path is defined',
831
- status: 'pass',
832
- severity: 'info',
833
- evidence: [{ label: 'path', value: `${ingestUrl.replace(/\/+$/, '')}/v1/otel/validation` }],
834
- };
835
- }
836
- function telemetryEnv(config) {
837
- return isRecord(config.parsed?.env) ? config.parsed.env : {};
838
- }
839
- function telemetryHeader(config) {
840
- const env = telemetryEnv(config);
841
- return typeof env.OTEL_EXPORTER_OTLP_HEADERS === 'string'
842
- ? env.OTEL_EXPORTER_OTLP_HEADERS
843
- : '';
844
- }
845
- function relativeHomePath(home) {
846
- return (path) => path.startsWith(home) ? `~/${path.slice(home.length).replace(/^\/+/, '')}` : path;
847
- }
848
- function offlineCloudChecks(isOffline) {
849
- const status = isOffline ? 'skipped' : 'warning';
850
- const evidence = [{ label: 'mode', value: isOffline ? 'offline' : 'missing api key' }];
851
- const remediation = isOffline
852
- ? 'Rerun without --offline to check Moda Cloud.'
853
- : 'Configure an API key before checking cloud data flow.';
854
- const nextCommand = isOffline ? 'moda doctor --online' : 'moda init';
855
- return [
856
- {
857
- id: 'cloud_api_reachable',
858
- group: 'cloud',
859
- title: 'Moda Cloud API is reachable',
860
- status,
861
- severity: 'warning',
862
- evidence,
863
- remediation,
864
- nextCommand,
865
- },
866
- {
867
- id: 'ingest_endpoint_reachable',
868
- group: 'cloud',
869
- title: 'Moda ingest endpoint is reachable',
870
- status,
871
- severity: 'warning',
872
- evidence,
873
- remediation,
874
- nextCommand,
875
- },
876
- {
877
- id: 'recent_data_flow',
878
- group: 'cloud',
879
- title: 'Recent traces/logs/runs exist',
880
- status,
881
- severity: 'warning',
882
- evidence,
883
- remediation,
884
- nextCommand,
885
- },
886
- {
887
- id: 'cloud_data_flow',
888
- group: 'cloud',
889
- title: 'Moda Cloud data flow',
890
- status,
891
- severity: 'warning',
892
- evidence,
893
- remediation,
894
- nextCommand,
895
- },
896
- ];
897
- }
898
- async function cloudDataFlowChecks(ingestUrl, apiKey) {
899
- let overview;
900
- let cloudApiCheck;
901
- try {
902
- overview = await callDataAPI('/overview?days_back=1');
903
- cloudApiCheck = {
904
- id: 'cloud_api_reachable',
905
- group: 'cloud',
906
- title: 'Moda Cloud API is reachable',
907
- status: 'pass',
908
- severity: 'info',
909
- evidence: [{ label: 'overview', value: 'reachable' }],
910
- };
911
- }
912
- catch (err) {
913
- const error = err instanceof Error ? err.message : String(err);
914
- return [
915
- {
916
- id: 'cloud_api_reachable',
917
- group: 'cloud',
918
- title: 'Moda Cloud API is reachable',
919
- status: 'warning',
920
- severity: 'warning',
921
- evidence: [{ label: 'error', value: error }],
922
- remediation: 'Check API key and base URL.',
923
- nextCommand: 'moda doctor --offline',
924
- },
925
- {
926
- id: 'ingest_endpoint_reachable',
927
- group: 'cloud',
928
- title: 'Moda ingest endpoint is reachable',
929
- status: 'skipped',
930
- severity: 'warning',
931
- evidence: [{ label: 'reason', value: 'cloud API unreachable' }],
932
- },
933
- {
934
- id: 'recent_data_flow',
935
- group: 'cloud',
936
- title: 'Recent traces/logs/runs exist',
937
- status: 'skipped',
938
- severity: 'warning',
939
- evidence: [{ label: 'reason', value: 'overview unavailable' }],
940
- },
941
- {
942
- id: 'cloud_data_flow',
943
- group: 'cloud',
944
- title: 'Moda Cloud data flow',
945
- status: 'warning',
946
- severity: 'warning',
947
- evidence: [{ label: 'error', value: error }],
948
- remediation: 'Check API key, base URL, and whether data has reached Moda.',
949
- nextCommand: 'moda doctor --offline',
950
- },
951
- ];
952
- }
953
- const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
954
- const recentCheck = recentDataFlowCheck(overview);
955
- const cloudStatus = cloudApiCheck.status === 'pass' &&
956
- ingestCheck.status === 'pass' &&
957
- recentCheck.status === 'pass'
958
- ? 'pass'
959
- : 'warning';
960
- return [
961
- cloudApiCheck,
962
- ingestCheck,
963
- recentCheck,
964
- {
965
- id: 'cloud_data_flow',
966
- group: 'cloud',
967
- title: 'Moda Cloud data flow',
968
- status: cloudStatus,
969
- severity: cloudStatus === 'pass' ? 'info' : 'warning',
970
- evidence: [
971
- { label: 'cloud_api', value: cloudApiCheck.status },
972
- { label: 'ingest', value: ingestCheck.status },
973
- { label: 'recent_data', value: recentCheck.status },
974
- ],
975
- remediation: cloudStatus === 'pass' ? undefined : 'Check API key, ingest URL, and whether data has reached Moda.',
976
- nextCommand: cloudStatus === 'pass' ? undefined : 'moda doctor --offline',
977
- },
978
- ];
979
- }
980
- async function ingestReachabilityCheck(ingestUrl, apiKey) {
981
- const url = `${ingestUrl.replace(/\/+$/, '')}/`;
982
- try {
983
- const response = await fetch(url, {
984
- method: 'GET',
985
- headers: { Authorization: `Bearer ${apiKey}` },
986
- signal: AbortSignal.timeout(10_000),
987
- });
988
- return {
989
- id: 'ingest_endpoint_reachable',
990
- group: 'cloud',
991
- title: 'Moda ingest endpoint is reachable',
992
- status: 'pass',
993
- severity: 'info',
994
- evidence: [
995
- { label: 'url', value: url },
996
- { label: 'http_status', value: String(response.status) },
997
- ],
998
- };
999
- }
1000
- catch (err) {
1001
- return {
1002
- id: 'ingest_endpoint_reachable',
1003
- group: 'cloud',
1004
- title: 'Moda ingest endpoint is reachable',
1005
- status: 'warning',
1006
- severity: 'warning',
1007
- evidence: [{ label: 'error', value: err instanceof Error ? err.message : String(err) }],
1008
- remediation: 'Check the configured ingest URL and network reachability.',
1009
- nextCommand: 'moda profiles create',
1010
- };
1011
- }
1012
- }
1013
- function recentDataFlowCheck(overview) {
1014
- const data = isRecord(overview) ? overview : {};
1015
- const conversations = numberAt(data, ['conversations', 'total']);
1016
- const toolFailures = numberAt(data, ['tool_failures', 'total']);
1017
- const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
1018
- const lastEventAt = lastActivityTimestamp(recentActivity);
1019
- const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
1020
- return {
1021
- id: 'recent_data_flow',
1022
- group: 'cloud',
1023
- title: 'Recent traces/logs/runs exist',
1024
- status: hasData ? 'pass' : 'warning',
1025
- severity: hasData ? 'info' : 'warning',
1026
- evidence: [
1027
- { label: 'conversations', value: String(conversations) },
1028
- { label: 'tool_failures', value: String(toolFailures) },
1029
- { label: 'last_event_at', value: lastEventAt ?? 'none' },
1030
- ],
1031
- remediation: hasData ? undefined : 'No recent Moda data was visible in the overview window.',
1032
- nextCommand: hasData ? undefined : 'moda hooks install --agent=all',
1033
- };
1034
- }
1035
- function numberAt(value, path) {
1036
- let cursor = value;
1037
- for (const key of path) {
1038
- if (!isRecord(cursor))
1039
- return 0;
1040
- cursor = cursor[key];
1041
- }
1042
- return typeof cursor === 'number' && Number.isFinite(cursor) ? cursor : 0;
1043
- }
1044
- function lastActivityTimestamp(items) {
1045
- const timestamps = items
1046
- .map((item) => isRecord(item) && typeof item.timestamp === 'string' ? item.timestamp : undefined)
1047
- .filter((item) => Boolean(item))
1048
- .sort();
1049
- return timestamps.at(-1);
1050
- }
1051
- function summarizeChecks(checks) {
1052
- const fail = checks.filter((check) => check.status === 'fail').length;
1053
- const warning = checks.filter((check) => check.status === 'warning').length;
1054
- const skipped = checks.filter((check) => check.status === 'skipped').length;
1055
- const pass = checks.filter((check) => check.status === 'pass').length;
1056
- return {
1057
- status: fail > 0 ? 'fail' : warning > 0 ? 'warning' : 'pass',
1058
- pass,
1059
- warning,
1060
- fail,
1061
- skipped,
1062
- };
1063
- }
1064
- function nextCommandsForChecks(checks) {
1065
- return [...new Set(checks
1066
- .filter((check) => check.status === 'fail' || check.status === 'warning')
1067
- .map((check) => check.nextCommand)
1068
- .filter((command) => Boolean(command)))];
1069
- }
1070
- function firstBrokenCheck(checks) {
1071
- return checks.find((check) => check.status === 'fail') ??
1072
- checks.find((check) => check.status === 'warning');
1073
- }
1074
- function checkStatus(checks, id) {
1075
- return checks.find((check) => check.id === id)?.status ?? 'skipped';
1076
- }
1077
- function doctorAgentWriteOptions(report, command) {
1078
- const broken = report.checks.filter((check) => check.status === 'fail' || check.status === 'warning');
1079
- return {
1080
- summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
1081
- findings: broken.slice(0, 16).map((check) => ({
1082
- id: check.id,
1083
- title: check.title,
1084
- status: check.status,
1085
- severity: check.severity,
1086
- description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
1087
- evidence_ref_ids: [`doctor:${check.id}`],
1088
- })),
1089
- evidenceRefs: report.checks.map((check) => ({
1090
- id: `doctor:${check.id}`,
1091
- kind: 'doctor_check',
1092
- label: check.title,
1093
- status: check.status,
1094
- })),
1095
- recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
1096
- id: `doctor_action_${index + 1}_${item.checkId}`,
1097
- title: item.action,
1098
- command: item.command,
1099
- rationale: `Doctor check ${item.checkId} is not passing.`,
1100
- mutability: item.command ? agentMutabilityForCommand(item.command) : 'unknown',
1101
- requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true,
1102
- })),
1103
- nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
1104
- command: nextCommand,
1105
- purpose: agentPurposeForCommand(nextCommand),
1106
- mutability: agentMutabilityForCommand(nextCommand),
1107
- requires_approval: agentCommandRequiresApproval(nextCommand),
1108
- })),
1109
- warnings: broken
1110
- .filter((check) => check.status === 'warning')
1111
- .slice(0, 16)
1112
- .map((check) => `${check.title}: ${check.remediation ?? check.status}`),
1113
- };
1114
- }
1115
- function agentMutabilityForCommand(command) {
1116
- if (/\bsync\b/.test(command))
1117
- return 'sync';
1118
- if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
1119
- return 'write';
1120
- return 'read';
1121
- }
1122
- function agentCommandRequiresApproval(command) {
1123
- return agentMutabilityForCommand(command) !== 'read' && !command.includes('--dry-run');
1124
- }
1125
- function agentPurposeForCommand(command) {
1126
- if (command.includes('doctor'))
1127
- return 'Re-run Moda diagnostics.';
1128
- if (command.includes('harness approve'))
1129
- return 'Approve the validated harness report after review.';
1130
- if (command.includes('harness sync'))
1131
- return 'Sync harness state to Moda Cloud.';
1132
- if (command.includes('harness scan'))
1133
- return 'Refresh the local harness manifest.';
1134
- if (command.includes('init'))
1135
- return 'Run Moda onboarding.';
1136
- return 'Inspect or repair Moda setup.';
1137
- }
1138
- function detectGitRepo(cwd) {
1139
- const toplevel = spawnSync('git', ['rev-parse', '--show-toplevel'], {
1140
- cwd,
1141
- encoding: 'utf8',
1142
- });
1143
- if (toplevel.status !== 0 || !toplevel.stdout.trim())
1144
- return undefined;
1145
- const root = toplevel.stdout.trim();
1146
- const origin = spawnSync('git', ['config', '--get', 'remote.origin.url'], {
1147
- cwd: root,
1148
- encoding: 'utf8',
1149
- });
1150
- return {
1151
- toplevel: root,
1152
- originUrl: origin.status === 0 ? origin.stdout.trim() : '',
1153
- };
1154
- }
1155
- function stableGraphHash(graph) {
1156
- return createHash('sha256')
1157
- .update(JSON.stringify(sortJson({
1158
- ...graph,
1159
- scan: {
1160
- ...graph.scan,
1161
- createdAt: undefined,
1162
- scannerVersion: undefined,
1163
- skipped: undefined,
1164
- },
1165
- })))
1166
- .digest('hex');
1167
- }
1168
- function sortJson(value) {
1169
- if (Array.isArray(value))
1170
- return value.map((item) => sortJson(item));
1171
- if (value && typeof value === 'object') {
1172
- return Object.fromEntries(Object.entries(value)
1173
- .filter(([, item]) => item !== undefined)
1174
- .sort(([a], [b]) => a.localeCompare(b))
1175
- .map(([key, item]) => [key, sortJson(item)]));
1176
- }
1177
- return value;
1178
- }
1179
- function isRecord(value) {
1180
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1181
- }
1182
- //# sourceMappingURL=doctor.js.map