bt-cli 0.4.25__tar.gz → 0.4.27__tar.gz

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 (174) hide show
  1. {bt_cli-0.4.25 → bt_cli-0.4.27}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.25 → bt_cli-0.4.27}/PKG-INFO +1 -1
  3. {bt_cli-0.4.25 → bt_cli-0.4.27}/pyproject.toml +1 -1
  4. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/client/base.py +49 -5
  6. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/jump_items.py +203 -27
  7. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/client/beyondinsight.py +86 -0
  8. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/__init__.py +2 -1
  9. bt_cli-0.4.27/src/bt_cli/pws/commands/attributes.py +266 -0
  10. {bt_cli-0.4.25 → bt_cli-0.4.27}/.claude/skills/bt/SKILL.md +0 -0
  11. {bt_cli-0.4.25 → bt_cli-0.4.27}/.claude/skills/entitle/SKILL.md +0 -0
  12. {bt_cli-0.4.25 → bt_cli-0.4.27}/.claude/skills/epmw/SKILL.md +0 -0
  13. {bt_cli-0.4.25 → bt_cli-0.4.27}/.claude/skills/pra/SKILL.md +0 -0
  14. {bt_cli-0.4.25 → bt_cli-0.4.27}/.claude/skills/pws/SKILL.md +0 -0
  15. {bt_cli-0.4.25 → bt_cli-0.4.27}/.env.example +0 -0
  16. {bt_cli-0.4.25 → bt_cli-0.4.27}/.github/workflows/ci.yml +0 -0
  17. {bt_cli-0.4.25 → bt_cli-0.4.27}/.github/workflows/release.yml +0 -0
  18. {bt_cli-0.4.25 → bt_cli-0.4.27}/.gitignore +0 -0
  19. {bt_cli-0.4.25 → bt_cli-0.4.27}/README.md +0 -0
  20. {bt_cli-0.4.25 → bt_cli-0.4.27}/assets/cli-help.png +0 -0
  21. {bt_cli-0.4.25 → bt_cli-0.4.27}/assets/cli-output.png +0 -0
  22. {bt_cli-0.4.25 → bt_cli-0.4.27}/bt-cli.spec +0 -0
  23. {bt_cli-0.4.25 → bt_cli-0.4.27}/bt_entry.py +0 -0
  24. {bt_cli-0.4.25 → bt_cli-0.4.27}/scripts/bt_entry.py +0 -0
  25. {bt_cli-0.4.25 → bt_cli-0.4.27}/scripts/sync-package-data.sh +0 -0
  26. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/cli.py +0 -0
  27. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/commands/__init__.py +0 -0
  28. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/commands/configure.py +0 -0
  29. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/commands/learn.py +0 -0
  30. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/commands/quick.py +0 -0
  31. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/__init__.py +0 -0
  32. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/auth.py +0 -0
  33. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/client.py +0 -0
  34. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/config.py +0 -0
  35. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/config_file.py +0 -0
  36. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/csv_utils.py +0 -0
  37. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/errors.py +0 -0
  38. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/output.py +0 -0
  39. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/prompts.py +0 -0
  40. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/core/rest_debug.py +0 -0
  41. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/CLAUDE.md +0 -0
  42. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/__init__.py +0 -0
  43. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  44. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  45. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  46. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  47. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  48. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/__init__.py +0 -0
  49. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/client/__init__.py +0 -0
  50. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/client/base.py +0 -0
  51. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/__init__.py +0 -0
  52. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/accounts.py +0 -0
  53. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/applications.py +0 -0
  54. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/auth.py +0 -0
  55. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/bundles.py +0 -0
  56. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/integrations.py +0 -0
  57. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/permissions.py +0 -0
  58. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/policies.py +0 -0
  59. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/resources.py +0 -0
  60. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/roles.py +0 -0
  61. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/users.py +0 -0
  62. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/commands/workflows.py +0 -0
  63. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/__init__.py +0 -0
  64. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/bundle.py +0 -0
  65. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/common.py +0 -0
  66. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/integration.py +0 -0
  67. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/permission.py +0 -0
  68. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/policy.py +0 -0
  69. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/resource.py +0 -0
  70. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/role.py +0 -0
  71. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/user.py +0 -0
  72. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/entitle/models/workflow.py +0 -0
  73. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/__init__.py +0 -0
  74. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/client/__init__.py +0 -0
  75. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/client/base.py +0 -0
  76. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/__init__.py +0 -0
  77. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/audits.py +0 -0
  78. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/auth.py +0 -0
  79. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/computers.py +0 -0
  80. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/events.py +0 -0
  81. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/groups.py +0 -0
  82. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/policies.py +0 -0
  83. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/quick.py +0 -0
  84. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/requests.py +0 -0
  85. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/roles.py +0 -0
  86. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/tasks.py +0 -0
  87. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/commands/users.py +0 -0
  88. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/epmw/models/__init__.py +0 -0
  89. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/__init__.py +0 -0
  90. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/client/__init__.py +0 -0
  91. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/__init__.py +0 -0
  92. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/auth.py +0 -0
  93. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/import_export.py +0 -0
  94. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  95. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  96. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  97. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/policies.py +0 -0
  98. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/quick.py +0 -0
  99. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/teams.py +0 -0
  100. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/users.py +0 -0
  101. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/commands/vault.py +0 -0
  102. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/__init__.py +0 -0
  103. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/common.py +0 -0
  104. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/jump_client.py +0 -0
  105. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/jump_group.py +0 -0
  106. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/jump_item.py +0 -0
  107. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/jumpoint.py +0 -0
  108. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/team.py +0 -0
  109. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/user.py +0 -0
  110. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pra/models/vault.py +0 -0
  111. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/__init__.py +0 -0
  112. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/client/__init__.py +0 -0
  113. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/client/base.py +0 -0
  114. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  115. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/accounts.py +0 -0
  116. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/assets.py +0 -0
  117. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/auth.py +0 -0
  118. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/clouds.py +0 -0
  119. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/config.py +0 -0
  120. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/credentials.py +0 -0
  121. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/databases.py +0 -0
  122. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/directories.py +0 -0
  123. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/functional.py +0 -0
  124. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/import_export.py +0 -0
  125. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/platforms.py +0 -0
  126. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/quick.py +0 -0
  127. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/search.py +0 -0
  128. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/secrets.py +0 -0
  129. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/systems.py +0 -0
  130. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/users.py +0 -0
  131. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/commands/workgroups.py +0 -0
  132. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/config.py +0 -0
  133. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/models/__init__.py +0 -0
  134. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/models/account.py +0 -0
  135. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/models/asset.py +0 -0
  136. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/models/common.py +0 -0
  137. {bt_cli-0.4.25 → bt_cli-0.4.27}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.25 → bt_cli-0.4.27}/tests/pws-smoke-test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.25**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.27**
