@moda-ai/cli 1.55.42 → 1.55.44

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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/{agent-86hghww1.js → agent-5ax7fqhe.js} +9 -8
  3. package/dist/{cli-daqkdnm1.js → cli-4pc0a18x.js} +1 -1
  4. package/dist/{cli-1z1sx8dg.js → cli-5aa48ry5.js} +5 -4
  5. package/dist/{cli-xddtv3yn.js → cli-8pcq2et6.js} +2 -40
  6. package/dist/{cli-hjm9151g.js → cli-8pksf2x7.js} +5 -5
  7. package/dist/{cli-vwpbspr5.js → cli-d4cdczhe.js} +5 -5
  8. package/dist/{cli-0xc5htv7.js → cli-dmm9jm9m.js} +1 -1
  9. package/dist/cli-e0kbye4s.js +39 -0
  10. package/dist/{cli-t0pb7qbs.js → cli-e24fjyrb.js} +118 -10
  11. package/dist/{cli-gt3yq1y5.js → cli-k2ka2kjb.js} +184 -2
  12. package/dist/{cli-pmzh2qjy.js → cli-sejqvvhv.js} +14 -6
  13. package/dist/{cli-srwzar82.js → cli-t4xfvrry.js} +34 -15
  14. package/dist/{cli-4sy4tytm.js → cli-tztzrh6b.js} +19 -5
  15. package/dist/{cli-he8csgsr.js → cli-wwbr5caw.js} +8 -8
  16. package/dist/{cli-77ekrx7d.js → cli-xcpwp0gb.js} +14 -7
  17. package/dist/{cli-mxfyq5nq.js → cli-xx0pjzkh.js} +4 -4
  18. package/dist/cli.js +82 -41
  19. package/dist/{doctor-c05njdv8.js → doctor-k4tdgvgv.js} +8 -7
  20. package/dist/{fix-aghp304m.js → fix-nk5xffz8.js} +4 -4
  21. package/dist/{harness-vabp33ak.js → harness-avynbq4f.js} +6 -5
  22. package/dist/{harness-github-actions-112503xs.js → harness-github-actions-gd6vyxb0.js} +6 -5
  23. package/dist/{index-dtxvvcq2.js → index-63k2a6gb.js} +12 -11
  24. package/dist/{production-intelligence-e5vsc6cw.js → production-intelligence-sv3qx7f4.js} +24 -7
  25. package/dist/{profile-commands-se9947c1.js → profile-commands-mh3kfqkn.js} +98 -20
  26. package/dist/{prompt-attribution-pbns1ya3.js → prompt-attribution-wvvnzw4m.js} +6 -5
  27. package/dist/prompt-source-mcp.js +6 -5
  28. package/dist/{prompts-ab-f5qrvpkt.js → prompts-ab-y7egdzg4.js} +8 -7
  29. package/dist/{prompts-propose-jg2j41wx.js → prompts-propose-5nw25v9a.js} +8 -7
  30. package/dist/{prompts-qppk0mhp.js → prompts-snnbyxmz.js} +6 -5
  31. package/dist/{provision-cya5rpxa.js → provision-t1dpzcjp.js} +6 -5
  32. package/dist/{registry-8a1940sr.js → registry-8zkxefjs.js} +3 -3
  33. package/dist/{skills-sh93b156.js → skills-d4215fa9.js} +6 -5
  34. package/dist/{sync-0pp1c8hs.js → sync-17f9yxb1.js} +8 -7
  35. package/package.json +1 -1
  36. package/skills/integration/index.json +2 -2
  37. package/skills/moda-cli/SKILL.md +6 -3
  38. package/skills/moda-cli/references/auth.md +16 -7
  39. package/skills/moda-cli/references/commands.md +4 -2
  40. package/skills/moda-cli/references/dashboard-parity.md +3 -0
  41. package/skills/moda-cli/references/detections.md +20 -0
  42. package/skills/moda-cli/references/output-protocol.md +1 -1
  43. package/skills/moda-cli/references/production-intelligence.md +48 -0
  44. package/skills/moda-cli/references/search-and-traces.md +3 -1
  45. package/skills/moda-cli/references/troubleshooting.md +14 -0
package/README.md CHANGED
@@ -416,7 +416,7 @@ Both commands accept two flags to inline a message window per row:
416
416
  | Variable | Required | Default | Description |
417
417
  |----------|----------|---------|-------------|
418
418
  | `MODA_API_KEY` | Yes | -- | Your Moda API key (`moda_sk_...`) |
419
- | `MODA_BASE_URL` | No | `https://moda.dev` | Base URL for the Data API |
419
+ | `MODA_BASE_URL` | No | `https://moda.dev` | Base URL for the Data API. A loopback URL needs a non-prod profile (`MODA_PROFILE=local`); on a prod profile commands exit 5 before sending anything |
420
420
 
421
421
  ## Documentation
422
422
 
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  buildDoctorReport
3
- } from "./cli-srwzar82.js";
4
- import"./cli-pmzh2qjy.js";
5
- import"./cli-he8csgsr.js";
3
+ } from "./cli-t4xfvrry.js";
4
+ import"./cli-sejqvvhv.js";
5
+ import"./cli-wwbr5caw.js";
6
6
  import"./cli-mm48br00.js";
