wealthbox-cli 1.2.0__tar.gz → 1.4.0__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 (128) hide show
  1. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.claude-plugin/marketplace.json +1 -1
  2. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.codex-plugin/plugin.json +1 -1
  3. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/PKG-INFO +1 -1
  4. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/changelog.md +16 -0
  5. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/.claude-plugin/plugin.json +1 -1
  6. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/pyproject.toml +1 -1
  7. wealthbox_cli-1.4.0/src/wealthbox_tools/cli/_skill_platforms.py +235 -0
  8. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_util.py +26 -0
  9. wealthbox_cli-1.4.0/src/wealthbox_tools/cli/doctor.py +203 -0
  10. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/main.py +7 -0
  11. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/skills.py +28 -54
  12. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/tasks.py +28 -7
  13. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +4 -1
  14. wealthbox_cli-1.2.0/src/wealthbox_tools/cli/_skill_platforms.py +0 -107
  15. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.env.example +0 -0
  16. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.github/workflows/ci.yml +0 -0
  17. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.github/workflows/docs.yml +0 -0
  18. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.gitignore +0 -0
  19. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/CONTRIBUTING.md +0 -0
  20. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/LICENSE +0 -0
  21. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/README.md +0 -0
  22. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/SECURITY.md +0 -0
  23. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/SKILL.md +0 -0
  24. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/bootstrap.md +0 -0
  25. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/contacts.md +0 -0
  26. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/events.md +0 -0
  27. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/notes.md +0 -0
  28. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/opportunities.md +0 -0
  29. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/projects.md +0 -0
  30. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/tasks.md +0 -0
  31. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/workflows.md +0 -0
  32. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/contacts.md +0 -0
  33. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/events.md +0 -0
  34. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/households.md +0 -0
  35. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/lookups.md +0 -0
  36. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/notes.md +0 -0
  37. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/opportunities.md +0 -0
  38. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/projects.md +0 -0
  39. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/tasks.md +0 -0
  40. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/workflows.md +0 -0
  41. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/cli-reference.md +0 -0
  42. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/getting-started.md +0 -0
  43. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/index.md +0 -0
  44. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/overrides/main.html +0 -0
  45. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/promotion.md +0 -0
  46. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/mkdocs.yml +0 -0
  47. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/SKILL.md +0 -0
  48. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/bootstrap.md +0 -0
  49. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
  50. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/events.md +0 -0
  51. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/notes.md +0 -0
  52. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
  53. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/projects.md +0 -0
  54. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
  55. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
  56. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/contacts.md +0 -0
  57. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/events.md +0 -0
  58. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/households.md +0 -0
  59. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/lookups.md +0 -0
  60. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/notes.md +0 -0
  61. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/opportunities.md +0 -0
  62. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/projects.md +0 -0
  63. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/tasks.md +0 -0
  64. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/workflows.md +0 -0
  65. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/__init__.py +0 -0
  66. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/__init__.py +0 -0
  67. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_config.py +0 -0
  68. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_skill_bootstrap.py +0 -0
  69. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_skill_paths.py +0 -0
  70. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/activity.py +0 -0
  71. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/categories.py +0 -0
  72. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/config.py +0 -0
  73. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/contacts.py +0 -0
  74. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/events.py +0 -0
  75. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/households.py +0 -0
  76. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/me.py +0 -0
  77. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/notes.py +0 -0
  78. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/opportunities.py +0 -0
  79. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/projects.py +0 -0
  80. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/users.py +0 -0
  81. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/workflows.py +0 -0
  82. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/__init__.py +0 -0
  83. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/activity.py +0 -0
  84. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/base.py +0 -0
  85. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/categories.py +0 -0
  86. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/comments.py +0 -0
  87. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/contacts.py +0 -0
  88. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/events.py +0 -0
  89. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/households.py +0 -0
  90. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/me.py +0 -0
  91. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/notes.py +0 -0
  92. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/opportunities.py +0 -0
  93. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/projects.py +0 -0
  94. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/tasks.py +0 -0
  95. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/users.py +0 -0
  96. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/workflows.py +0 -0
  97. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/__init__.py +0 -0
  98. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/activity.py +0 -0
  99. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/comments.py +0 -0
  100. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/common.py +0 -0
  101. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/contacts.py +0 -0
  102. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/custom_fields.py +0 -0
  103. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/enums.py +0 -0
  104. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/events.py +0 -0
  105. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/households.py +0 -0
  106. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/notes.py +0 -0
  107. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/opportunities.py +0 -0
  108. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/projects.py +0 -0
  109. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/tasks.py +0 -0
  110. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/workflows.py +0 -0
  111. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/py.typed +0 -0
  112. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +0 -0
  113. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +0 -0
  114. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
  115. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
  116. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
  117. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
  118. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
  119. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
  120. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
  121. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +0 -0
  122. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
  123. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
  124. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +0 -0
  125. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
  126. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
  127. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
  128. {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/workflows.md +0 -0
@@ -10,7 +10,7 @@
10
10
  "name": "wealthbox-crm",
11
11
  "source": "./plugins/wealthbox-crm",
12
12
  "description": "Wealthbox CRM agent skill — natural-language CRM automation backed by the `wbox` CLI.",
13
- "version": "1.2.0",
13
+ "version": "1.3.0",
14
14
  "category": "productivity",
15
15
  "tags": [
16
16
  "wealthbox",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wealthbox-crm",
3
3
  "description": "Wealthbox CRM agent skill — drive contacts, tasks, notes, events, and households via natural language. Bundles SKILL.md, references, and firm-bootstrap workflow. Requires the `wbox` CLI (auto-install instructions in SKILL.md).",
4
- "version": "1.2.0",
4
+ "version": "1.4.0",
5
5
  "author": "Kadin Bullock",
6
6
  "homepage": "https://github.com/massive-value/wealthbox-cli",
7
7
  "repository": "https://github.com/massive-value/wealthbox-cli",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wealthbox-cli
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: CLI and client library for the Wealthbox CRM API
5
5
  Project-URL: Homepage, https://github.com/massive-value/wealthbox-cli
6
6
  Project-URL: Documentation, https://massive-value.github.io/wealthbox-cli/
@@ -6,6 +6,22 @@ This project uses [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.3.0](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.3.0) — 2026-05-03
10
+
11
+ ### Added
12
+
13
+ - **`wbox doctor`** — comprehensive top-level health check at the CLI root. Reports the wbox CLI version + Python version + binary location; authentication source detection (flag / env var / config file / `.env`) plus a smoke test against `/me`; agent CLI presence (`claude` / `codex` on PATH); legacy skill installs; plugin installs (managed via `claude plugin install` / `codex plugin install`); firm data state with file count, generated-vs-hand-edited split, and oldest-generated-file timestamp; and a Summary section listing actionable issues. `wbox skills doctor` keeps working as an alias of the new top-level command — both call the same function so output never drifts.
14
+ - `wbox skills list` and `wbox skills doctor` now detect plugin-installed copies of `wealthbox-crm` under `~/.claude/plugins/cache/.../skills/wealthbox-crm/` and `~/.codex/plugins/cache/.../skills/wealthbox-crm/`. Previously a user who installed via the marketplace plugin path saw "not installed" everywhere even though the plugin was actively serving the skill.
15
+ - Bootstrap installer (`scripts/install.sh` / `scripts/install.ps1`) now prefers the Claude Code plugin marketplace path when `claude` is on PATH — runs `claude plugin marketplace add` + `claude plugin install` directly, then offers a separate Codex install. Falls back to the legacy `wbox skills install` picker when no `claude` CLI is detected.
16
+ - Bootstrap installer pre-flight checks Windows PowerShell `ExecutionPolicy` and offers to set `RemoteSigned` for the current user if needed (no admin required), instead of bombing partway through with Astral's terse error.
17
+
18
+ ### Fixed
19
+
20
+ - Bootstrap `install.sh` no longer hangs at startup. The previous version did `exec </dev/tty` at the top, which cut off bash's source of piped script content (`curl … | bash`) and waited forever for the rest of the script to arrive on the user's keyboard. The whole body is now wrapped in a `main()` function so bash reads it fully before executing.
21
+ - Bootstrap `install.ps1` no longer auto-closes its window on error. Wrapped in try/catch/finally with explicit step headers and a "Press Enter to close" pause so a transient PowerShell host stays open long enough to show the actual error.
22
+
23
+ ---
24
+
9
25
  ## [1.2.0](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.2.0) — 2026-05-02
10
26
 
11
27
  ### Changed
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wealthbox-crm",
3
3
  "description": "Wealthbox CRM agent skill — drive contacts, tasks, notes, events, and households via natural language. Bundles SKILL.md, references, and firm-bootstrap workflow. Requires the `wbox` CLI (auto-install instructions in SKILL.md).",
4
- "version": "1.2.0",
4
+ "version": "1.4.0",
5
5
  "author": {
6
6
  "name": "Kadin Bullock"
7
7
  },
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wealthbox-cli"
7
- version = "1.2.0"
7
+ version = "1.4.0"
8
8
  description = "CLI and client library for the Wealthbox CRM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ SKILL_NAME = "wealthbox-crm"
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Platform:
13
+ id: str
14
+ label: str
15
+ root_dir: Path
16
+ skill_filename: str
17
+ requires_project_cwd: bool
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class PluginInstall:
22
+ """A copy of `wealthbox-crm` discovered inside an agent host's plugin cache.
23
+
24
+ These are managed by the host CLI (`claude plugin install ...` or the
25
+ Codex equivalent) — `wbox skills install` does not write here, and
26
+ `wbox skills uninstall` does not touch them.
27
+
28
+ `active` is True when the host's `installed_plugins.json` has a record
29
+ pointing at this version's installPath. Old/cached versions kept around
30
+ by the host for rollback report active=False. If the host has no
31
+ `installed_plugins.json` (e.g., older Claude Code, or a Codex setup that
32
+ hasn't shipped one yet), `active` defaults to True since we can't tell.
33
+ """
34
+ host: str # 'claude-code-user' | 'claude-code-project' | 'codex'
35
+ marketplace: str
36
+ version: str
37
+ skill_dir: Path
38
+ active: bool = True
39
+
40
+
41
+ def _home() -> Path:
42
+ return Path.home()
43
+
44
+
45
+ def _read_active_install_paths(plugins_root: Path) -> set[Path] | None:
46
+ """Read <plugins_root>/installed_plugins.json and return the set of
47
+ install-path roots the host considers active across every scope.
48
+
49
+ Returns None when no state file exists — caller should treat every
50
+ discovered plugin as active in that case (we don't have enough info
51
+ to differentiate cached vs active).
52
+ """
53
+ state_file = plugins_root / "installed_plugins.json"
54
+ if not state_file.is_file():
55
+ return None
56
+ try:
57
+ data = json.loads(state_file.read_text(encoding="utf-8"))
58
+ except (json.JSONDecodeError, OSError):
59
+ return None
60
+ active: set[Path] = set()
61
+ for entries in (data.get("plugins") or {}).values():
62
+ if not isinstance(entries, list):
63
+ continue
64
+ for entry in entries:
65
+ install_path = entry.get("installPath")
66
+ if not install_path:
67
+ continue
68
+ try:
69
+ active.add(Path(install_path).resolve())
70
+ except OSError:
71
+ active.add(Path(install_path))
72
+ return active
73
+
74
+
75
+ def _scan_plugin_cache(
76
+ cache_root: Path,
77
+ host: str,
78
+ active_paths: set[Path] | None,
79
+ ) -> list[PluginInstall]:
80
+ """Find any `<marketplace>/<plugin-name>/<version>/skills/wealthbox-crm/`
81
+ matches under a Claude Code- or Codex-style plugin cache root."""
82
+ if not cache_root.is_dir():
83
+ return []
84
+ out: list[PluginInstall] = []
85
+ for marketplace_dir in cache_root.iterdir():
86
+ if not marketplace_dir.is_dir():
87
+ continue
88
+ plugin_dir = marketplace_dir / SKILL_NAME
89
+ if not plugin_dir.is_dir():
90
+ continue
91
+ for version_dir in plugin_dir.iterdir():
92
+ if not version_dir.is_dir():
93
+ continue
94
+ skill_dir = version_dir / "skills" / SKILL_NAME
95
+ if not (skill_dir / "SKILL.md").is_file():
96
+ continue
97
+ try:
98
+ version_dir_resolved = version_dir.resolve()
99
+ except OSError:
100
+ version_dir_resolved = version_dir
101
+ if active_paths is None:
102
+ is_active = True
103
+ else:
104
+ is_active = version_dir_resolved in active_paths
105
+ out.append(PluginInstall(
106
+ host=host,
107
+ marketplace=marketplace_dir.name,
108
+ version=version_dir.name,
109
+ skill_dir=skill_dir,
110
+ active=is_active,
111
+ ))
112
+ return out
113
+
114
+
115
+ def detect_plugin_installs() -> list[PluginInstall]:
116
+ """Discover every host-managed plugin copy of wealthbox-crm on this
117
+ machine. Includes Claude Code user scope, Claude Code project scope
118
+ (cwd-relative), and Codex.
119
+
120
+ Deduped by resolved skill_dir path so a single install isn't reported
121
+ twice when home and cwd point at the same directory (rare in real use,
122
+ common in tests).
123
+ """
124
+ home = _home()
125
+ cwd = Path.cwd()
126
+ candidates = [
127
+ (home / ".claude" / "plugins", "claude-code-user"),
128
+ (cwd / ".claude" / "plugins", "claude-code-project"),
129
+ (home / ".codex" / "plugins", "codex"),
130
+ ]
131
+ installs: list[PluginInstall] = []
132
+ seen: set[Path] = set()
133
+ for plugins_root, host in candidates:
134
+ active_paths = _read_active_install_paths(plugins_root)
135
+ for pi in _scan_plugin_cache(
136
+ plugins_root / "cache", host=host, active_paths=active_paths,
137
+ ):
138
+ try:
139
+ key = pi.skill_dir.resolve()
140
+ except OSError:
141
+ key = pi.skill_dir
142
+ if key in seen:
143
+ continue
144
+ seen.add(key)
145
+ installs.append(pi)
146
+ return installs
147
+
148
+
149
+ def detect_platforms() -> list[Platform]:
150
+ home = _home()
151
+ cwd = Path.cwd()
152
+ return [
153
+ Platform(
154
+ id="claude-code-user",
155
+ label="Claude Code (user)",
156
+ root_dir=home / ".claude" / "skills",
157
+ skill_filename="SKILL.md",
158
+ requires_project_cwd=False,
159
+ ),
160
+ Platform(
161
+ id="claude-code-project",
162
+ label="Claude Code (current project)",
163
+ root_dir=cwd / ".claude" / "skills",
164
+ skill_filename="SKILL.md",
165
+ requires_project_cwd=True,
166
+ ),
167
+ Platform(
168
+ id="codex",
169
+ label="Codex",
170
+ root_dir=home / ".codex" / "skills",
171
+ skill_filename="SKILL.md",
172
+ requires_project_cwd=False,
173
+ ),
174
+ ]
175
+
176
+
177
+ def skill_dir(platform: Platform) -> Path:
178
+ return platform.root_dir / "wealthbox-crm"
179
+
180
+
181
+ class SkillInstallError(Exception):
182
+ """Raised when a skill install/uninstall cannot proceed safely."""
183
+
184
+
185
+ def is_installed(platform: Platform) -> bool:
186
+ dest = skill_dir(platform)
187
+ return dest.is_dir() and (dest / platform.skill_filename).is_file()
188
+
189
+
190
+ def install_skill(platform: Platform, src: Path, *, force: bool) -> None:
191
+ dest = skill_dir(platform)
192
+ if dest.exists() and not force:
193
+ raise SkillInstallError(
194
+ f"Skill already installed at {dest} (use --force to overwrite)"
195
+ )
196
+ if dest.exists():
197
+ _safe_rmtree(platform, dest)
198
+ dest.parent.mkdir(parents=True, exist_ok=True)
199
+ shutil.copytree(src, dest)
200
+
201
+
202
+ def upgrade_skill(platform: Platform, src: Path) -> None:
203
+ """Re-copy the bundled template over an existing install.
204
+
205
+ Preserves the user's `firm/` directory and root `_meta.json` (the
206
+ bundled template ships neither). Refreshes SKILL.md, references/,
207
+ firm-examples/, and bootstrap.md.
208
+ """
209
+ dest = skill_dir(platform)
210
+ if not dest.is_dir() or not (dest / platform.skill_filename).is_file():
211
+ raise SkillInstallError(
212
+ f"Cannot upgrade: nothing installed at {dest}"
213
+ )
214
+ shutil.copytree(src, dest, dirs_exist_ok=True)
215
+
216
+
217
+ def uninstall_skill(platform: Platform) -> None:
218
+ dest = skill_dir(platform)
219
+ if not dest.exists():
220
+ return
221
+ _safe_rmtree(platform, dest)
222
+
223
+
224
+ def _safe_rmtree(platform: Platform, target: Path) -> None:
225
+ root = platform.root_dir.resolve()
226
+ try:
227
+ resolved = target.resolve(strict=True)
228
+ except FileNotFoundError:
229
+ return
230
+ # Guard: resolved target must sit inside resolved root AND end with "wealthbox-crm".
231
+ if resolved.name != "wealthbox-crm" or root not in resolved.parents:
232
+ raise SkillInstallError(
233
+ f"refusing to remove {resolved}: not a standard skill path under {root}"
234
+ )
235
+ shutil.rmtree(resolved)
@@ -416,6 +416,32 @@ def build_resource_filter(
416
416
  return result
417
417
 
418
418
 
419
+ async def resolve_category_id(
420
+ client: WealthboxClient, category_type: CategoryType, value: str
421
+ ) -> int:
422
+ """Resolve a category name or numeric string to an integer ID.
423
+
424
+ If `value` is purely digits, returns int(value) without an API call.
425
+ Otherwise fetches all categories of the given type and case-insensitively
426
+ matches `name`. Raises typer.BadParameter on miss with available names.
427
+ """
428
+ if value.isdigit():
429
+ return int(value)
430
+ data = await client.list_all_categories(category_type)
431
+ items = data.get(category_type.value, [])
432
+ target = value.strip().casefold()
433
+ for item in items:
434
+ if str(item.get("name", "")).casefold() == target:
435
+ return int(item["id"])
436
+ available = sorted(str(i["name"]) for i in items if i.get("name"))
437
+ label = category_type.value.replace("_", " ")
438
+ if available:
439
+ raise typer.BadParameter(
440
+ f"Unknown {label} value '{value}'. Available: {', '.join(available)}"
441
+ )
442
+ raise typer.BadParameter(f"No {label} configured in this workspace.")
443
+
444
+
419
445
  def parse_more_fields(more_fields: str, reserved: set[str]) -> dict[str, Any]:
420
446
  """Parse --more-fields JSON and validate against reserved keys.
421
447
 
@@ -0,0 +1,203 @@
1
+ """Comprehensive health check across the wbox install.
2
+
3
+ `wbox doctor` and `wbox skills doctor` both call into `run_doctor()` so
4
+ the legacy `skills doctor` command keeps working as an alias.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import platform as _platform_module
10
+ import shutil
11
+ import sys
12
+ from datetime import datetime
13
+ from importlib.metadata import version as _pkg_version
14
+
15
+ import typer
16
+
17
+ from ._config import _config_path, load_config
18
+ from ._skill_bootstrap import migrate_legacy_firm, read_firm_meta, read_meta
19
+ from ._skill_paths import firm_dir, firm_meta_path
20
+ from ._skill_platforms import (
21
+ detect_platforms,
22
+ detect_plugin_installs,
23
+ is_installed,
24
+ skill_dir,
25
+ )
26
+
27
+
28
+ def _detect_token_source(token_arg: str | None) -> tuple[str | None, str]:
29
+ """Return (token, source_label) matching get_client's resolution order:
30
+ --token flag > WEALTHBOX_TOKEN env > config file > .env (loaded as env)."""
31
+ if token_arg:
32
+ return token_arg, "--token flag"
33
+ env_token = os.environ.get("WEALTHBOX_TOKEN")
34
+ if env_token:
35
+ return env_token, "env var (WEALTHBOX_TOKEN or .env)"
36
+ cfg = load_config()
37
+ cfg_token = cfg.get("token")
38
+ if cfg_token:
39
+ return cfg_token, f"config file ({_config_path()})"
40
+ return None, "not set"
41
+
42
+
43
+ def run_doctor(token: str | None = None) -> int:
44
+ """Run all health checks. Returns exit code (0 = healthy, 1 = issues found)."""
45
+ issues: list[str] = []
46
+
47
+ migrated_from = _ensure_firm_migrated()
48
+ if migrated_from is not None:
49
+ typer.echo(f"! migrated legacy firm/ data from {migrated_from} to {firm_dir()}")
50
+ typer.echo("")
51
+
52
+ # --- wbox CLI ---------------------------------------------------------
53
+ typer.echo("# wbox CLI")
54
+ cli_version = _pkg_version("wealthbox-cli")
55
+ py_version = ".".join(str(v) for v in sys.version_info[:3])
56
+ binary = shutil.which("wbox") or "(not on PATH -running via python -m)"
57
+ typer.echo(f" version: {cli_version}")
58
+ typer.echo(f" python: {py_version} ({_platform_module.system()})")
59
+ typer.echo(f" binary: {binary}")
60
+
61
+ # --- Authentication ---------------------------------------------------
62
+ typer.echo("\n# Authentication")
63
+ config_path = _config_path()
64
+ if config_path.exists():
65
+ cfg = load_config()
66
+ cfg_token = cfg.get("token", "")
67
+ masked = f"...{cfg_token[-4:]}" if len(cfg_token) > 4 else "****"
68
+ typer.echo(f" config: {config_path} (present, masked: {masked})")
69
+ else:
70
+ typer.echo(f" config: {config_path} (not present)")
71
+ resolved_token, source = _detect_token_source(token)
72
+ typer.echo(f" source: {source}")
73
+ if not resolved_token:
74
+ typer.echo(" smoke: skipped (no token)")
75
+ issues.append("token not configured - run 'wbox config set-token'")
76
+ else:
77
+ from wealthbox_tools.client import WealthboxAPIError
78
+
79
+ from ._util import run_client
80
+ try:
81
+ me = run_client(token, lambda c: c.get_me())
82
+ accounts = me.get("accounts") or []
83
+ firm_name = accounts[0].get("name") if accounts else "(no firm)"
84
+ typer.echo(
85
+ f" smoke: OK /me returned {me.get('name')} "
86
+ f"({firm_name}, user {me.get('id')})"
87
+ )
88
+ except WealthboxAPIError as e:
89
+ typer.echo(f" smoke: FAILED token rejected by /me: {e}")
90
+ issues.append("token rejected by /me - check it's still valid")
91
+ except Exception as e:
92
+ typer.echo(f" smoke: FAILED network/config error: {e}")
93
+ issues.append(f"smoke test failed: {e}")
94
+
95
+ # --- Agent CLIs -------------------------------------------------------
96
+ typer.echo("\n# Agent CLIs on PATH")
97
+ claude_path = shutil.which("claude") or shutil.which("claude.exe")
98
+ codex_path = shutil.which("codex") or shutil.which("codex.exe")
99
+ typer.echo(f" claude: {claude_path or 'not detected'}")
100
+ typer.echo(f" codex: {codex_path or 'not detected'}")
101
+
102
+ # --- Legacy skill installs --------------------------------------------
103
+ typer.echo("\n# Skill installs (legacy template-copy)")
104
+ for p in detect_platforms():
105
+ status = "installed" if is_installed(p) else "not installed"
106
+ meta = read_meta(skill_dir(p)) if is_installed(p) else {}
107
+ template_v = (meta.get("template") or {}).get("cli_version", "-")
108
+ upgrade_hint = ""
109
+ if template_v not in ("-", cli_version):
110
+ upgrade_hint = f" [upgrade: {template_v} -> {cli_version}]"
111
+ issues.append(
112
+ f"{p.id} skill template is {template_v}, latest is {cli_version} "
113
+ f"- run 'wbox skills upgrade'"
114
+ )
115
+ typer.echo(
116
+ f" {p.id:<22} {status:<16} template={template_v:<10} {skill_dir(p)}{upgrade_hint}"
117
+ )
118
+
119
+ # --- Plugin installs --------------------------------------------------
120
+ plugin_installs = detect_plugin_installs()
121
+ typer.echo("\n# Plugin installs (managed by host CLI: claude/codex plugin)")
122
+ if not plugin_installs:
123
+ typer.echo(" (none detected)")
124
+ else:
125
+ # Active first, then cached (descending version) so the live
126
+ # install is the first thing the eye lands on.
127
+ plugin_installs.sort(key=lambda p: (not p.active, p.version), reverse=False)
128
+ for pi in plugin_installs:
129
+ status_label = "active" if pi.active else "cached"
130
+ typer.echo(
131
+ f" {pi.host:<22} plugin@{pi.marketplace:<18} "
132
+ f"version={pi.version:<8} {status_label:<7} {pi.skill_dir}"
133
+ )
134
+
135
+ # --- Firm data --------------------------------------------------------
136
+ typer.echo("\n# Firm data (canonical)")
137
+ typer.echo(f" path: {firm_dir()}")
138
+ typer.echo(f" meta: {firm_meta_path()}")
139
+ firm_meta = read_firm_meta()
140
+ if not firm_meta:
141
+ typer.echo(" state: not bootstrapped")
142
+ issues.append("firm data not bootstrapped - run 'wbox skills bootstrap'")
143
+ else:
144
+ identity = firm_meta.get("identity") or {}
145
+ if firm_meta.get("onboarded_at"):
146
+ firm_state = "onboarded"
147
+ else:
148
+ firm_state = "bootstrapped (qualitative pending)"
149
+ issues.append(
150
+ "firm onboarding incomplete - open the skill in your agent to "
151
+ "walk through bootstrap.md (it'll call 'wbox skills mark-onboarded')"
152
+ )
153
+ typer.echo(f" state: {firm_state}")
154
+ typer.echo(f" firm: {identity.get('name', '-')}")
155
+ typer.echo(f" user: {identity.get('user_name', '-')}")
156
+ if firm_dir().exists():
157
+ md_files = sorted(p.name for p in firm_dir().glob("*.md"))
158
+ generated_set = set((firm_meta.get("files") or {}).keys())
159
+ handed = [f for f in md_files if f not in generated_set and not f.startswith("_")]
160
+ typer.echo(
161
+ f" files: {len(md_files)} markdown ({len(generated_set)} generated, "
162
+ f"{len(handed)} hand-edited)"
163
+ )
164
+ file_ts = list((firm_meta.get("files") or {}).values())
165
+ if file_ts:
166
+ try:
167
+ oldest = min(datetime.fromisoformat(t).date() for t in file_ts)
168
+ typer.echo(f" oldest: {oldest}")
169
+ except (ValueError, TypeError):
170
+ pass
171
+
172
+ # --- Summary ----------------------------------------------------------
173
+ typer.echo("\n# Summary")
174
+ if not issues:
175
+ typer.echo(" All checks passed.")
176
+ return 0
177
+ typer.echo(f" {len(issues)} issue(s) found:")
178
+ for i in issues:
179
+ typer.echo(f" ! {i}")
180
+ return 1
181
+
182
+
183
+ def _ensure_firm_migrated():
184
+ """Migrate legacy firm/ data on first read (idempotent)."""
185
+ installed = [skill_dir(p) for p in detect_platforms() if is_installed(p)]
186
+ return migrate_legacy_firm(installed)
187
+
188
+
189
+ def doctor_cmd(
190
+ token: str | None = typer.Option(
191
+ None, "--token",
192
+ help="Override the API token for the smoke test. Overrides env var, config, .env.",
193
+ ),
194
+ ) -> None:
195
+ """Run the comprehensive doctor.
196
+
197
+ Always exits 0 — issues are surfaced in the Summary section. The doctor
198
+ is informational, not a CI gate. (A future --strict flag could change
199
+ this for scripting.) Token is intentionally NOT bound to
200
+ `envvar=WEALTHBOX_TOKEN` so the doctor can report whether the token came
201
+ from the flag, env var, config file, or .env file.
202
+ """
203
+ run_doctor(token=token)
@@ -8,6 +8,7 @@ from .activity import app as activity_app
8
8
  from .categories import app as categories_app
9
9
  from .config import app as config_app
10
10
  from .contacts import app as contacts_app
11
+ from .doctor import doctor_cmd as _doctor_cmd
11
12
  from .events import app as events_app
12
13
  from .households import app as households_app
13
14
  from .me import app as me_app
@@ -35,6 +36,12 @@ def _main(
35
36
  raise typer.Exit()
36
37
 
37
38
 
39
+ app.command(
40
+ "doctor",
41
+ help="Comprehensive health check: CLI version, auth, agent CLIs, skills, plugins, firm.",
42
+ )(_doctor_cmd)
43
+
44
+
38
45
  app.add_typer(activity_app, name="activity")
39
46
  app.add_typer(categories_app, name="categories")
40
47
  app.add_typer(config_app, name="config")