4
4
 
5
5
  ## Setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.25
3
+ Version: 0.4.27
4
4
  Summary: BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM
5
5
  Author-email: Dave Grendysz <dgrendysz@beyondtrust.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.25"
7
+ version = "0.4.27"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.25"
3
+ __version__ = "0.4.27"
@@ -278,13 +278,22 @@ class PRAClient:
278
278
  self,
279
279
  jump_group_id: Optional[int] = None,
280
280
  jumpoint_id: Optional[int] = None,
281
+ tag: Optional[str] = None,
282
+ name: Optional[str] = None,
283
+ hostname: Optional[str] = None,
281
284
  ) -> List[Dict[str, Any]]:
282
- """List Shell Jump items."""
285
+ """List Shell Jump items with optional filters."""
283
286
  params = {}
284
287
  if jump_group_id:
285
288
  params["jump_group_id"] = jump_group_id
286
289
  if jumpoint_id:
287
290
  params["jumpoint_id"] = jumpoint_id
291
+ if tag:
292
+ params["tag"] = tag
293
+ if name:
294
+ params["name"] = name
295
+ if hostname:
296
+ params["hostname"] = hostname
288
297
  return self.get_paginated("/jump-item/shell-jump", params)
289
298
 
290
299
  def get_shell_jump(self, item_id: int) -> Dict[str, Any]:
@@ -375,13 +384,22 @@ class PRAClient:
375
384
  self,
376
385
  jump_group_id: Optional[int] = None,
377
386
  jumpoint_id: Optional[int] = None,
387
+ tag: Optional[str] = None,
388
+ name: Optional[str] = None,
389
+ hostname: Optional[str] = None,
378
390
  ) -> List[Dict[str, Any]]:
379
- """List Remote RDP Jump items."""
391
+ """List Remote RDP Jump items with optional filters."""
380
392
  params = {}
381
393
  if jump_group_id:
382
394
  params["jump_group_id"] = jump_group_id
383
395
  if jumpoint_id:
384
396
  params["jumpoint_id"] = jumpoint_id
397
+ if tag:
398
+ params["tag"] = tag
399
+ if name:
400
+ params["name"] = name
401
+ if hostname:
402
+ params["hostname"] = hostname
385
403
  return self.get_paginated("/jump-item/remote-rdp", params)
386
404
 
387
405
  def get_rdp_jump(self, item_id: int) -> Dict[str, Any]:
@@ -423,21 +441,36 @@ class PRAClient:
423
441
  def list_vnc_jumps(
424
442
  self,
425
443
  jump_group_id: Optional[int] = None,
444
+ tag: Optional[str] = None,
445
+ name: Optional[str] = None,
446
+ hostname: Optional[str] = None,
426
447
  ) -> List[Dict[str, Any]]:
427
- """List Remote VNC Jump items."""
448
+ """List Remote VNC Jump items with optional filters."""
428
449
  params = {}
429
450
  if jump_group_id:
430
451
  params["jump_group_id"] = jump_group_id
452
+ if tag:
453
+ params["tag"] = tag
454
+ if name:
455
+ params["name"] = name
456
+ if hostname:
457
+ params["hostname"] = hostname
431
458
  return self.get_paginated("/jump-item/remote-vnc", params)
432
459
 
433
460
  def list_web_jumps(
434
461
  self,
435
462
  jump_group_id: Optional[int] = None,
463
+ tag: Optional[str] = None,
464
+ name: Optional[str] = None,
436
465
  ) -> List[Dict[str, Any]]:
437
- """List Web Jump items."""
466
+ """List Web Jump items with optional filters."""
438
467
  params = {}
439
468
  if jump_group_id:
440
469
  params["jump_group_id"] = jump_group_id
470
+ if tag:
471
+ params["tag"] = tag
472
+ if name:
473
+ params["name"] = name
441
474
  return self.get_paginated("/jump-item/web-jump", params)
442
475
 
443
476
  def get_web_jump(self, item_id: int) -> Dict[str, Any]:
@@ -452,11 +485,22 @@ class PRAClient:
452
485
  self,
453
486
  jump_group_id: Optional[int] = None,
454
487
  tunnel_type: Optional[str] = None,
488
+ tag: Optional[str] = None,
489
+ name: Optional[str] = None,
490
+ hostname: Optional[str] = None,
455
491
  ) -> List[Dict[str, Any]]:
456
- """List Protocol Tunnel Jump items (TCP, MSSQL, K8s)."""
492
+ """List Protocol Tunnel Jump items (TCP, MSSQL, K8s) with optional filters."""
457
493
  params = {}
458
494
  if jump_group_id:
459
495
  params["jump_group_id"] = jump_group_id
496
+ if tunnel_type:
497
+ params["tunnel_type"] = tunnel_type
498
+ if tag:
499
+ params["tag"] = tag
500
+ if name:
501
+ params["name"] = name
502
+ if hostname:
503
+ params["hostname"] = hostname
460
504
  return self.get_paginated("/jump-item/protocol-tunnel-jump", params)