7
7
  import {
8
8
  readHarnessSyncState
@@ -15,15 +15,16 @@ import {
15
15
  readHarnessReport,
16
16
  validateHarnessReport
17
17
  } from "./cli-cesds766.js";
18
- import"./cli-xddtv3yn.js";
19
- import"./cli-4sy4tytm.js";
20
- import"./cli-0xc5htv7.js";
18
+ import"./cli-8pcq2et6.js";
19
+ import"./cli-e0kbye4s.js";
20
+ import"./cli-tztzrh6b.js";
21
+ import"./cli-dmm9jm9m.js";
21
22
  import {
22
23
  resolveApiKey,
23
24
  resolveIngestUrl,
24
25
  resolveModaBaseUrl,
25
26
  resolveTenantId
26
- } from "./cli-t0pb7qbs.js";
27
+ } from "./cli-e24fjyrb.js";
27
28
  import {
28
29
  CliInputError
29
30
  } from "./cli-k4tqwah4.js";
@@ -94,7 +95,7 @@ async function buildAgentContext(context) {
94
95
  const graph = readHarnessGraphIfPresent(context.cwd);
95
96
  const reportState = readHarnessReportState(context.cwd);
96
97
  const syncState = readHarnessSyncState(context.cwd);
97
- const harness = graph ? await import("./harness-vabp33ak.js") : undefined;
98
+ const harness = graph ? await import("./harness-avynbq4f.js") : undefined;
98
99
  const topFindings = doctor.checks.filter((check) => check.status === "fail" || check.status === "warning").slice(0, 12).map(checkToFinding);
99
100
  const evidenceRefs = [
100
101
  ...doctor.checks.map(checkToEvidenceRef),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  callControlAPI
3
- } from "./cli-0xc5htv7.js";
3
+ } from "./cli-dmm9jm9m.js";
4
4
 
5
5
  // src/replay-runs.ts
6
6
  var DEFAULT_POLL_INTERVAL_MS = 15000;
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  fetchApiKeyProbe
3
- } from "./cli-77ekrx7d.js";
3
+ } from "./cli-xcpwp0gb.js";
4
4
  import {
5
5
  authFetch,
6
6
  getCachedLoginContext
7
- } from "./cli-pmzh2qjy.js";
7
+ } from "./cli-sejqvvhv.js";
8
8
  import {
9
9
  embeddedTenantIdFromApiKey,
10
10
  getModaDir,
@@ -15,7 +15,7 @@ import {
15
15
  resolveTenantIdFromActiveProfile,
16
16
  saveCliConfig,
17
17
  writeSecretJsonFile
18
- } from "./cli-t0pb7qbs.js";
18
+ } from "./cli-e24fjyrb.js";
19
19
  import {
20
20
  CliAccessError,
21
21
  CliInputError,
@@ -118,8 +118,9 @@ function bootstrapLabel(custom) {
118
118
  }
119
119
  var embeddedTenantId = embeddedTenantIdFromApiKey;
120
120
  async function isStoredKeyLive(key, options = {}) {
121
+ const baseUrl = resolveModaBaseUrl(options);
121
122
  try {
122
- const response = await fetchApiKeyProbe(resolveModaBaseUrl(options), { "x-api-key": key }, VALIDATE_TIMEOUT_MS);
123
+ const response = await fetchApiKeyProbe(baseUrl, { "x-api-key": key }, VALIDATE_TIMEOUT_MS);
123
124
  if (response.status !== 200)
124
125
  return false;
125
126
  await response.json();
@@ -1,45 +1,7 @@
1
1
  import {
2
2
  createCliOutput,
3
3
  outputModeFromFlags
4
- } from "./cli-t0pb7qbs.js";
5
-
6
- // src/boolean-words.ts
7
- var BOOLEAN_TRUE_WORDS = new Set(["true", "yes", "1", "on"]);
8
- var BOOLEAN_FALSE_WORDS = new Set(["false", "no", "0", "off"]);
9
- function parseBooleanFlagValue(value) {
10
- if (typeof value === "boolean")
11
- return value;
12
- if (typeof value === "number")
13
- return value === 1 ? true : value === 0 ? false : undefined;
14
- if (typeof value !== "string")
15
- return;
16
- const text = value.trim().toLowerCase();
17
- if (BOOLEAN_TRUE_WORDS.has(text))
18
- return true;
19
- if (BOOLEAN_FALSE_WORDS.has(text))
20
- return false;
21
- return;
22
- }
23
- function flagValue(flags, name) {
24
- return flags[name] ?? flags[name.replace(/-/g, "_")];
25
- }
26
- function withoutFlag(flags, name) {
27
- const snake = name.replace(/-/g, "_");
28
- return Object.fromEntries(Object.entries(flags).filter(([key]) => key !== name && key !== snake));
29
- }
30
- function optOutFlagSet(flags, name, strict = true) {
31
- const raw = flagValue(flags, name);
32
- if (raw === undefined)
33
- return false;
34
- const value = parseBooleanFlagValue(raw);
35
- return strict ? value === true : value !== false;
36
- }
37
- function envSwitchedOff(value) {
38
- return parseBooleanFlagValue(value) === false;
39
- }
40
- function envOptOutSet(value) {
41
- return value !== undefined && value.trim() !== "" && parseBooleanFlagValue(value) !== false;
42
- }
4
+ } from "./cli-e24fjyrb.js";
43
5
 
44
6
  // src/command-context.ts
45
7
  function flagEnabled(flags, key) {
@@ -99,4 +61,4 @@ function createCommandContext(input, options = {}) {
99
61
  };
100
62
  }
101
63
 
102
- export { parseBooleanFlagValue, withoutFlag, optOutFlagSet, envSwitchedOff, envOptOutSet, stringOption, createCommandContext };
64
+ export { stringOption, createCommandContext };
@@ -4,19 +4,19 @@ import {
4
4
  parsePromptFileForAb,
5
5
  readPromptLock,
6
6
  runPromptSync
7
- } from "./cli-he8csgsr.js";
7
+ } from "./cli-wwbr5caw.js";
8
8
  import {
9
9
  DEFAULT_POLL_INTERVAL_MS,
10
10
  DEFAULT_WAIT_TIMEOUT_MS,
11
11
  fetchLatestRun
12
- } from "./cli-daqkdnm1.js";
12
+ } from "./cli-4pc0a18x.js";
13
13
  import {
14
14
  UUID_RE
15
- } from "./cli-4sy4tytm.js";
15
+ } from "./cli-tztzrh6b.js";
16
16
  import {
17
17
  callControlAPI,
18
18
  callDataAPI
19
- } from "./cli-0xc5htv7.js";
19
+ } from "./cli-dmm9jm9m.js";
20
20
  import {
21
21
  EXIT_ERROR,
22
22
  EXIT_OK,
@@ -24,7 +24,7 @@ import {
24
24
  setPlatformTenantOverride,
25
25
  shellArg,
26
26
  validateConfig
27
- } from "./cli-t0pb7qbs.js";
27
+ } from "./cli-e24fjyrb.js";
28
28
  import {
29
29
  CliInputError
30
30
  } from "./cli-k4tqwah4.js";
