@knowttl/mg-axi 0.1.1 → 0.1.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 (40) hide show
  1. package/README.md +149 -38
  2. package/dist/api.js +134 -8
  3. package/dist/bin/mg-axi-hook.js +31 -0
  4. package/dist/catalogue.js +390 -2
  5. package/dist/cli.js +116 -8
  6. package/dist/docs.js +45 -5
  7. package/dist/doctor.js +2 -2
  8. package/dist/entra-access-reviews.js +11 -4
  9. package/dist/entra-apps.js +13 -8
  10. package/dist/entra-audit-logs.js +8 -4
  11. package/dist/entra-auth-methods.js +8 -4
  12. package/dist/entra-certificate-auth.js +12 -8
  13. package/dist/entra-conditional-access.js +9 -4
  14. package/dist/entra-contacts.js +19 -12
  15. package/dist/entra-contracts.js +7 -4
  16. package/dist/entra-custom-security-attributes.js +19 -12
  17. package/dist/entra-data-policy-operations.js +7 -4
  18. package/dist/entra-delegated-admin.js +55 -6
  19. package/dist/entra-deleted-items.js +6 -4
  20. package/dist/entra-directory-objects.js +6 -4
  21. package/dist/entra-directory.js +19 -12
  22. package/dist/entra-domains.js +13 -8
  23. package/dist/entra-entitlement-management.js +12 -4
  24. package/dist/entra-group-pim.js +431 -0
  25. package/dist/entra-groups.js +13 -8
  26. package/dist/entra-lifecycle-workflows.js +136 -9
  27. package/dist/entra-organization.js +13 -8
  28. package/dist/entra-risk-prevention.js +436 -0
  29. package/dist/entra-risk.js +213 -5
  30. package/dist/entra-roles.js +10 -4
  31. package/dist/entra-subscriptions.js +6 -4
  32. package/dist/entra-users.js +7 -4
  33. package/dist/graph-session.js +29 -5
  34. package/dist/hook.js +84 -0
  35. package/dist/list-totals.js +52 -0
  36. package/dist/setup.js +5 -0
  37. package/docs/coverage.md +79 -13
  38. package/inventory/operations.json +2 -2
  39. package/package.json +4 -3
  40. package/skills/mg-axi/SKILL.md +56 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { encode } from "@toon-format/toon";
2
- import { AxiError, runAxiCli } from "axi-sdk-js";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { AxiError, installSessionStartHooks, runAxiCli } from "axi-sdk-js";
3
5
  import { LEAVES, home, leafHelp, operationFor, resolveCommand, DESCRIPTION, TOP_LEVEL_HELP } from "./catalogue.js";
4
6
  import { VERSION } from "./version.js";
5
7
  import { Profiles } from "./profiles.js";
@@ -10,11 +12,12 @@ import { revokeUserSessions } from "./entra-user-revoke-sessions.js";
10
12
  import { createMutationCoordinator, mutationFetchTransport } from "./mutations.js";
11
13
  import { TRANSITIVE_OPERATION, listGroupMemberOf, listGroupMembers, listGroups, showGroup } from "./entra-groups.js";
12
14
  import { listDirectoryRoles, showDirectoryRole, listRoleAssignments, listPimEligible, listPimActive } from "./entra-roles.js";
15
+ import { listGroupAssignmentSchedules, showGroupAssignmentSchedule, listGroupAssignmentInstances, showGroupAssignmentInstance, listGroupEligibilitySchedules, showGroupEligibilitySchedule, listGroupEligibilityInstances, showGroupEligibilityInstance, listGroupEligibilityRequests, showGroupEligibilityRequest } from "./entra-group-pim.js";
13
16
  import { listAdministrativeUnitMembers, listAdministrativeUnits, listDevices, showAdministrativeUnit, showDevice } from "./entra-directory.js";
14
17
  import { listSignIns, showSignIn, listDirectoryAudits, showDirectoryAudit } from "./entra-audit-logs.js";
15
18
  import { listApplicationOwners, listApplications, listServicePrincipalOwners, listServicePrincipals, showApplication, showServicePrincipal } from "./entra-apps.js";
16
19
  import { listAppRoleAssignments, listOAuth2Grants } from "./entra-grants.js";
17
- import { listRiskyUsers, showRiskyUser, listRiskDetections, showRiskDetection } from "./entra-risk.js";
20
+ import { listRiskyUsers, showRiskyUser, listRiskDetections, showRiskDetection, listRiskyServicePrincipals, showRiskyServicePrincipal, listRiskyServicePrincipalHistory, showRiskyServicePrincipalHistory, listServicePrincipalRiskDetections, showServicePrincipalRiskDetection } from "./entra-risk.js";
18
21
  import { dismissRiskyUser } from "./entra-risk-dismiss.js";
19
22
  import { listPolicies, showPolicy, listNamedLocations, showNamedLocation } from "./entra-conditional-access.js";
20
23
  import { listBrandingLocalizations, listOrganizations, showBranding, showBrandingLocalization, showOrganization } from "./entra-organization.js";
@@ -28,7 +31,7 @@ import { listDirectoryObjects, showDirectoryObject, countDirectoryObjects } from
28
31
  import { listDeletedItems, showDeletedItem, countDeletedItems } from "./entra-deleted-items.js";
29
32
  import { listContacts, showContact, countContacts, showContactManager, listContactDirectReports, showContactDirectReport, countContactDirectReports, castDirectReports, listContactMemberOf, showContactMemberOf, countContactMemberOf, transitMembership, castMembership } from "./entra-contacts.js";
30
33
  import { countContracts, listContracts, showContract } from "./entra-contracts.js";
31
- import { listDelegatedAdminCustomers, showDelegatedAdminCustomer, listDelegatedAdminRelationships, showDelegatedAdminRelationship, listDelegatedAdminAccessAssignments, showDelegatedAdminAccessAssignment, listDelegatedAdminOperations, showDelegatedAdminOperation, listDelegatedAdminRequests, showDelegatedAdminRequest, listDelegatedAdminServiceManagementDetails, showDelegatedAdminServiceManagementDetail } from "./entra-delegated-admin.js";
34
+ import { listDelegatedAdminCustomers, showDelegatedAdminCustomer, listDelegatedAdminRelationships, showDelegatedAdminRelationship, listDelegatedAdminAccessAssignments, showDelegatedAdminAccessAssignment, listDelegatedAdminOperations, showDelegatedAdminOperation, listDelegatedAdminRequests, showDelegatedAdminRequest, listDelegatedAdminServiceManagementDetails, showDelegatedAdminServiceManagementDetail, countDelegatedAdminCustomers, countDelegatedAdminServiceManagementDetails, countDelegatedAdminRelationships, countDelegatedAdminAccessAssignments, countDelegatedAdminOperations, countDelegatedAdminRequests } from "./entra-delegated-admin.js";
32
35
  import { listMultiTenantOrganizationTenants, showMultiTenantOrganization, showMultiTenantOrganizationJoinRequest, countMultiTenantOrganizationTenants } from "./entra-multi-tenant-organization.js";