461
505
 
462
506
  def get_protocol_tunnel(self, item_id: int) -> Dict[str, Any]:
@@ -1,12 +1,29 @@
1
1
  """Jump Item commands (shell, RDP, VNC, web, tunnels)."""
2
2
 
3
- from typing import Optional
3
+ from fnmatch import fnmatch
4
+ from typing import List, Optional
4
5
 
5
6
  import httpx
6
7
  import typer
7
8
 
8
9
  from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_success, print_api_error
9
10
 
11
+
12
+ def _filter_by_pattern(items: List[dict], pattern: str, field: str = "name") -> List[dict]:
13
+ """Filter items by fnmatch pattern on a field."""
14
+ return [item for item in items if fnmatch(item.get(field, "").lower(), pattern.lower())]
15
+
16
+
17
+ def _find_by_name(items: List[dict], name: str) -> Optional[dict]:
18
+ """Find a single item by exact name match (case-insensitive)."""
19
+ name_lower = name.lower()
20
+ matches = [item for item in items if item.get("name", "").lower() == name_lower]
21
+ if len(matches) == 1:
22
+ return matches[0]
23
+ elif len(matches) > 1:
24
+ raise ValueError(f"Multiple items found with name '{name}'. Use ID instead.")
25
+ return None
26
+
10
27
  app = typer.Typer(no_args_is_help=True)
11
28
 
12
29
  # Shell Jump subcommands
@@ -18,6 +35,10 @@ app.add_typer(shell_app, name="shell")
18
35
  def list_shell_jumps(
19
36
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
20
37
  jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Filter by Jumpoint"),
38
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
39
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
40
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
41
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
21
42
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
22
43
  ):
23
44
  """List Shell Jump items."""