@@ -44,16 +44,16 @@ import {
44
44
  import {
45
45
  createCommandContext,
46
46
  stringOption
47
- } from "./cli-xddtv3yn.js";
47
+ } from "./cli-8pcq2et6.js";
48
48
  import {
49
49
  assertFlags,
50
50
  assertPositionals,
51
51
  requireTenant
52
- } from "./cli-4sy4tytm.js";
52
+ } from "./cli-tztzrh6b.js";
53
53
  import {
54
54
  ApiError,
55
55
  callControlAPI
56
- } from "./cli-0xc5htv7.js";
56
+ } from "./cli-dmm9jm9m.js";
57
57
  import {
58
58
  embeddedTenantIdFromApiKey,
59
59
  getActiveProfile,
@@ -63,7 +63,7 @@ import {
63
63
  resolveModaBaseUrl,
64
64
  resolveTenantId,
65
65
  shellArg
66
- } from "./cli-t0pb7qbs.js";
66
+ } from "./cli-e24fjyrb.js";
67
67
  import {
68
68
  CliInputError,
69
69
  CliInputRequiredError
@@ -14048,7 +14048,7 @@ async function runHarnessCommand(context) {
14048
14048
  throw new CliInputError("Focused remote prompt analysis is not supported yet.", "Use local --analyst=claude|codex|cursor|local-scan, or drop --concern for a full remote report.");
14049
14049
  }
14050
14050
  if (context.flags["github-actions"] === "true") {
14051
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-112503xs.js");
14051
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-gd6vyxb0.js");
14052
14052
  const result = await runGithubActionsAnalyze(rootDir, {
14053
14053
  writeReport: (report2) => {
14054
14054
  const normalized = normalizeHarnessReport(report2);
@@ -3,7 +3,7 @@ import {
3
3
  recordApiRequest,
4
4
  resolveApiKey,
5
5
  resolveModaBaseUrl
6
- } from "./cli-t0pb7qbs.js";
6
+ } from "./cli-e24fjyrb.js";
7
7
 
8
8
  // src/api-client.ts
9
9
  var REQUEST_TIMEOUT_MS = 30000;
@@ -0,0 +1,39 @@
1
+ // src/boolean-words.ts
2
+ var BOOLEAN_TRUE_WORDS = new Set(["true", "yes", "1", "on"]);
3
+ var BOOLEAN_FALSE_WORDS = new Set(["false", "no", "0", "off"]);
4
+ function parseBooleanFlagValue(value) {
5
+ if (typeof value === "boolean")
6
+ return value;
7
+ if (typeof value === "number")
8
+ return value === 1 ? true : value === 0 ? false : undefined;
9
+ if (typeof value !== "string")
10
+ return;
11
+ const text = value.trim().toLowerCase();
12
+ if (BOOLEAN_TRUE_WORDS.has(text))
13
+ return true;
14
+ if (BOOLEAN_FALSE_WORDS.has(text))
15
+ return false;
16
+ return;
17
+ }
18
+ function flagValue(flags, name) {
19
+ return flags[name] ?? flags[name.replace(/-/g, "_")];
20
+ }
21
+ function withoutFlag(flags, name) {
22
+ const snake = name.replace(/-/g, "_");
23
+ return Object.fromEntries(Object.entries(flags).filter(([key]) => key !== name && key !== snake));
24
+ }
25
+ function optOutFlagSet(flags, name, strict = true) {
26
+ const raw = flagValue(flags, name);
27
+ if (raw === undefined)
28
+ return false;
29
+ const value = parseBooleanFlagValue(raw);
30
+ return strict ? value === true : value !== false;
31
+ }
32
+ function envSwitchedOff(value) {
33
+ return parseBooleanFlagValue(value) === false;
34
+ }
35
+ function envOptOutSet(value) {
36
+ return value !== undefined && value.trim() !== "" && parseBooleanFlagValue(value) !== false;
37
+ }
38
+
39
+ export { parseBooleanFlagValue, withoutFlag, optOutFlagSet, envSwitchedOff, envOptOutSet };
@@ -111,8 +111,14 @@ function createDefaultProfiles(options = {}) {
111
111
  };
112
112
  }
113
113
  function isLoopbackHostname(hostname) {
114
- const host = hostname.toLowerCase();
115
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
114
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
115
+ if (host === "localhost" || host.endsWith(".localhost"))
116
+ return true;
117
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || host === "0.0.0.0")
118
+ return true;
119
+ if (host === "::1" || host === "::")
120
+ return true;
121
+ return /^::ffff:(7f[0-9a-f]{2}:[0-9a-f]{1,4}|127\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.test(host);
116
122
  }
117
123
  function isLoopbackBaseUrl(url) {
118
124
  try {
@@ -287,16 +293,33 @@ function profileBaseUrlOrigin(name, options = {}) {
287
293
  return sanitizeProfileBaseUrl(url, environment) === normalizeModaBaseUrl(url) ? "profile" : "default";
288
294
  }
289
295
  function describeBaseUrlSource(options) {
290
- const envBase = optionalString(getEnv(options).MODA_BASE_URL);
291
- if (envBase) {
292
- const environment = activeProfileEnvironment(options);
293
- if (!environmentAllowsLoopback(environment) && isLoopbackBaseUrl(normalizeModaBaseUrl(envBase))) {
294
- return `default (env:MODA_BASE_URL loopback ignored for ${environment} profile)`;
295
- }
296
- return "env:MODA_BASE_URL";
296
+ const refused = refusedLoopbackBaseUrl(options);
297
+ if (refused) {
298
+ return `refused (env:MODA_BASE_URL is loopback and the active profile '${refused.profile}' is ${refused.environment})`;
297
299
  }
300
+ if (optionalString(getEnv(options).MODA_BASE_URL))
301
+ return "env:MODA_BASE_URL";
298
302
  return profileBaseUrlOrigin(options.profile, options);
299
303
  }
304
+ function refusedLoopbackBaseUrl(options = {}) {
305
+ const envBase = optionalString(getEnv(options).MODA_BASE_URL);
306
+ if (!envBase)
307
+ return;
308
+ const url = normalizeModaBaseUrl(envBase);
309
+ if (!isLoopbackBaseUrl(url))
310
+ return;
311
+ let profile;
312
+ let environment;
313
+ try {
314
+ const active = getActiveProfile(options);
315
+ profile = active.name;
316
+ environment = active.environment;
317
+ } catch {
318
+ profile = optionalString(options.profile) ?? optionalString(getEnv(options).MODA_PROFILE) ?? DEFAULT_PROFILE_NAME;
319
+ environment = "prod";
320
+ }
321
+ return environmentAllowsLoopback(environment) ? undefined : { url, profile, environment };
322
+ }
300
323
  function loadProfileConfig(options = {}) {
301
324
  const path = getModaPaths(options).profileConfigPath;
302
325
  const parsed = safeReadRecord(path);
@@ -471,6 +494,8 @@ function resolveBaseUrlStrict(options = {}) {
471
494
  } catch {
472
495
  return;
473
496
  }
497
+ if (refusedLoopbackBaseUrl(options))
498
+ return;
474
499
  return resolveBaseUrl(options);
475
500
  }
476
501
  function resolveIngestUrl(options = {}) {
@@ -600,8 +625,29 @@ function resolveApiTenantId(options = {}) {
600
625
  return (apiKey ? embeddedTenantIdFromApiKey(apiKey) : undefined) || resolveTenantId(options);
601
626
  }
602
627
  function resolveModaBaseUrl(options = {}) {
628
+ const refused = refusedLoopbackBaseUrl(options);
629
+ if (refused)
630
+ throw refusedBaseUrlError(refused);
603
631
  return resolveBaseUrl(options) || PROD_BASE_URL;
604
632
  }
633
+ function describeModaBaseUrl(options = {}) {
634
+ const refused = refusedLoopbackBaseUrl(options);
635
+ return refused ? { url: refused.url, refused } : { url: resolveBaseUrl(options) || PROD_BASE_URL };
636
+ }
637
+ function refusedBaseUrlMessage(refused) {
638
+ return `MODA_BASE_URL=${refused.url} is a loopback URL, and the active profile '${refused.profile}' is a ${refused.environment} profile. ` + "The CLI does not send a prod profile's credentials to a loopback server, and does not send requests meant for it to production instead.";
639
+ }
640
+ function refusedBaseUrlError(refused) {
641
+ return new CliInputRequiredError(`${refusedBaseUrlMessage(refused)} Nothing was sent.`, {
642
+ id: "moda_base_url_refused",
643
+ question: `Target the local stack at ${refused.url} with the local profile, or production with MODA_BASE_URL unset?`,
644
+ kind: "select",
645
+ choices: [
646
+ { value: "local", label: `Use the local profile for ${refused.url}`, resume_flags: ["--profile=local"] },
647
+ { value: "production", label: "Use production: unset MODA_BASE_URL and re-run unchanged", resume_flags: [] }
648
+ ]
649
+ }, "Re-run with --profile=local (or MODA_PROFILE=local) to use the local stack, or unset MODA_BASE_URL to use production.");
650
+ }
605
651
  function resolveModaBaseUrlStrict(options = {}) {
606
652
  return resolveBaseUrlStrict(options);
607
653
  }
@@ -660,6 +706,64 @@ function shellArg(value) {
660
706
  return `'${value.replace(/'/g, `'\\''`)}'`;
661
707
  }
662
708
 
709
+ // src/api-access-errors.ts
710
+ var MESSAGE_PATTERNS = [
711
+ [/unknown or deleted tenant|does not exist or is not accessible/i, "tenant_not_found"],
712
+ [/pinned to another tenant|cannot select another/i, "tenant_not_accessible"],
713
+ [/does not match the tenant in the url/i, "tenant_mismatch"],
714
+ [/api key is read-only/i, "read_only_key"]
715
+ ];
716
+ var REASONS = new Set(MESSAGE_PATTERNS.map(([, reason]) => reason));
717
+ function apiForbiddenReason(code, message) {
718
+ if (code && REASONS.has(code))
719
+ return code;
720
+ if (!message)
721
+ return;
722
+ return MESSAGE_PATTERNS.find(([pattern]) => pattern.test(message))?.[1];
723
+ }
724
+ function errorBodyFields(body) {
725
+ if (typeof body !== "string" || !body.trimStart().startsWith("{"))
726
+ return {};
727
+ try {
728
+ const parsed = JSON.parse(body);
729
+ return {
730
+ ...typeof parsed.code === "string" ? { code: parsed.code } : {},
731
+ ...typeof parsed.message === "string" ? { message: parsed.message } : {}
732
+ };
733
+ } catch {
734
+ return {};
735
+ }
736
+ }
737
+ function apiForbiddenError(reason, serverMessage) {
738
+ const server = serverMessage?.trim().replace(/\.$/, "");
739
+ switch (reason) {
740
+ case "tenant_not_found":
741
+ return {
742
+ code: "not_found",
743
+ message: server && /does not exist/i.test(server) ? `${server} (HTTP 403).` : "The tenant this request names does not exist or is not accessible to this API key (HTTP 403).",
744
+ cause: "The tenant id comes from MODA_TENANT_ID (or --tenant-id). No tenant has that id, or it was deleted. " + "Set it to a tenant id from the dashboard URL or from `moda auth whoami` with a browser session, then retry."
745
+ };
746
+ case "tenant_not_accessible":
747
+ return {
748
+ code: "access_denied",
749
+ message: `The Moda API refused this request (HTTP 403): ${server ?? "this API key cannot act on the tenant requested"}.`,
750
+ cause: "This key acts on one tenant only. Unset MODA_TENANT_ID (and drop --tenant-id) so it acts on its own tenant, " + "or use a key for the tenant you want. With MODA_TENANT_ID unset, `moda auth whoami` shows the key's tenant."
751
+ };
752
+ case "tenant_mismatch":
753
+ return {
754
+ code: "access_denied",
755
+ message: `The Moda API refused this request (HTTP 403): ${server ?? "the tenant header does not match the tenant in the URL"}.`,
756
+ cause: "MODA_TENANT_ID names a different tenant than this command. Make them the same tenant, or unset MODA_TENANT_ID."
757
+ };
758
+ case "read_only_key":
759
+ return {
760
+ code: "access_denied",
761
+ message: `The Moda API refused this request (HTTP 403): ${server ?? "this API key is read-only"}.`,
762
+ cause: "This key is read-only: it can run reads but not writes. Run this command with a full-access key."
763
+ };
764
+ }
765
+ }
766
+
663
767
  // src/legacy-names.ts
664
768
  var LEGACY_NAMES = [
665
769
  { legacy: "conversations", use: "moda traces list", canonical: { command: "traces", subcommand: "list" }, silent: true },
@@ -1613,6 +1717,10 @@ function errorToAgentError(error) {
1613
1717
  };
1614
1718
  }
1615
1719
  if (apiErrorStatus(error) === 403) {
1720
+ const body = errorBodyFields(error.responseBody);
1721
+ const reason = apiForbiddenReason(body.code, body.message);
1722
+ if (reason)
1723
+ return { ...apiForbiddenError(reason, body.message), blocking: true };
1616
1724
  return {
1617
1725
  code: "access_denied",
1618
1726
  message: `The Moda API refused this request (HTTP 403): ${serverErrorMessage(error) ?? "the API key does not allow it"}.`,
@@ -2187,4 +2295,4 @@ function extractSuggestedCommands(text) {
2187
2295
  return commands.length > 0 ? commands : undefined;
2188
2296
  }
2189
2297
 
2190
- export { PROD_BASE_URL, DEFAULT_INGEST_URL, getModaPaths, getModaDir, isLoopbackBaseUrl, describeBaseUrlSource, loadProfileConfig, saveProfileConfig, getActiveProfile, setActiveProfile, upsertProfile, clearStoredApiKeyCredentials, secretFilePathFromRef, resolveSecretReference, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, ensureSecretFilePermissions, writeSecretJsonFile, VERSION, MODA_BASE_URL, loadCliConfig, saveCliConfig, resolveApiKey, embeddedTenantIdFromApiKey, resolveIngestUrl2 as resolveIngestUrl, resolveTenantId, resolveApiTenantId, resolveModaBaseUrl, resolveModaBaseUrlStrict, validateConfig, isPlatformApiKey, setPlatformTenantOverride, apiKeyHeaders, recordApiRequest, getApiRequestCount, resetApiRequestCount, shellArg, LEGACY_NAMES, canonicalCommandText, setAgentUpdateNotice, EXIT_OK, EXIT_ERROR, EXIT_DEGRADED, EXIT_AUTH_REQUIRED, EXIT_INPUT_REQUIRED, EXIT_CANCELLED, AGENT_ERROR_CODES, wasOutputCancelled, safeWriteStdout, safeWriteStderr, explicitOutputMode, outputModeFromFlags, resolveOutputMode, createCliOutput, agentEnvelopeFits, errorToAgentError, agentMaxOutputBytes, continuationCommand };
2298
+ export { PROD_BASE_URL, DEFAULT_INGEST_URL, getModaPaths, getModaDir, isLoopbackBaseUrl, describeBaseUrlSource, loadProfileConfig, saveProfileConfig, getActiveProfile, setActiveProfile, upsertProfile, clearStoredApiKeyCredentials, secretFilePathFromRef, resolveSecretReference, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, ensureSecretFilePermissions, writeSecretJsonFile, VERSION, MODA_BASE_URL, loadCliConfig, saveCliConfig, resolveApiKey, embeddedTenantIdFromApiKey, resolveIngestUrl2 as resolveIngestUrl, resolveTenantId, resolveApiTenantId, resolveModaBaseUrl, describeModaBaseUrl, refusedBaseUrlMessage, resolveModaBaseUrlStrict, validateConfig, isPlatformApiKey, setPlatformTenantOverride, apiKeyHeaders, recordApiRequest, getApiRequestCount, resetApiRequestCount, shellArg, apiForbiddenReason, errorBodyFields, apiForbiddenError, LEGACY_NAMES, canonicalCommandText, setAgentUpdateNotice, EXIT_OK, EXIT_ERROR, EXIT_DEGRADED, EXIT_AUTH_REQUIRED, EXIT_INPUT_REQUIRED, EXIT_CANCELLED, AGENT_ERROR_CODES, wasOutputCancelled, safeWriteStdout, safeWriteStderr, explicitOutputMode, outputModeFromFlags, resolveOutputMode, createCliOutput, agentEnvelopeFits, errorToAgentError, agentMaxOutputBytes, continuationCommand };
@@ -1,6 +1,12 @@
1
+ import {
2
+ parseBooleanFlagValue
3
+ } from "./cli-e0kbye4s.js";
4
+ import {
5
+ callDataAPI
6
+ } from "./cli-dmm9jm9m.js";
1
7
  import {
2
8
  shellArg
3
- } from "./cli-t0pb7qbs.js";
9
+ } from "./cli-e24fjyrb.js";
4
10
  import {
5
11
  CliInputError
6
12
  } from "./cli-k4tqwah4.js";
@@ -533,6 +539,182 @@ function shapeToolCatalogDetail(response, opts) {
533
539
  };
534
540
  }
535
541
 
542
+ // src/trends.ts
543
+ var OVERVIEW_TREND_FLAGS = ["compare", "series", "since", "until", "agent-name", "unit"];
544
+ function flagOn(value) {
545
+ return value !== undefined && parseBooleanFlagValue(value) !== false;
546
+ }
547
+ function overviewTrendsQuery(flags, daysBack) {
548
+ const wanted = OVERVIEW_TREND_FLAGS.some((name) => name === "compare" || name === "series" ? flagOn(flags[name]) : flags[name] !== undefined);
549
+ if (!wanted)
550
+ return null;
551
+ if (flags.raw !== undefined && flagOn(flags.raw)) {
552
+ throw new CliInputError("--raw prints the /overview payload only; it cannot be combined with --compare, --series, --since, --until, --agent-name or --unit.", "Drop --raw to get the briefing with its trends block. Nothing was sent.", "flag_conflict");
553
+ }
554
+ const query = new URLSearchParams;
555
+ if (flags.since !== undefined) {
556
+ if (flags["days-back"] !== undefined) {
557
+ throw new CliInputError("Pass --days-back or --since/--until, not both: --since sets the trends window and the previous window is the same span before it.", "Example: moda overview --compare --since=2026-10-17T14:00:00Z (after the deploy against the same span before it). Nothing was sent.", "flag_conflict");
558
+ }
559
+ query.set("since", flags.since);
560
+ if (flags.until !== undefined)
561
+ query.set("until", flags.until);
562
+ } else if (flags.until !== undefined) {
563
+ throw new CliInputError("--until needs --since: the trends window runs from --since to --until (default now).", "Example: moda overview --compare --since=2026-10-10 --until=2026-10-12. Nothing was sent.", "missing_value:since");
564
+ } else {
565
+ query.set("days_back", String(daysBack));
566
+ }
567
+ if (flags["agent-name"] !== undefined)
568
+ query.set("agent", flags["agent-name"]);
569
+ if (flags.unit !== undefined)
570
+ query.set("unit", flags.unit);
571
+ if (flagOn(flags.series))
572
+ query.set("series", "1");
573
+ return query;
574
+ }
575
+ var TRENDS_TIMEOUT_MS = 90000;
576
+ async function loadTrends(query) {
577
+ const data = await callDataAPI(`/trends?${query.toString()}`, { method: "GET" }, { timeoutMs: TRENDS_TIMEOUT_MS, retries: 0 });
578
+ return asRecord(data) ?? {};
579
+ }
580
+ var HANDLER_FAMILY = {
581
+ emotions: "emotion",
582
+ frustrations: "frustration",
583
+ laziness: "laziness",
584
+ hallucinations: "hallucination",
585
+ "tool-failures": "tool_failure"
586
+ };
587
+ function assertCompareScope(handler, params) {
588
+ const family = HANDLER_FAMILY[handler];
589
+ if (!family)
590
+ throw new CliInputError(`--compare is not available for '${handler}'.`);
591
+ const scoped = ["user_id", "conversation_id", "conversation_ids", "ids_from", "cohort"].filter((key) => params[key] !== undefined);
592
+ if (scoped.length > 0) {
593
+ const names = scoped.map((key) => `--${key.replace(/_/g, "-")}`).join(", ");
594
+ throw new CliInputError(`--compare covers the whole workspace for this family, so it cannot be combined with ${names}.`, `Drop ${names} to compare the family, or drop --compare to keep the scoped read. Nothing was sent.`, "flag_conflict");
595
+ }
596
+ return family;
597
+ }
598
+ async function loadFamilyCompare(handler, params) {
599
+ const family = assertCompareScope(handler, params);
600
+ const query = new URLSearchParams({
601
+ days_back: String(params.days_back ?? 7),
602
+ include: "detectors,rates",
603
+ family: params.family && (family === "emotion" || family === "laziness") ? `${family}:${params.family}` : family
604
+ });
605
+ const trends = await loadTrends(query);
606
+ const window = asRecord(trends.window) ?? {};
607
+ const detectors = asRecord(trends.detectors) ?? {};
608
+ const rates = asRecord(trends.rates) ?? {};
609
+ const rateKey = Object.keys(rates).find((key) => key.endsWith("_pct"));
610
+ return {
611
+ window: { from: window.from, to: window.to, previous_from: window.previous_from, previous_to: window.previous_to },
612
+ kpis: detectors[family] ?? detectors[Object.keys(detectors)[0] ?? ""] ?? null,
613
+ ...rateKey ? { rate: { unit: rates.unit, ...asRecord(rates[rateKey]) } } : {},
614
+ note: "Dashboard KPI tiles for the family in this window against the previous window of equal length " + "(delta = current - previous; delta_pct = relative change). List filters other than the family do not apply here.",
615
+ ...Array.isArray(trends.notes) && trends.notes.length > 0 ? { notes: trends.notes } : {}
616
+ };
617
+ }
618
+ function withSummaryCompare(data, compare) {
619
+ if (!compare)
620
+ return data;
621
+ const record = asRecord(data);
622
+ if (!record)
623
+ return data;
624
+ return { ...record, summary: { ...asRecord(record.summary) ?? {}, compare } };
625
+ }
626
+ function signed(value, suffix = "") {
627
+ if (value === null)
628
+ return "n/a";
629
+ return `${value > 0 ? "+" : ""}${value}${suffix}`;
630
+ }
631
+ function compareText(value, isRate) {
632
+ const c = asRecord(value);
633
+ if (!c)
634
+ return "n/a";
635
+ const current = asNumber(c.current);
636
+ const previous = asNumber(c.previous);
637
+ const unit = isRate ? "%" : "";
638
+ const delta = signed(asNumber(c.delta), isRate ? " pts" : "");
639
+ const pct = asNumber(c.delta_pct);
640
+ return `${current === null ? "n/a" : `${current}${unit}`} (was ${previous === null ? "n/a" : `${previous}${unit}`}; ${delta}${pct === null ? "" : `, ${signed(pct, "%")}`})${c.spike === true ? " SPIKE" : ""}`;
641
+ }
642
+ function renderTrendsHuman(trends) {
643
+ const lines = [];
644
+ const window = asRecord(trends.window) ?? {};
645
+ lines.push(`Trends ${asString(window.from) ?? "?"} to ${asString(window.to) ?? "?"} against ${asString(window.previous_from) ?? "?"} to ${asString(window.previous_to) ?? "?"}`);
646
+ const agent = asRecord(trends.agent);
647
+ if (agent && agent.selected !== "__all__")
648
+ lines.push(` Agent ${String(agent.label ?? agent.selected)}`);
649
+ const sections = [
650
+ ["Overview", asRecord(trends.overview)],
651
+ ["Problems", asRecord(trends.problems)],
652
+ [`Rates per ${asString(asRecord(trends.rates)?.unit) ?? "unit"}`, asRecord(trends.rates)]
653
+ ];
654
+ for (const [title, section] of sections) {
655
+ if (!section)
656
+ continue;
657
+ lines.push(` ${title}`);
658
+ for (const [key, value] of Object.entries(section)) {
659
+ if (!asRecord(value) || !("current" in value))
660
+ continue;
661
+ lines.push(` ${key.padEnd(26)} ${compareText(value, key.endsWith("_pct"))}`);
662
+ }
663
+ }
664
+ const detectors = asRecord(trends.detectors);
665
+ for (const [family, block] of Object.entries(detectors ?? {})) {
666
+ lines.push(` Detector ${family}`);
667
+ for (const [key, value] of Object.entries(asRecord(block) ?? {})) {
668
+ if (!asRecord(value) || !("current" in value))
669
+ continue;
670
+ lines.push(` ${key.padEnd(26)} ${compareText(value, key.endsWith("_pct"))}`);
671
+ }
672
+ }
673
+ const series = asRecord(trends.series);
674
+ if (series) {
675
+ const names = Object.keys(series).filter((key) => key !== "bucket");
676
+ lines.push(` Series per ${asString(series.bucket) ?? "bucket"}: ${names.join(", ")} (use --json for the points)`);
677
+ }
678
+ for (const note of Array.isArray(trends.notes) ? trends.notes : [])
679
+ lines.push(` Note: ${String(note)}`);
680
+ return lines;
681
+ }
682
+ function trendsSummaryText(trends) {
683
+ const overview = asRecord(trends.overview);
684
+ if (!overview)
685
+ return null;
686
+ const parts = [];
687
+ const label = {
688
+ conversations: "traces",
689
+ negative_emotion_rate_pct: "negative emotion",
690
+ tool_failure_rate_pct: "failed tool calls",
691
+ ungrounded_rate_pct: "ungrounded claims"
692
+ };
693
+ for (const [key, name] of Object.entries(label)) {
694
+ const c = asRecord(overview[key]);
695
+ if (!c)
696
+ continue;
697
+ const isRate = key.endsWith("_pct");
698
+ const current = asNumber(c.current);
699
+ if (current === null)
700
+ continue;
701
+ const pct = asNumber(c.delta_pct);
702
+ parts.push(`${name} ${current}${isRate ? "%" : ""} (${isRate ? signed(asNumber(c.delta), " pts") : signed(pct, "%")})`);
703
+ }
704
+ return parts.length > 0 ? `Against the previous window of equal length: ${parts.join(", ")}.` : null;
705
+ }
706
+ function problemTrendSummary(data) {
707
+ const record = asRecord(data) ?? {};
708
+ const days = asNumber(record.days_back) ?? 30;
709
+ const incidents = asRecord(asRecord(record.compare)?.incidents);
710
+ const lastSeen = asString(record.last_seen);
711
+ const seen = lastSeen ? ` Last incident on ${lastSeen}.` : " No incident on record.";
712
+ if (!incidents) {
713
+ return `Incidents per day for the last ${days} day(s); no previous window to compare (the history keeps 90 days).${seen}`;
714
+ }
715
+ return `Incidents in the last ${days} day(s): ${compareText(incidents, false)} against the ${days} day(s) before.${seen}`;
716
+ }
717
+
536
718
  // src/intelligence-schema.ts
537
719
  var INTELLIGENCE_SCHEMA_VERSION = "moda.intelligence.v1";
538
720
  var provenanceSchema = { type: "string", enum: ["measured", "inferred"] };
@@ -676,4 +858,4 @@ var MODA_INTELLIGENCE_V1_JSON_SCHEMA = {
676
858
  }
677
859
  };
678
860
 
679
- export { BATCH_MAX_TARGETS, BATCH_CONCURRENCY, parseBatchTarget, parseTargetLines, readIdsFrom, collectBatchTargets, mapWithConcurrency, contextTarget, asRecord, asString, asNumber, frustrationAnchorIndex, frustrationsFromEmotions, withFrustrationAnchors, trimToolFailureConversations, withToolFailureAnchors, embeddedWindowSize, withoutDetectionWindows, compactProblemDossier, toolFailureExamplesCommand, toolCatalogGetCommand, shapeToolCatalogDetail, INTELLIGENCE_SCHEMA_VERSION, MODA_INTELLIGENCE_V1_JSON_SCHEMA };
861
+ export { BATCH_MAX_TARGETS, BATCH_CONCURRENCY, parseBatchTarget, parseTargetLines, readIdsFrom, collectBatchTargets, mapWithConcurrency, contextTarget, asRecord, asString, asNumber, frustrationAnchorIndex, frustrationsFromEmotions, withFrustrationAnchors, trimToolFailureConversations, withToolFailureAnchors, embeddedWindowSize, withoutDetectionWindows, compactProblemDossier, toolFailureExamplesCommand, toolCatalogGetCommand, shapeToolCatalogDetail, overviewTrendsQuery, loadTrends, assertCompareScope, loadFamilyCompare, withSummaryCompare, renderTrendsHuman, trendsSummaryText, problemTrendSummary, INTELLIGENCE_SCHEMA_VERSION, MODA_INTELLIGENCE_V1_JSON_SCHEMA };