33
36
  import { showTenantInformation } from "./entra-tenant-information.js";
34
37
  import { countLifecyclePolicies, listLifecyclePolicies, showLifecyclePolicy, countSettingTemplates, listSettingTemplates, showSettingTemplate } from "./entra-group-lifecycle.js";
@@ -36,9 +39,10 @@ import { listAttributeSets, showAttributeSet, countAttributeSets, listCustomSecu
36
39
  import { listIdentityProviders, showIdentityProvider, countIdentityProviders, availableIdentityProviderTypes } from "./entra-identity-providers.js";
37
40
  import { listFederationConfigurations, showFederationConfiguration, countFederationConfigurations, availableFederationProviderTypes } from "./entra-federation-configurations.js";
38
41
  import { listDataPolicyOperations, showDataPolicyOperation, countDataPolicyOperations } from "./entra-data-policy-operations.js";
42
+ import { listFraudProtectionProviders, showFraudProtectionProvider, countFraudProtectionProviders, listWafProviders, showWafProvider, countWafProviders, listWafVerifications, showWafVerification, countWafVerifications, showVerificationProvider } from "./entra-risk-prevention.js";
39
43
  import { listDefinitions, showDefinition, listInstances, showInstance, listDecisions, showDecision, listContactedReviewers, showContactedReviewer, listStages, showStage } from "./entra-access-reviews.js";
40
44
  import { listCatalogs, showCatalog, countCatalogs, listAccessPackages, showAccessPackage, countAccessPackages, listAssignmentPolicies, showAssignmentPolicy, countAssignmentPolicies, listResourceRoleScopes, showResourceRoleScope, countResourceRoleScopes, listAssignments, showAssignment, countAssignments, listAssignmentRequests, showAssignmentRequest, countAssignmentRequests } from "./entra-entitlement-management.js";
41
- import { listWorkflows, showWorkflow, countWorkflows, listWorkflowTemplates, showWorkflowTemplate, countWorkflowTemplates, listTaskDefinitions, showTaskDefinition, countTaskDefinitions, showLifecycleSettings, listRuns, showRun, countRuns, listUserProcessingResults, showUserProcessingResult, countUserProcessingResults, listSubjectProcessingResults, showSubjectProcessingResult, countSubjectProcessingResults, listTaskReports, showTaskReport, countTaskReports, showTaskReportTask, showTaskReportTaskDefinition } from "./entra-lifecycle-workflows.js";
45
+ import { listWorkflows, showWorkflow, countWorkflows, listWorkflowTemplates, showWorkflowTemplate, countWorkflowTemplates, listTaskDefinitions, showTaskDefinition, countTaskDefinitions, showLifecycleSettings, listRuns, showRun, countRuns, listUserProcessingResults, showUserProcessingResult, countUserProcessingResults, listSubjectProcessingResults, showSubjectProcessingResult, countSubjectProcessingResults, listTaskReports, showTaskReport, countTaskReports, showTaskReportTask, showTaskReportTaskDefinition, listRunUserProcessingResults, showRunUserProcessingResult, countRunUserProcessingResults, listRunSubjectProcessingResults, showRunSubjectProcessingResult, countRunSubjectProcessingResults, listRunTaskProcessingResults, showRunTaskProcessingResult, countRunTaskProcessingResults } from "./entra-lifecycle-workflows.js";
42
46
  import { listAuthenticationMethods, listRegistrationDetails } from "./entra-auth-methods.js";
43
47
  import { addGroupMember } from "./entra-group-member-add.js";
44
48
  import { fetchTransport } from "./api.js";
@@ -62,6 +66,36 @@ function localHome(name) {
62
66
  output.help.unshift("npx -y @knowttl/mg-axi profile list", "npx -y @knowttl/mg-axi login --help");
63
67
  return output;
64
68
  }
69
+ // One dispatch for the hook installer: resolve the sibling hook entry point
70
+ // installed next to this binary and register it as the SessionStart command
71
+ // for Claude Code, Codex and OpenCode through the SDK. No config, profile,
72
+ // transport or network is involved; the hook itself prints the local-only
73
+ // summary from src/hook.ts. Install failures throw loudly; hook failures
74
+ // never do.
75
+ function runSetupHooks() {
76
+ const hookPath = join(dirname(process.argv[1] ?? ""), "mg-axi-hook.js");
77
+ if (!existsSync(hookPath)) {
78
+ throw new AxiError("Cannot locate the mg-axi-hook entry point", "HOOK_INSTALL_FAILED", [
79
+ "Reinstall @knowttl/mg-axi so dist/bin/mg-axi-hook.js sits next to the main entry point",
80
+ "Run mg-axi setup hooks again after reinstalling",
81
+ ]);
82
+ }
83
+ const failures = [];
84
+ installSessionStartHooks({
85
+ marker: "mg-axi-hook",
86
+ execPath: hookPath,
87
+ binaryNames: ["mg-axi-hook"],
88
+ distEntrypoints: ["dist/bin/mg-axi-hook.js"],
89
+ onError: (message) => { failures.push(message); },
90
+ });
91
+ if (failures.length > 0) {
92
+ throw new AxiError("Failed to install mg-axi agent hooks", "HOOK_INSTALL_FAILED", failures);
93
+ }
94
+ return {
95
+ hooks: { status: "installed", integrations: "Claude Code, Codex, OpenCode" },
96
+ help: ["Restart your agent session to receive mg-axi ambient context"],
97
+ };
98
+ }
65
99
  async function readCursor(cursor) {
66
100
  if (cursor !== "-")
67
101
  return cursor;
@@ -98,6 +132,8 @@ export async function executeArgv(argv, overrides = {}) {
98
132
  return profiles.resolve(flags.profile);
99
133
  if (leaf.path === "setup")
100
134
  return setupView(profiles);
135
+ if (leaf.path === "setup hooks")
136
+ return runSetupHooks();
101
137
  if (leaf.path === "doctor") {
102
138
  const names = doctorTargets(profiles, flags.profile);
103
139
  let delegated = overrides.delegated;
@@ -238,7 +274,7 @@ export async function executeArgv(argv, overrides = {}) {
238
274
  default: return countResourceRoleScopes(session, flags, selected.profile, operation, help, selected.name);
239
275
  }
240
276
  }
241
- if (leaf.path === "entra lifecycle workflow list" || leaf.path === "entra lifecycle workflow show" || leaf.path === "entra lifecycle workflow count" || leaf.path === "entra lifecycle workflow-template list" || leaf.path === "entra lifecycle workflow-template show" || leaf.path === "entra lifecycle workflow-template count" || leaf.path === "entra lifecycle task-definition list" || leaf.path === "entra lifecycle task-definition show" || leaf.path === "entra lifecycle task-definition count" || leaf.path === "entra lifecycle settings show" || leaf.path === "entra lifecycle run list" || leaf.path === "entra lifecycle run show" || leaf.path === "entra lifecycle run count" || leaf.path === "entra lifecycle user-processing-result list" || leaf.path === "entra lifecycle user-processing-result show" || leaf.path === "entra lifecycle user-processing-result count" || leaf.path === "entra lifecycle subject-processing-result list" || leaf.path === "entra lifecycle subject-processing-result show" || leaf.path === "entra lifecycle subject-processing-result count" || leaf.path === "entra lifecycle task-report list" || leaf.path === "entra lifecycle task-report show" || leaf.path === "entra lifecycle task-report count" || leaf.path === "entra lifecycle task-report task show" || leaf.path === "entra lifecycle task-report task-definition show") {
277
+ if (leaf.path === "entra lifecycle workflow list" || leaf.path === "entra lifecycle workflow show" || leaf.path === "entra lifecycle workflow count" || leaf.path === "entra lifecycle workflow-template list" || leaf.path === "entra lifecycle workflow-template show" || leaf.path === "entra lifecycle workflow-template count" || leaf.path === "entra lifecycle task-definition list" || leaf.path === "entra lifecycle task-definition show" || leaf.path === "entra lifecycle task-definition count" || leaf.path === "entra lifecycle settings show" || leaf.path === "entra lifecycle run list" || leaf.path === "entra lifecycle run show" || leaf.path === "entra lifecycle run count" || leaf.path === "entra lifecycle user-processing-result list" || leaf.path === "entra lifecycle user-processing-result show" || leaf.path === "entra lifecycle user-processing-result count" || leaf.path === "entra lifecycle subject-processing-result list" || leaf.path === "entra lifecycle subject-processing-result show" || leaf.path === "entra lifecycle subject-processing-result count" || leaf.path === "entra lifecycle task-report list" || leaf.path === "entra lifecycle task-report show" || leaf.path === "entra lifecycle task-report count" || leaf.path === "entra lifecycle task-report task show" || leaf.path === "entra lifecycle task-report task-definition show" || leaf.path === "entra lifecycle run user-processing-result list" || leaf.path === "entra lifecycle run user-processing-result show" || leaf.path === "entra lifecycle run user-processing-result count" || leaf.path === "entra lifecycle run subject-processing-result list" || leaf.path === "entra lifecycle run subject-processing-result show" || leaf.path === "entra lifecycle run subject-processing-result count" || leaf.path === "entra lifecycle run task-processing-result list" || leaf.path === "entra lifecycle run task-processing-result show" || leaf.path === "entra lifecycle run task-processing-result count") {
242
278
  if (String(flags["api-version"] ?? "v1.0") !== "v1.0") {
243
279
  throw new AxiError("Lifecycle-workflow reads support v1.0 only; beta needs its own review", "VALIDATION_ERROR", [leafHelp(leaf)]);
244
280
  }
@@ -287,6 +323,15 @@ export async function executeArgv(argv, overrides = {}) {
287
323
  case "entra lifecycle task-report count": return countTaskReports(session, flags, selected.profile, operation, help, selected.name);
288
324
  case "entra lifecycle task-report task show": return showTaskReportTask(session, flags, selected.profile, operation, help, selected.name);
289
325
  case "entra lifecycle task-report task-definition show": return showTaskReportTaskDefinition(session, flags, selected.profile, operation, help, selected.name);
326
+ case "entra lifecycle run user-processing-result list": return listRunUserProcessingResults(session, flags, selected.profile, operation, help, selected.name);
327
+ case "entra lifecycle run user-processing-result show": return showRunUserProcessingResult(session, flags, selected.profile, operation, help, selected.name);
328
+ case "entra lifecycle run user-processing-result count": return countRunUserProcessingResults(session, flags, selected.profile, operation, help, selected.name);
329
+ case "entra lifecycle run subject-processing-result list": return listRunSubjectProcessingResults(session, flags, selected.profile, operation, help, selected.name);
330
+ case "entra lifecycle run subject-processing-result show": return showRunSubjectProcessingResult(session, flags, selected.profile, operation, help, selected.name);
331
+ case "entra lifecycle run subject-processing-result count": return countRunSubjectProcessingResults(session, flags, selected.profile, operation, help, selected.name);
332
+ case "entra lifecycle run task-processing-result list": return listRunTaskProcessingResults(session, flags, selected.profile, operation, help, selected.name);
333
+ case "entra lifecycle run task-processing-result show": return showRunTaskProcessingResult(session, flags, selected.profile, operation, help, selected.name);
334
+ case "entra lifecycle run task-processing-result count": return countRunTaskProcessingResults(session, flags, selected.profile, operation, help, selected.name);
290
335
  default: return showLifecycleSettings(session, flags, selected.profile, operation, help, selected.name);
291
336
  }
292
337
  }
@@ -541,6 +586,11 @@ export async function executeArgv(argv, overrides = {}) {
541
586
  });
542
587
  }
543
588
  if (leaf.path === "entra directory-role list" || leaf.path === "entra directory-role show" || leaf.path === "entra role-assignment list" || leaf.path === "entra pim eligible list" || leaf.path === "entra pim active list"
589
+ || leaf.path === "entra pim group-assignment-schedule list" || leaf.path === "entra pim group-assignment-schedule show"
590
+ || leaf.path === "entra pim group-assignment-instance list" || leaf.path === "entra pim group-assignment-instance show"
591
+ || leaf.path === "entra pim group-eligibility-schedule list" || leaf.path === "entra pim group-eligibility-schedule show"
592
+ || leaf.path === "entra pim group-eligibility-instance list" || leaf.path === "entra pim group-eligibility-instance show"
593
+ || leaf.path === "entra pim group-eligibility-request list" || leaf.path === "entra pim group-eligibility-request show"
544
594
  || leaf.path === "entra device list" || leaf.path === "entra device show"
545
595
  || leaf.path === "entra administrative-unit list" || leaf.path === "entra administrative-unit show"
546
596
  || leaf.path === "entra administrative-unit member list") {
@@ -569,6 +619,16 @@ export async function executeArgv(argv, overrides = {}) {
569
619
  case "entra role-assignment list": return listRoleAssignments(session, flags, selected.profile, operation, help, selected.name);
570
620
  case "entra pim eligible list": return listPimEligible(session, flags, selected.profile, operation, help, selected.name);
571
621
  case "entra pim active list": return listPimActive(session, flags, selected.profile, operation, help, selected.name);
622
+ case "entra pim group-assignment-schedule list": return listGroupAssignmentSchedules(session, flags, selected.profile, operation, help, selected.name);
623
+ case "entra pim group-assignment-schedule show": return showGroupAssignmentSchedule(session, flags, selected.profile, operation, help, selected.name);
624
+ case "entra pim group-assignment-instance list": return listGroupAssignmentInstances(session, flags, selected.profile, operation, help, selected.name);
625
+ case "entra pim group-assignment-instance show": return showGroupAssignmentInstance(session, flags, selected.profile, operation, help, selected.name);
626
+ case "entra pim group-eligibility-schedule list": return listGroupEligibilitySchedules(session, flags, selected.profile, operation, help, selected.name);
627
+ case "entra pim group-eligibility-schedule show": return showGroupEligibilitySchedule(session, flags, selected.profile, operation, help, selected.name);
628
+ case "entra pim group-eligibility-instance list": return listGroupEligibilityInstances(session, flags, selected.profile, operation, help, selected.name);
629
+ case "entra pim group-eligibility-instance show": return showGroupEligibilityInstance(session, flags, selected.profile, operation, help, selected.name);
630
+ case "entra pim group-eligibility-request list": return listGroupEligibilityRequests(session, flags, selected.profile, operation, help, selected.name);
631
+ case "entra pim group-eligibility-request show": return showGroupEligibilityRequest(session, flags, selected.profile, operation, help, selected.name);
572
632
  case "entra device list": return listDevices(session, flags, selected.profile, operation, help, selected.name);
573
633
  case "entra device show": return showDevice(session, flags, selected.profile, operation, help, selected.name);
574
634
  case "entra administrative-unit list": return listAdministrativeUnits(session, flags, selected.profile, operation, help, selected.name);
@@ -702,7 +762,7 @@ export async function executeArgv(argv, overrides = {}) {
702
762
  help: leafHelp(leaf),
703
763
  });
704
764
  }
705
- if (leaf.path === "entra risky-user list" || leaf.path === "entra risky-user show" || leaf.path === "entra risk-detection list" || leaf.path === "entra risk-detection show") {
765
+ if (leaf.path === "entra risky-user list" || leaf.path === "entra risky-user show" || leaf.path === "entra risk-detection list" || leaf.path === "entra risk-detection show" || leaf.path === "entra risky-service-principal list" || leaf.path === "entra risky-service-principal show" || leaf.path === "entra risky-service-principal history list" || leaf.path === "entra risky-service-principal history show" || leaf.path === "entra service-principal-risk-detection list" || leaf.path === "entra service-principal-risk-detection show") {
706
766
  const selected = profiles.resolve(flags.profile);
707
767
  const operation = operationFor(leaf, String(flags["api-version"] ?? "v1.0"));
708
768
  if (!operation || operation.method !== "GET") {
@@ -728,7 +788,13 @@ export async function executeArgv(argv, overrides = {}) {
728
788
  case "entra risky-user list": return listRiskyUsers(session, flags, selected.profile, operation, help, selected.name);
729
789
  case "entra risky-user show": return showRiskyUser(session, flags, selected.profile, operation, help, selected.name);
730
790
  case "entra risk-detection list": return listRiskDetections(session, flags, selected.profile, operation, help, selected.name);
731
- default: return showRiskDetection(session, flags, selected.profile, operation, help, selected.name);
791
+ case "entra risk-detection show": return showRiskDetection(session, flags, selected.profile, operation, help, selected.name);
792
+ case "entra risky-service-principal list": return listRiskyServicePrincipals(session, flags, selected.profile, operation, help, selected.name);
793
+ case "entra risky-service-principal show": return showRiskyServicePrincipal(session, flags, selected.profile, operation, help, selected.name);
794
+ case "entra risky-service-principal history list": return listRiskyServicePrincipalHistory(session, flags, selected.profile, operation, help, selected.name);
795
+ case "entra risky-service-principal history show": return showRiskyServicePrincipalHistory(session, flags, selected.profile, operation, help, selected.name);
796
+ case "entra service-principal-risk-detection list": return listServicePrincipalRiskDetections(session, flags, selected.profile, operation, help, selected.name);
797
+ default: return showServicePrincipalRiskDetection(session, flags, selected.profile, operation, help, selected.name);
732
798
  }
733
799
  }
734
800
  if (leaf.path === "entra domain list" || leaf.path === "entra domain show"
@@ -1106,7 +1172,7 @@ export async function executeArgv(argv, overrides = {}) {
1106
1172
  default: return countContracts(session, flags, selected.profile, operation, help, selected.name);
1107
1173
  }
1108
1174
  }
1109
- if (leaf.path === "entra delegated-admin-customer list" || leaf.path === "entra delegated-admin-customer show" || leaf.path === "entra delegated-admin-relationship list" || leaf.path === "entra delegated-admin-relationship show" || leaf.path === "entra delegated-admin-relationship list-access-assignments" || leaf.path === "entra delegated-admin-relationship show-access-assignment" || leaf.path === "entra delegated-admin-relationship list-operations" || leaf.path === "entra delegated-admin-relationship show-operation" || leaf.path === "entra delegated-admin-relationship list-requests" || leaf.path === "entra delegated-admin-relationship show-request" || leaf.path === "entra delegated-admin-customer list-service-management-details" || leaf.path === "entra delegated-admin-customer show-service-management-detail") {
1175
+ if (leaf.path === "entra delegated-admin-customer list" || leaf.path === "entra delegated-admin-customer show" || leaf.path === "entra delegated-admin-relationship list" || leaf.path === "entra delegated-admin-relationship show" || leaf.path === "entra delegated-admin-relationship list-access-assignments" || leaf.path === "entra delegated-admin-relationship show-access-assignment" || leaf.path === "entra delegated-admin-relationship list-operations" || leaf.path === "entra delegated-admin-relationship show-operation" || leaf.path === "entra delegated-admin-relationship list-requests" || leaf.path === "entra delegated-admin-relationship show-request" || leaf.path === "entra delegated-admin-customer list-service-management-details" || leaf.path === "entra delegated-admin-customer show-service-management-detail" || leaf.path === "entra delegated-admin-customer count" || leaf.path === "entra delegated-admin-customer count-service-management-details" || leaf.path === "entra delegated-admin-relationship count" || leaf.path === "entra delegated-admin-relationship count-access-assignments" || leaf.path === "entra delegated-admin-relationship count-operations" || leaf.path === "entra delegated-admin-relationship count-requests") {
1110
1176
  if (String(flags["api-version"] ?? "v1.0") !== "v1.0") {
1111
1177
  throw new AxiError("Delegated-admin reads support v1.0 only; beta needs its own review", "VALIDATION_ERROR", [leafHelp(leaf)]);
1112
1178
  }
@@ -1141,6 +1207,12 @@ export async function executeArgv(argv, overrides = {}) {
1141
1207
  case "entra delegated-admin-relationship list-requests": return listDelegatedAdminRequests(session, flags, selected.profile, operation, help, selected.name);
1142
1208
  case "entra delegated-admin-relationship show-request": return showDelegatedAdminRequest(session, flags, selected.profile, operation, help, selected.name);
1143
1209
  case "entra delegated-admin-customer list-service-management-details": return listDelegatedAdminServiceManagementDetails(session, flags, selected.profile, operation, help, selected.name);
1210
+ case "entra delegated-admin-customer count": return countDelegatedAdminCustomers(session, flags, selected.profile, operation, help, selected.name);
1211
+ case "entra delegated-admin-customer count-service-management-details": return countDelegatedAdminServiceManagementDetails(session, flags, selected.profile, operation, help, selected.name);
1212
+ case "entra delegated-admin-relationship count": return countDelegatedAdminRelationships(session, flags, selected.profile, operation, help, selected.name);
1213
+ case "entra delegated-admin-relationship count-access-assignments": return countDelegatedAdminAccessAssignments(session, flags, selected.profile, operation, help, selected.name);
1214
+ case "entra delegated-admin-relationship count-operations": return countDelegatedAdminOperations(session, flags, selected.profile, operation, help, selected.name);
1215
+ case "entra delegated-admin-relationship count-requests": return countDelegatedAdminRequests(session, flags, selected.profile, operation, help, selected.name);
1144
1216
  default: return showDelegatedAdminServiceManagementDetail(session, flags, selected.profile, operation, help, selected.name);
1145
1217
  }
1146
1218
  }
@@ -1288,6 +1360,42 @@ export async function executeArgv(argv, overrides = {}) {
1288
1360
  default: return countDataPolicyOperations(session, flags, selected.profile, operation, help, selected.name);
1289
1361
  }
1290
1362
  }
1363
+ if (leaf.path === "entra fraud-protection-provider list" || leaf.path === "entra fraud-protection-provider show" || leaf.path === "entra fraud-protection-provider count" || leaf.path === "entra web-application-firewall-provider list" || leaf.path === "entra web-application-firewall-provider show" || leaf.path === "entra web-application-firewall-provider count" || leaf.path === "entra web-application-firewall-verification list" || leaf.path === "entra web-application-firewall-verification show" || leaf.path === "entra web-application-firewall-verification count" || leaf.path === "entra web-application-firewall-verification provider show") {
1364
+ if (String(flags["api-version"] ?? "v1.0") !== "v1.0") {
1365
+ throw new AxiError("Risk-prevention reads support v1.0 only; beta needs its own review", "VALIDATION_ERROR", [leafHelp(leaf)]);
1366
+ }
1367
+ const selected = profiles.resolve(flags.profile);
1368
+ const operation = operationFor(leaf, "v1.0");
1369
+ if (!operation || operation.method !== "GET") {
1370
+ throw new AxiError(`Unknown catalogued Graph operation for ${leaf.path}`, "VALIDATION_ERROR", [leafHelp(leaf)]);
1371
+ }
1372
+ let delegated = overrides.delegated;
1373
+ let application = overrides.application;
1374
+ if (!delegated) {
1375
+ const { DelegatedAuth: Service } = await import("./auth.js");
1376
+ const { MsalProvider } = await import("./msal-provider.js");
1377
+ delegated = new Service(new MsalProvider());
1378
+ }
1379
+ if (!application) {
1380
+ const { ApplicationAuth: Service } = await import("./app-auth.js");
1381
+ const { MsalApplicationProvider } = await import("./msal-app-provider.js");
1382
+ application = new Service(new MsalApplicationProvider());
1383
+ }
1384
+ const session = new GraphSession({ delegated, application, transport: overrides.transport ?? fetchTransport });
1385
+ const help = leafHelp(leaf);
1386
+ switch (leaf.path) {
1387
+ case "entra fraud-protection-provider list": return listFraudProtectionProviders(session, flags, selected.profile, operation, help, selected.name);
1388
+ case "entra fraud-protection-provider show": return showFraudProtectionProvider(session, flags, selected.profile, operation, help, selected.name);
1389
+ case "entra fraud-protection-provider count": return countFraudProtectionProviders(session, flags, selected.profile, operation, help);
1390
+ case "entra web-application-firewall-provider list": return listWafProviders(session, flags, selected.profile, operation, help, selected.name);
1391
+ case "entra web-application-firewall-provider show": return showWafProvider(session, flags, selected.profile, operation, help, selected.name);
1392
+ case "entra web-application-firewall-provider count": return countWafProviders(session, flags, selected.profile, operation, help);
1393
+ case "entra web-application-firewall-verification list": return listWafVerifications(session, flags, selected.profile, operation, help, selected.name);
1394
+ case "entra web-application-firewall-verification show": return showWafVerification(session, flags, selected.profile, operation, help, selected.name);
1395
+ case "entra web-application-firewall-verification count": return countWafVerifications(session, flags, selected.profile, operation, help);
1396
+ default: return showVerificationProvider(session, flags, selected.profile, operation, help, selected.name);
1397
+ }
1398
+ }
1291
1399
  const operation = operationFor(leaf, String(flags["api-version"] ?? "v1.0"));
1292
1400
  throw new AxiError(`Command is not executable: ${operation?.disposition ?? "unavailable"} (${operation?.owningSlice ?? "no inventory mapping"})`, "NOT_IMPLEMENTED", [leafHelp(leaf)]);
1293
1401
  }
package/dist/docs.js CHANGED
@@ -74,10 +74,15 @@ const READ_AREA_LABELS = [
74
74
  ["entra service-principal ", "applications and consent grants"],
75
75
  ["entra risky-user ", "risk"],
76
76
  ["entra risk-detection ", "risk"],
77
+ ["entra risky-service-principal ", "risk"],
78
+ ["entra service-principal-risk-detection ", "risk"],
77
79
  ["entra conditional-access ", "Conditional Access"],
78
80
  ["entra identity-provider ", "identity providers"],
79
81
  ["entra federation-configuration ", "federation configurations"],
80
82
  ["entra data-policy-operation ", "data policy operations"],
83
+ ["entra fraud-protection-provider ", "risk prevention"],
84
+ ["entra web-application-firewall-provider ", "risk prevention"],
85
+ ["entra web-application-firewall-verification ", "risk prevention"],
81
86
  ["entra access-review ", "access reviews"],
82
87
  ["entra entitlement catalog ", "entitlement catalogs and access packages"],
83
88
  ["entra entitlement access-package ", "entitlement catalogs and access packages"],
@@ -200,6 +205,19 @@ export function skillDocument() {
200
205
  "Configuration defaults to `~/.mg-axi/config.json`; `MG_AXI_CONFIG` selects a separate file.",
201
206
  "`mg-axi setup` shows the selected path and writes nothing.",
202
207
  "",
208
+ "Agent discovery has two install paths, and either suffices alone.",
209
+ "For ambient context at every agent session start instead of the skill, install the CLI globally and opt into the session hook:",
210
+ "",
211
+ "```sh",
212
+ "npm install --global @knowttl/mg-axi",
213
+ "mg-axi setup hooks",
214
+ "```",
215
+ "",
216
+ "This installs a `SessionStart` hook for Claude Code, Codex and OpenCode that prints a short local-only summary (configured profile names with tenant labels, auth-cache presence, write posture and version).",
217
+ "The hook makes no network, Graph or sign-in call, and prints a short `not configured` record with exit 0 when unconfigured.",
218
+ "Restart the agent session after running the installer.",
219
+ "No other ordinary command installs hooks, plugins or configuration.",
220
+ "",
203
221
  "## Selecting a profile",
204
222
  "",
205
223
  "Read leaves accept `--profile <name>`.",
@@ -273,6 +291,7 @@ export function capabilityDocument() {
273
291
  const deferredProviderRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Deferred to a later EXT-03 "));
274
292
  const deferredFederationRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate federation-configuration scope "));
275
293
  const deferredDataPolicyRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Deferred by firstmate data-policy-operations scope "));
294
+ const deferredRiskPreventionRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Deferred by firstmate risk-prevention scope "));
276
295
  const unavailableInvitationRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Marked unavailable by firstmate EXT-03b decision: "));
277
296
  const deferredOrganizationRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate organization scope "));
278
297
  const unavailableCertAuthRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Marked unavailable by firstmate mg-ext-01d decision: "));
@@ -285,6 +304,7 @@ export function capabilityDocument() {
285
304
  const deferredContractRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate contracts scope "));
286
305
  const deferredDelegatedAdminRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate delegated-admin scope "));
287
306
  const deferredMtoRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate multi-tenant-organization scope "));
307
+ const deferredReportsRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate reports scope "));
288
308
  const deferredGroupLifecycleRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate group-lifecycle scope "));
289
309
  const deferredCustomSecurityRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate custom-security-attributes scope "));
290
310
  const deferredAccessReviewRows = rows.filter(row => row.owningSlice === "EXT-02c");
@@ -364,6 +384,18 @@ export function capabilityDocument() {
364
384
  "|---|---|---|---|",
365
385
  ...deferredDataPolicyRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
366
386
  "",
387
+ "## EXT-03 risk-prevention scope decisions",
388
+ "",
389
+ "This change covers the ten v1.0 workforce risk-prevention reads above (fraud-protection-provider list, show and count; web-application-firewall-provider list, show and count; web-application-firewall-verification list, show, count and provider show).",
390
+ "The eleven beta operations below remain scheduled with an explicit deferred disposition to a later EXT-03 subfamily; no new commands or raw access are approved.",
391
+ "Risk-prevention reads support v1.0 only; beta needs its own review.",
392
+ "API keys, client secrets and other key material (Arkose privateKey/publicKey, HUMAN serverToken, Akamai clientSecret/clientToken/accessToken, Cloudflare apiToken) are never selectable and never projected; application profiles are refused before credentials because Graph documents no supported application permission.",
393
+ "Workforce context only; no external-customer support is claimed.",
394
+ "",
395
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
396
+ "|---|---|---|---|",
397
+ ...deferredRiskPreventionRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
398
+ "",
367
399
  "## EXT-03 invitations scope decisions",
368
400
  "",
369
401
  "Firstmate decision (mg-ext-03b inbox 001): do not implement these reads and add no scope.",
@@ -495,9 +527,9 @@ export function capabilityDocument() {
495
527
  ...deferredContractRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`), "",
496
528
  "## EXT-04 delegated-admin scope decisions",
497
529
  "",
498
- "Firstmate scope: approve narrowing this change to the twelve v1.0 delegated-admin reads above (customer list/show, relationship list/show, access-assignment list/show, operation list/show, request list/show and service-management-detail list/show).",
530
+ "Firstmate scope: approve narrowing this change to the eighteen v1.0 delegated-admin reads above (customer list/show/count, relationship list/show/count, access-assignment list/show/count, operation list/show/count, request list/show/count and service-management-detail list/show/count).",
499
531
  "The operations below remain scheduled with an explicit deferred disposition to a later EXT-04 subfamily; no new commands or raw access are approved.",
500
- "Scalar counts need a separate query and response contract from the approved list/show reads; the tenantRelationship container root needs its own projection review; multi-tenant-organization reads shipped in the section below, and tenant-lookup functions ship as tenant-information show under the mg-ext-04e function-argument contract.",
532
+ "The tenantRelationship container root needs its own projection review; multi-tenant-organization reads shipped in the section below, and tenant-lookup functions ship as tenant-information show under the mg-ext-04e function-argument contract.",
501
533
  "",
502
534
  "| Inventory operation | Disposition | Owning slice | Deferral reason |",
503
535
  "|---|---|---|---|",
@@ -510,6 +542,14 @@ export function capabilityDocument() {
510
542
  "| Inventory operation | Disposition | Owning slice | Deferral reason |",
511
543
  "|---|---|---|---|",
512
544
  ...deferredMtoRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`), "",
545
+ "## EXT-04 reports scope decisions",
546
+ "",
547
+ "Firstmate scope: approve deferring the v1.0 relying-party summary read below; no named command or raw access is approved for it.",
548
+ "The relying-party summary read below remains scheduled with an explicit deferred disposition: its period function argument needs a new session-guard function-argument binding beyond the three allowlisted tenant-lookup/commerce routes, and the request-path guard must stay byte-identical to origin/main apart from READ_SCOPES additions, so no named command or raw access is approved for it until its own guard review lands.",
549
+ "",
550
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
551
+ "|---|---|---|---|",
552
+ ...deferredReportsRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`), "",
513
553
  "## EXT-04 tenant-lookup scope decisions",
514
554
  "",
515
555
  "Firstmate scope: approve the mg-ext-04e function-argument contract with the three v1.0 reads it unblocks (tenant-information show by domain name or tenant ID, subscription show by commerce subscription ID).",
@@ -538,9 +578,9 @@ export function capabilityDocument() {
538
578
  "",
539
579
  "## EXT-02 lifecycle-workflows scope decisions",
540
580
  "",
541
- "This change covers the twenty-four v1.0 lifecycle-workflows first-, second- and third-part reads above (workflow, workflow-template and task-definition list/show/count reads, the tenant settings show, top-level run and user/subject processing-result list/show/count reads, plus task-report list/show/count reads with the task and taskDefinition singles).",
542
- "Run-nested and third-level processing results, taskProcessingResults sub-reads, reprocessed runs, subject sub-reads, workflow tasks, template tasks, versions, insights, deleted items and custom task extensions stay scheduled for follow-up parts with no new commands or raw access approved; so do the six top-level summary functions, which stay deferred because they need bracketed start/end arguments outside the session-guard function-binding allowlist while the request-path guard cannot change beyond READ_SCOPES additions. Every beta operation and every mutation (no workflow create/update/delete/activate/run, no settings update, no restore) stays out.",
543
- "No new commands or raw access are approved for any lifecycle-workflows row outside the twenty-four above.",
581
+ "This change covers the thirty-three v1.0 lifecycle-workflows first-, second-, third- and fourth-part reads above (workflow, workflow-template and task-definition list/show/count reads, the tenant settings show, top-level run and user/subject processing-result list/show/count reads, task-report list/show/count reads with the task and taskDefinition singles, plus run-nested user/subject/task processing-result list/show/count reads).",
582
+ "Third-level processing results, taskProcessingResults sub-reads under a user or subject result, reprocessed runs, subject sub-reads, workflow tasks, template tasks, versions, insights, deleted items and custom task extensions stay scheduled for follow-up parts with no new commands or raw access approved; so do the six summary functions (the run user/subject summaries plus the four top-level ones), which stay deferred because they need bracketed start/end arguments outside the session-guard function-binding allowlist while the request-path guard cannot change beyond READ_SCOPES additions. Every beta operation and every mutation (no workflow create/update/delete/activate/run, no settings update, no restore) stays out.",
583
+ "No new commands or raw access are approved for any lifecycle-workflows row outside the thirty-three above.",
544
584
  "",
545
585
  "## Named writes",
546
586
  "",
package/dist/doctor.js CHANGED
@@ -44,8 +44,8 @@ export async function runDoctor(args) {
44
44
  // surfaced row, remainder buffered in a discarded cursor. Doctor keeps
45
45
  // no cursor and accepts none; recovery reruns doctor, not the read.
46
46
  const result = (await listUsers(args.session, { limit: "1" }, selected.profile, operation, `npx -y @knowttl/mg-axi ${DOCTOR_CHECK} --help`, name));
47
- if (!result.count.complete && result.count.reason !== "row limit reached; buffered remainder is preserved in the cursor") {
48
- throw new AxiError(`Doctor read incomplete: ${result.count.reason}`, "GRAPH_ERROR", [rerun]);
47
+ if (!result.complete && result.reason !== "row limit reached; buffered remainder is preserved in the cursor") {
48
+ throw new AxiError(`Doctor read incomplete: ${result.reason}`, "GRAPH_ERROR", [rerun]);
49
49
  }
50
50
  profiles.push({ name, mode: selected.profile.mode, check: DOCTOR_CHECK, status: "ok",
51
51
  detail: result.users.length ? "1 user row returned" : "0 users matched; the absence of results is the answer" });
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-02 access-reviews subfamily: the read-only access-review mapping.
3
4
  // See README.md for the supported commands and usage. Operation construction
4
5
  // stays beside its command; the shared session owns URLs, credentials, paging,
@@ -236,6 +237,7 @@ const ACCESS_REVIEW_DENIAL_HINTS = [
236
237
  const DEFINITION_LIST = {
237
238
  command: "entra access-review definition list",
238
239
  key: "definitions",
240
+ noun: "access-review definitions",
239
241
  known: DEFINITION_KNOWN,
240
242
  knownList: KNOWN_DEFINITION_FIELDS,
241
243
  defaultSelect: DEFAULT_DEFINITION_LIST_SELECT,
@@ -249,6 +251,7 @@ const DEFINITION_LIST = {
249
251
  const INSTANCE_LIST = {
250
252
  command: "entra access-review instance list",
251
253
  key: "instances",
254
+ noun: "access-review instances",
252
255
  known: INSTANCE_KNOWN,
253
256
  knownList: KNOWN_INSTANCE_FIELDS,
254
257
  defaultSelect: DEFAULT_INSTANCE_LIST_SELECT,
@@ -261,6 +264,7 @@ const INSTANCE_LIST = {
261
264
  const DECISION_LIST = {
262
265
  command: "entra access-review decision list",
263
266
  key: "decisions",
267
+ noun: "access-review decisions",
264
268
  known: DECISION_KNOWN,
265
269
  knownList: KNOWN_DECISION_FIELDS,
266
270
  defaultSelect: DEFAULT_DECISION_SELECT,
@@ -273,6 +277,7 @@ const DECISION_LIST = {
273
277
  const CONTACTED_REVIEWER_LIST = {
274
278
  command: "entra access-review contacted-reviewer list",
275
279
  key: "contactedReviewers",
280
+ noun: "contacted reviewers",
276
281
  known: CONTACTED_REVIEWER_KNOWN,
277
282
  knownList: KNOWN_CONTACTED_REVIEWER_FIELDS,
278
283
  defaultSelect: DEFAULT_CONTACTED_REVIEWER_LIST_SELECT,
@@ -286,6 +291,7 @@ const CONTACTED_REVIEWER_LIST = {
286
291
  const STAGE_LIST = {
287
292
  command: "entra access-review stage list",
288
293
  key: "stages",
294
+ noun: "access-review stages",
289
295
  known: STAGE_KNOWN,
290
296
  knownList: KNOWN_STAGE_FIELDS,
291
297
  defaultSelect: DEFAULT_STAGE_LIST_SELECT,
@@ -347,16 +353,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
347
353
  if (!result.complete) {
348
354
  return {
349
355
  [shape.key]: rows,
350
- count: { returned: rows.length, complete: false, reason: result.reason },
356
+ ...listTotals(rows.length, result.total, shape.noun, false),
357
+ complete: false,
358
+ reason: result.reason,
351
359
  cursor: result.cursor,
352
360
  help: [...truncationHints, resumeHint, ...standing],
353
361
  };
354
362
  }
355
- const count = { returned: rows.length, complete: true };
356
363
  if (!rows.length) {
357
- return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
364
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
358
365
  }
359
- return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
366
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
360
367
  }
361
368
  async function showOne(key, command, known, knownList, defaultSelect, session, flags, profile, operation, help, profileName, params, malformed) {
362
369
  const { select, fields } = selectedFields(flags, defaultSelect, known, knownList, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  import { SAFE_CREDENTIAL_FIELDS } from "./graph-session.js";
3
4
  // READ-07: the Entra application and service-principal read mapping behind
4
5
  // `mg-axi entra application list/show`, `mg-axi entra service-principal
@@ -270,23 +271,25 @@ async function listCollection(session, flags, profile, operation, help, profileN
270
271
  if (!result.complete) {
271
272
  return {
272
273
  [key]: rows,
273
- count: { returned: rows.length, complete: false, reason: result.reason },
274
+ ...listTotals(rows.length, result.total, emptyNoun, false),
275
+ complete: false,
276
+ reason: result.reason,
274
277
  cursor: result.cursor,
275
278
  help: [...truncationHints, resumeHint(profileName), showHint],
276
279
  };
277
280
  }
278
- const count = { returned: rows.length, complete: true };
279
281
  if (!rows.length) {
280
282
  return {
281
283
  [key]: rows,
282
- count,
284
+ ...listTotals(rows.length, result.total, emptyNoun, true),
285
+ complete: true,
283
286
  help: [
284
287
  `0 ${emptyNoun} matched; the absence of results is the answer, not an error`,
285
288
  showHint,
286
289
  ],
287
290
  };
288
291
  }
289
- return { [key]: rows, count, help: [...truncationHints, showHint] };
292
+ return { [key]: rows, ...listTotals(rows.length, result.total, emptyNoun, true), complete: true, help: [...truncationHints, showHint] };
290
293
  }
291
294
  async function showSingle(session, flags, profile, operation, help, profileName, command, key, known, knownList, defaults, param, flag) {
292
295
  checkReadOperation(operation, help);
@@ -351,20 +354,22 @@ async function listOwners(session, flags, profile, operation, help, profileName,
351
354
  if (!result.complete) {
352
355
  return {
353
356
  owners: rows,
354
- count: { returned: rows.length, complete: false, reason: result.reason },
357
+ ...listTotals(rows.length, result.total, "owners", false),
358
+ complete: false,
359
+ reason: result.reason,
355
360
  cursor: result.cursor,
356
361
  help: [...truncationHints, ...limitedHints, resumeHint(profileName)],
357
362
  };
358
363
  }
359
- const count = { returned: rows.length, complete: true };
360
364
  if (!rows.length) {
361
365
  return {
362
366
  owners: rows,
363
- count,
367
+ ...listTotals(rows.length, result.total, "owners", true),
368
+ complete: true,
364
369
  help: [...limitedHints, "0 owners matched; the absence of results is the answer, not an error"],
365
370
  };
366
371
  }
367
- return { owners: rows, count, help: [...truncationHints, ...limitedHints] };
372
+ return { owners: rows, ...listTotals(rows.length, result.total, "owners", true), complete: true, help: [...truncationHints, ...limitedHints] };
368
373
  }
369
374
  export async function listApplicationOwners(session, flags, profile, operation, help, profileName) {
370
375
  return listOwners(session, flags, profile, operation, help, profileName, "entra application owner list", "application-id", "application");
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // READ-05: the Entra sign-in and directory-audit read mapping behind
3
4
  // `mg-axi entra sign-in list/show` and `mg-axi entra directory-audit
4
5
  // list/show`. Operation construction stays beside its command; the shared
@@ -228,6 +229,7 @@ const AUDIT_DENIAL_HINTS = [
228
229
  ];
229
230
  const SIGNIN_LIST = {
230
231
  noun: "sign-in",
232
+ plural: "sign-ins",
231
233
  key: "signIns",
232
234
  dateField: "createdDateTime",
233
235
  known: SIGNIN_KNOWN,
@@ -243,6 +245,7 @@ const SIGNIN_LIST = {
243
245
  };
244
246
  const AUDIT_LIST = {
245
247
  noun: "directory-audit",
248
+ plural: "directory audits",
246
249
  key: "directoryAudits",
247
250
  dateField: "activityDateTime",
248
251
  known: AUDIT_KNOWN,
@@ -313,16 +316,17 @@ async function listLogs(shape, session, flags, profile, operation, help, profile
313
316
  if (!result.complete) {
314
317
  return {
315
318
  [shape.key]: rows,
316
- count: { returned: rows.length, complete: false, reason: result.reason },
319
+ ...listTotals(rows.length, result.total, shape.plural, false),
320
+ complete: false,
321
+ reason: result.reason,
317
322
  cursor: result.cursor,
318
323
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`, showHint],
319
324
  };
320
325
  }
321
- const count = { returned: rows.length, complete: true };
322
326
  if (!rows.length) {
323
- return { [shape.key]: rows, count, help: shape.emptyHints(profileName) };
327
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.plural, true), complete: true, help: shape.emptyHints(profileName) };
324
328
  }
325
- return { [shape.key]: rows, count, help: [...truncationHints, showHint] };
329
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.plural, true), complete: true, help: [...truncationHints, showHint] };
326
330
  }
327
331
  const SIGNIN_SHOW = {
328
332
  noun: "sign-in",
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // READ-04: the Entra authentication-method and registration-report read
3
4
  // mapping behind `mg-axi entra user authentication-method list` and
4
5
  // `mg-axi entra registration list`. Operation construction stays beside its
@@ -203,6 +204,7 @@ const REPORT_DENIAL_HINTS = [
203
204
  const METHOD_LIST = {
204
205
  command: "entra user authentication-method list",
205
206
  key: "authenticationMethods",
207
+ noun: "authentication methods",
206
208
  known: KNOWN_METHODS,
207
209
  knownList: KNOWN_METHOD_FIELDS,
208
210
  defaultSelect: DEFAULT_METHOD_SELECT,
@@ -218,6 +220,7 @@ const METHOD_LIST = {
218
220
  const REPORT_LIST = {
219
221
  command: "entra registration list",
220
222
  key: "registrationDetails",
223
+ noun: "registration rows",
221
224
  known: KNOWN_REPORT,
222
225
  knownList: KNOWN_REPORT_FIELDS,
223
226
  defaultSelect: DEFAULT_REPORT_SELECT,
@@ -274,16 +277,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
274
277
  if (!result.complete) {
275
278
  return {
276
279
  [shape.key]: rows,
277
- count: { returned: rows.length, complete: false, reason: result.reason },
280
+ ...listTotals(rows.length, result.total, shape.noun, false),
281
+ complete: false,
282
+ reason: result.reason,
278
283
  cursor: result.cursor,
279
284
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
280
285
  };
281
286
  }
282
- const count = { returned: rows.length, complete: true };
283
287
  if (!rows.length) {
284
- return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
288
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
285
289
  }
286
- return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
290
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
287
291
  }
288
292
  export async function listAuthenticationMethods(session, flags, profile, operation, help, profileName) {
289
293
  const user = String(flags.user);