@@ -25,7 +46,17 @@ def list_shell_jumps(
25
46
 
26
47
  try:
27
48
  client = get_client()
28
- items = client.list_shell_jumps(jump_group_id=jump_group_id, jumpoint_id=jumpoint_id)
49
+ items = client.list_shell_jumps(
50
+ jump_group_id=jump_group_id,
51
+ jumpoint_id=jumpoint_id,
52
+ tag=tag,
53
+ name=name,
54
+ hostname=hostname,
55
+ )
56
+
57
+ # Client-side pattern filtering
58
+ if name_pattern:
59
+ items = _filter_by_pattern(items, name_pattern)
29
60
 
30
61
  if output == OutputFormat.JSON:
31
62
  print_json(items)
@@ -197,19 +228,44 @@ def update_shell_jump(
197
228
 
198
229
  @shell_app.command("delete")
199
230
  def delete_shell_jump(
200
- item_id: int = typer.Argument(..., help="Shell Jump ID"),
231
+ item_id: Optional[int] = typer.Argument(None, help="Shell Jump ID"),
232
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
201
233
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
234
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
202
235
  ):
203
- """Delete a Shell Jump item."""
236
+ """Delete a Shell Jump item by ID or name."""
204
237
  from bt_cli.pra.client import get_client
205
238
 
206
- if not force:
207
- typer.confirm(f"Delete shell jump {item_id}?", abort=True)
239
+ if not item_id and not name:
240
+ print_error("Either ITEM_ID or --name is required")
241
+ raise typer.Exit(1)
208
242
 
209
243
  try:
210
244
  client = get_client()
245
+
246
+ # Look up by name if provided
247
+ if name and not item_id:
248
+ items = client.list_shell_jumps(name=name)
249
+ if not items:
250
+ print_error(f"No shell jump found with name '{name}'")
251
+ raise typer.Exit(1)
252
+ if len(items) > 1:
253
+ print_error(f"Multiple shell jumps found with name '{name}'. Use ID instead.")
254
+ raise typer.Exit(1)
255
+ item = items[0]
256
+ item_id = item.get("id")
257
+ else:
258
+ item = client.get_shell_jump(item_id)
259
+
260
+ if dry_run:
261
+ print_success(f"[DRY-RUN] Would delete shell jump: {item.get('name')} (ID: {item_id})")
262
+ return
263
+
264
+ if not force:
265
+ typer.confirm(f"Delete shell jump '{item.get('name')}' (ID: {item_id})?", abort=True)
266
+
211
267
  client.delete_shell_jump(item_id)
212
- print_success(f"Deleted shell jump {item_id}")
268
+ print_success(f"Deleted shell jump '{item.get('name')}' (ID: {item_id})")
213
269
  except httpx.HTTPStatusError as e:
214
270
  print_api_error(e, "delete shell jump")
215
271
  raise typer.Exit(1)
@@ -230,6 +286,10 @@ app.add_typer(rdp_app, name="rdp")
230
286
  def list_rdp_jumps(
231
287
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
232
288
  jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Filter by Jumpoint"),
289
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
290
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
291
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
292
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
233
293
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
234
294
  ):
235
295
  """List Remote RDP Jump items."""
@@ -237,7 +297,16 @@ def list_rdp_jumps(
237
297
 
238
298
  try:
239
299
  client = get_client()
240
- items = client.list_rdp_jumps(jump_group_id=jump_group_id, jumpoint_id=jumpoint_id)
300
+ items = client.list_rdp_jumps(
301
+ jump_group_id=jump_group_id,
302
+ jumpoint_id=jumpoint_id,
303
+ tag=tag,
304
+ name=name,
305
+ hostname=hostname,
306
+ )
307
+
308
+ if name_pattern:
309
+ items = _filter_by_pattern(items, name_pattern)
241
310
 
242
311
  if output == OutputFormat.JSON:
243
312
  print_json(items)
@@ -356,19 +425,43 @@ def create_rdp_jump(
356
425
 
357
426
  @rdp_app.command("delete")
358
427
  def delete_rdp_jump(
359
- item_id: int = typer.Argument(..., help="RDP Jump ID"),
428
+ item_id: Optional[int] = typer.Argument(None, help="RDP Jump ID"),
429
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
360
430
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
431
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
361
432
  ):
362
- """Delete a Remote RDP Jump item."""
433
+ """Delete a Remote RDP Jump item by ID or name."""
363
434
  from bt_cli.pra.client import get_client
364
435
 
365
- if not force:
366
- typer.confirm(f"Delete RDP jump {item_id}?", abort=True)
436
+ if not item_id and not name:
437
+ print_error("Either ITEM_ID or --name is required")
438
+ raise typer.Exit(1)
367
439
 
368
440
  try:
369
441
  client = get_client()
442
+
443
+ if name and not item_id:
444
+ items = client.list_rdp_jumps(name=name)
445
+ if not items:
446
+ print_error(f"No RDP jump found with name '{name}'")
447
+ raise typer.Exit(1)
448
+ if len(items) > 1:
449
+ print_error(f"Multiple RDP jumps found with name '{name}'. Use ID instead.")
450
+ raise typer.Exit(1)
451
+ item = items[0]
452
+ item_id = item.get("id")
453
+ else:
454
+ item = client.get_rdp_jump(item_id)
455
+
456
+ if dry_run:
457
+ print_success(f"[DRY-RUN] Would delete RDP jump: {item.get('name')} (ID: {item_id})")
458
+ return
459
+
460
+ if not force:
461
+ typer.confirm(f"Delete RDP jump '{item.get('name')}' (ID: {item_id})?", abort=True)
462
+
370
463
  client.delete_rdp_jump(item_id)
371
- print_success(f"Deleted RDP jump {item_id}")
464
+ print_success(f"Deleted RDP jump '{item.get('name')}' (ID: {item_id})")
372
465
  except httpx.HTTPStatusError as e:
373
466
  print_api_error(e, "delete RDP jump")
374
467
  raise typer.Exit(1)
@@ -388,6 +481,10 @@ app.add_typer(vnc_app, name="vnc")
388
481
  @vnc_app.command("list")
389
482
  def list_vnc_jumps(
390
483
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
484
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
485
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
486
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
487
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
391
488
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
392
489
  ):
393
490
  """List Remote VNC Jump items."""
@@ -395,7 +492,15 @@ def list_vnc_jumps(
395
492
 
396
493
  try:
397
494
  client = get_client()
398
- items = client.list_vnc_jumps(jump_group_id=jump_group_id)
495
+ items = client.list_vnc_jumps(
496
+ jump_group_id=jump_group_id,
497
+ tag=tag,
498
+ name=name,
499
+ hostname=hostname,
500
+ )
501
+
502
+ if name_pattern:
503
+ items = _filter_by_pattern(items, name_pattern)
399
504
 
400
505
  if output == OutputFormat.JSON:
401
506
  print_json(items)
@@ -427,6 +532,9 @@ app.add_typer(web_app, name="web")
427
532
  @web_app.command("list")
428
533
  def list_web_jumps(
429
534
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
535
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
536
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
537
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
430
538
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
431
539
  ):
432
540
  """List Web Jump items."""
@@ -434,7 +542,14 @@ def list_web_jumps(
434
542
 
435
543
  try:
436
544
  client = get_client()
437
- items = client.list_web_jumps(jump_group_id=jump_group_id)
545
+ items = client.list_web_jumps(
546
+ jump_group_id=jump_group_id,
547
+ tag=tag,
548
+ name=name,
549
+ )
550
+
551
+ if name_pattern:
552
+ items = _filter_by_pattern(items, name_pattern)
438
553
 
439
554
  if output == OutputFormat.JSON:
440
555
  print_json(items)
@@ -498,19 +613,43 @@ def get_web_jump(
498
613
 
499
614
  @web_app.command("delete")
500
615
  def delete_web_jump(
501
- item_id: int = typer.Argument(..., help="Web Jump ID"),
616
+ item_id: Optional[int] = typer.Argument(None, help="Web Jump ID"),
617
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
502
618
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
619
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
503
620
  ):
504
- """Delete a Web Jump item."""
621
+ """Delete a Web Jump item by ID or name."""
505
622
  from bt_cli.pra.client import get_client
506
623
 
507
- if not force:
508
- typer.confirm(f"Delete web jump {item_id}?", abort=True)
624
+ if not item_id and not name:
625
+ print_error("Either ITEM_ID or --name is required")
626
+ raise typer.Exit(1)
509
627
 
510
628
  try:
511
629
  client = get_client()
630
+
631
+ if name and not item_id:
632
+ items = client.list_web_jumps(name=name)
633
+ if not items:
634
+ print_error(f"No web jump found with name '{name}'")
635
+ raise typer.Exit(1)
636
+ if len(items) > 1:
637
+ print_error(f"Multiple web jumps found with name '{name}'. Use ID instead.")
638
+ raise typer.Exit(1)
639
+ item = items[0]
640
+ item_id = item.get("id")
641
+ else:
642
+ item = client.get_web_jump(item_id)
643
+
644
+ if dry_run:
645
+ print_success(f"[DRY-RUN] Would delete web jump: {item.get('name')} (ID: {item_id})")
646
+ return
647
+
648
+ if not force:
649
+ typer.confirm(f"Delete web jump '{item.get('name')}' (ID: {item_id})?", abort=True)
650
+
512
651
  client.delete_web_jump(item_id)
513
- print_success(f"Deleted web jump {item_id}")
652
+ print_success(f"Deleted web jump '{item.get('name')}' (ID: {item_id})")
514
653
  except httpx.HTTPStatusError as e:
515
654
  print_api_error(e, "delete web jump")
516
655
  raise typer.Exit(1)
@@ -530,7 +669,11 @@ app.add_typer(tunnel_app, name="tunnel")
530
669
  @tunnel_app.command("list")
531
670
  def list_protocol_tunnels(
532
671
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
533
- tunnel_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type: tcp, mssql, psql, mysql, k8s"),
672
+ tunnel_type: Optional[str] = typer.Option(None, "--type", help="Filter by type: tcp, mssql, psql, mysql, k8s"),
673
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
674
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
675
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
676
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
534
677
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
535
678
  ):
536
679
  """List Protocol Tunnel Jump items."""
@@ -538,7 +681,16 @@ def list_protocol_tunnels(
538
681
 
539
682
  try:
540
683
  client = get_client()
541
- items = client.list_protocol_tunnels(jump_group_id=jump_group_id, tunnel_type=tunnel_type)
684
+ items = client.list_protocol_tunnels(
685
+ jump_group_id=jump_group_id,
686
+ tunnel_type=tunnel_type,
687
+ tag=tag,
688
+ name=name,
689
+ hostname=hostname,
690
+ )
691
+
692
+ if name_pattern:
693
+ items = _filter_by_pattern(items, name_pattern)
542
694
 
543
695
  if output == OutputFormat.JSON:
544
696
  print_json(items)
@@ -678,19 +830,43 @@ def create_protocol_tunnel(
678
830
 
679
831
  @tunnel_app.command("delete")
680
832
  def delete_protocol_tunnel(
681
- item_id: int = typer.Argument(..., help="Protocol Tunnel ID"),
833
+ item_id: Optional[int] = typer.Argument(None, help="Protocol Tunnel ID"),
834
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
682
835
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
836
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
683
837
  ):
684
- """Delete a Protocol Tunnel Jump item."""
838
+ """Delete a Protocol Tunnel Jump item by ID or name."""
685
839
  from bt_cli.pra.client import get_client
686
840
 
687
- if not force:
688
- typer.confirm(f"Delete protocol tunnel {item_id}?", abort=True)
841
+ if not item_id and not name:
842
+ print_error("Either ITEM_ID or --name is required")
843
+ raise typer.Exit(1)
689
844
 
690
845
  try:
691
846
  client = get_client()
847
+
848
+ if name and not item_id:
849
+ items = client.list_protocol_tunnels(name=name)
850
+ if not items:
851
+ print_error(f"No protocol tunnel found with name '{name}'")
852
+ raise typer.Exit(1)
853
+ if len(items) > 1:
854
+ print_error(f"Multiple protocol tunnels found with name '{name}'. Use ID instead.")
855
+ raise typer.Exit(1)
856
+ item = items[0]
857
+ item_id = item.get("id")
858
+ else:
859
+ item = client.get_protocol_tunnel(item_id)
860
+
861
+ if dry_run:
862
+ print_success(f"[DRY-RUN] Would delete protocol tunnel: {item.get('name')} (ID: {item_id})")
863
+ return
864
+
865
+ if not force:
866
+ typer.confirm(f"Delete protocol tunnel '{item.get('name')}' (ID: {item_id})?", abort=True)
867
+
692
868
  client.delete_protocol_tunnel(item_id)
693
- print_success(f"Deleted protocol tunnel {item_id}")
869
+ print_success(f"Deleted protocol tunnel '{item.get('name')}' (ID: {item_id})")
694
870
  except httpx.HTTPStatusError as e:
695
871
  print_api_error(e, "delete protocol tunnel")
696
872
  raise typer.Exit(1)
@@ -867,3 +867,89 @@ class BeyondInsightMixin:
867
867
  Attribute object
868
868
  """
869
869
  return self.get(f"/Attributes/{attribute_id}")
870
+
871
+ def get_managed_system_attributes(
872
+ self: "PasswordSafeClient",
873
+ managed_system_id: int,
874
+ ) -> list[dict[str, Any]]:
875
+ """Get attributes for a managed system.
876
+
877
+ Args:
878
+ managed_system_id: Managed system ID
879
+
880
+ Returns:
881
+ List of attribute objects assigned to this system
882
+ """
883
+ return self.get(f"/ManagedSystems/{managed_system_id}/Attributes")
884
+
885
+ def assign_managed_system_attribute(
886
+ self: "PasswordSafeClient",
887
+ managed_system_id: int,
888
+ attribute_id: int,
889
+ ) -> dict[str, Any]:
890
+ """Assign an attribute to a managed system.
891
+
892
+ Args:
893
+ managed_system_id: Managed system ID
894
+ attribute_id: Attribute ID to assign
895
+
896
+ Returns:
897
+ The created assignment
898
+ """
899
+ return self.post(f"/ManagedSystems/{managed_system_id}/Attributes/{attribute_id}")
900
+
901
+ def remove_managed_system_attribute(
902
+ self: "PasswordSafeClient",
903
+ managed_system_id: int,
904
+ attribute_id: int,
905
+ ) -> None:
906
+ """Remove an attribute from a managed system.
907
+
908
+ Args:
909
+ managed_system_id: Managed system ID
910
+ attribute_id: Attribute ID to remove
911
+ """
912
+ self.delete(f"/ManagedSystems/{managed_system_id}/Attributes/{attribute_id}")
913
+
914
+ def get_managed_account_attributes(
915
+ self: "PasswordSafeClient",
916
+ account_id: int,
917
+ ) -> list[dict[str, Any]]:
918
+ """Get attributes for a managed account.
919
+
920
+ Args:
921
+ account_id: Managed account ID
922
+
923
+ Returns:
924
+ List of attribute objects assigned to this account
925
+ """
926
+ return self.get(f"/ManagedAccounts/{account_id}/Attributes")
927
+
928
+ def assign_managed_account_attribute(
929
+ self: "PasswordSafeClient",
930
+ account_id: int,
931
+ attribute_id: int,
932
+ ) -> dict[str, Any]:
933
+ """Assign an attribute to a managed account.
934
+
935
+ Args:
936
+ account_id: Managed account ID
937
+ attribute_id: Attribute ID to assign
938
+
939
+ Returns:
940
+ The created assignment
941
+ """
942
+ return self.post(f"/ManagedAccounts/{account_id}/Attributes/{attribute_id}")
943
+
944
+ def remove_managed_account_attribute(
945
+ self: "PasswordSafeClient",
946
+ account_id: int,
947
+ attribute_id: int,
948
+ ) -> None:
949
+ """Remove an attribute from a managed account.
950
+
951
+ Args:
952
+ account_id: Managed account ID
953
+ attribute_id: Attribute ID to remove
954
+ """
955
+ self.delete(f"/ManagedAccounts/{account_id}/Attributes/{attribute_id}")
@@ -10,7 +10,7 @@ app = typer.Typer(
10
10
 
11
11
  # Import and register command groups
12
12
  from . import auth, systems, accounts, assets, workgroups, platforms, users
13
- from . import credentials, config, databases, directories, clouds, secrets, quick, functional, search
13
+ from . import credentials, config, databases, directories, clouds, secrets, quick, functional, search, attributes
14
14
  from .import_export import import_app, export_app
15
15
 
16
16
  app.add_typer(auth.app, name="auth", help="Authentication commands")
@@ -29,5 +29,6 @@ app.add_typer(secrets.app, name="secrets", help="Secrets Safe (folders and secre
29
29
  app.add_typer(functional.app, name="functional", help="Functional accounts for auto-management")
30
30
  app.add_typer(users.app, name="users", help="Manage users, user groups, and roles")
31
31
  app.add_typer(quick.app, name="quick", help="Quick commands - common multi-step operations")
32
+ app.add_typer(attributes.app, name="attributes", help="Manage attributes for systems/accounts")
32
33
  app.add_typer(import_app, name="import", help="Import resources from CSV")
33
34
  app.add_typer(export_app, name="export", help="Export sample CSV templates")