wealthbox-cli 2.2.0__tar.gz → 2.3.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 (103) hide show
  1. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/CHANGELOG.md +16 -0
  2. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/PKG-INFO +22 -1
  3. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/README.md +21 -0
  4. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/getting-started.md +7 -1
  5. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/pyproject.toml +1 -1
  6. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/_util.py +74 -0
  7. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/contacts.py +48 -12
  8. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/self_cmd.py +21 -0
  9. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/common.py +10 -2
  10. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/self_upgrade.py +30 -0
  11. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +2 -0
  12. wealthbox_cli-2.3.0/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +34 -0
  13. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +59 -2
  14. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +5 -0
  15. wealthbox_cli-2.2.0/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -21
  16. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/.github/workflows/ci.yml +0 -0
  17. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/.github/workflows/docs.yml +0 -0
  18. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/.github/workflows/release-binaries.yml +0 -0
  19. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/.gitignore +0 -0
  20. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/CONTRIBUTING.md +0 -0
  21. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/LICENSE +0 -0
  22. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/SECURITY.md +0 -0
  23. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/agents/domain.md +0 -0
  24. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/agents/issue-tracker.md +0 -0
  25. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/agents/triage-labels.md +0 -0
  26. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/cli-reference.md +0 -0
  27. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/index.md +0 -0
  28. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/overrides/main.html +0 -0
  29. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/docs/promotion.md +0 -0
  30. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/mkdocs.yml +0 -0
  31. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/__init__.py +0 -0
  32. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/__init__.py +0 -0
  33. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/_config.py +0 -0
  34. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/_skill_bootstrap.py +0 -0
  35. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/_skill_paths.py +0 -0
  36. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/_skill_platforms.py +0 -0
  37. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/activity.py +0 -0
  38. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/categories.py +0 -0
  39. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/config.py +0 -0
  40. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/doctor.py +0 -0
  41. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/events.py +0 -0
  42. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/firm.py +0 -0
  43. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/households.py +0 -0
  44. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/internals.py +0 -0
  45. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/main.py +0 -0
  46. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/me.py +0 -0
  47. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/notes.py +0 -0
  48. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/opportunities.py +0 -0
  49. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/prefs.py +0 -0
  50. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/projects.py +0 -0
  51. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/skills.py +0 -0
  52. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/tasks.py +0 -0
  53. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/users.py +0 -0
  54. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/cli/workflows.py +0 -0
  55. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/__init__.py +0 -0
  56. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/activity.py +0 -0
  57. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/base.py +0 -0
  58. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/categories.py +0 -0
  59. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/comments.py +0 -0
  60. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/contacts.py +0 -0
  61. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/events.py +0 -0
  62. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/households.py +0 -0
  63. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/me.py +0 -0
  64. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/notes.py +0 -0
  65. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/opportunities.py +0 -0
  66. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/projects.py +0 -0
  67. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/tasks.py +0 -0
  68. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/users.py +0 -0
  69. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/client/workflows.py +0 -0
  70. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/firm/__init__.py +0 -0
  71. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/firm/archive.py +0 -0
  72. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/firm/diff.py +0 -0
  73. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/internals/__init__.py +0 -0
  74. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/internals/skill_ref_gen.py +0 -0
  75. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/__init__.py +0 -0
  76. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/activity.py +0 -0
  77. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/comments.py +0 -0
  78. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/contacts.py +0 -0
  79. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/custom_fields.py +0 -0
  80. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/enums.py +0 -0
  81. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/events.py +0 -0
  82. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/households.py +0 -0
  83. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/notes.py +0 -0
  84. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/opportunities.py +0 -0
  85. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/projects.py +0 -0
  86. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/tasks.py +0 -0
  87. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/models/workflows.py +0 -0
  88. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/py.typed +0 -0
  89. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +0 -0
  90. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
  91. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
  92. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
  93. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
  94. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
  95. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
  96. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
  97. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +0 -0
  98. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
  99. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
  100. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
  101. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +0 -0
  102. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/src/wealthbox_tools/skills/wealthbox-crm/references/workflows.md +0 -0
  103. {wealthbox_cli-2.2.0 → wealthbox_cli-2.3.0}/uv.lock +0 -0
@@ -4,6 +4,22 @@ All notable changes to `wealthbox-cli` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.2.1] - 2026-05-13
8
+
9
+ Bug-fix release. `wbox self upgrade` was silently no-op'ing for users who
10
+ installed `wbox` via `uv tool install` or `pipx`: it computed the install root
11
+ from `sys.executable` (the venv's `python.exe`) and tried to swap the
12
+ console-script shim there, but `~/.local/bin/wbox.exe` (or pipx's equivalent)
13
+ is a separate copy uv/pipx made — replacing the venv-side shim never changed
14
+ what `PATH` resolved.
15
+
16
+ ### Fixed
17
+ - `wbox self upgrade` now detects the install kind (`bundle` / `uv-tool` /
18
+ `pipx` / `pip`) up front. Non-bundle installs exit `1` with the correct
19
+ follow-up command (`uv tool upgrade wealthbox-cli`, `pipx upgrade
20
+ wealthbox-cli`, or `pip install --upgrade wealthbox-cli`) instead of
21
+ scheduling a swap that would never take effect on `PATH`.
22
+
7
23
  ## [2.2.0] - 2026-05-13
8
24
 
9
25
  Small feature + correctness release. Disambiguates the two IDs returned by
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wealthbox-cli
3
- Version: 2.2.0
3
+ Version: 2.3.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/
@@ -60,6 +60,13 @@ The installer:
60
60
  2. Installs the AI agent skill into [Claude Code](https://claude.ai/download) (and Codex, if you have it).
61
61
  3. Asks for your **Wealthbox API token**. *Where to find it:* in Wealthbox, click your initials in the top right → **Settings** → **API Access** → **Create Access Token**. Paste the token when prompted.
62
62
 
63
+ Re-running is idempotent: it keeps your stored token, refreshes the skill template, and leaves PATH alone if already correct. To install just the binary without the AI agent skill, on Windows download install.ps1 and run with `-SkipSkills`:
64
+
65
+ ```powershell
66
+ iwr https://raw.githubusercontent.com/massive-value/wealthbox-cli/main/scripts/install.ps1 -OutFile install.ps1
67
+ .\install.ps1 -SkipSkills
68
+ ```
69
+
63
70
  That's it. Open Claude Code and try one of the prompts below.
64
71
 
65
72
  ------------------------------------------------------------------------
@@ -107,6 +114,19 @@ You stay in control: the agent shows you what it's about to do before it does it
107
114
 
108
115
  The installer drops a single prebuilt `wbox` binary onto your PATH (`~/.local/bin/wbox` on Mac/Linux, `%LOCALAPPDATA%\Programs\wbox\wbox.exe` on Windows), then registers the AI agent skill with Claude Code (and Codex, if it's installed). Firm data lives at `~/.config/wbox/firm/` (Mac/Linux) or `%APPDATA%\wbox\firm\` (Windows) and survives reinstalls and upgrades. Run `wbox doctor` to see your install status anytime, and `wbox skills upgrade` to pull the latest skill template.
109
116
 
117
+ ### Upgrading
118
+
119
+ The upgrade command depends on how you installed:
120
+
121
+ | If you installed via... | Upgrade with |
122
+ |---|---|
123
+ | `install.sh` / `install.ps1` (the one-line installer above) | `wbox self upgrade` |
124
+ | `pip install wealthbox-cli` | `pip install --upgrade wealthbox-cli` |
125
+ | `pipx install wealthbox-cli` | `pipx upgrade wealthbox-cli` |
126
+ | `uv tool install wealthbox-cli` | `uv tool upgrade wealthbox-cli` |
127
+
128
+ `wbox self upgrade` only swaps the standalone bundle the one-line installer puts in place; on pip/pipx/uv installs it refuses with the correct follow-up command rather than half-replacing a venv-managed shim.
129
+
110
130
  ------------------------------------------------------------------------
111
131
 
112
132
  ## For developers and power users
@@ -178,6 +198,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [CLAUDE.md](CLAUDE.md) for the archit
178
198
  <summary><b>Troubleshooting</b></summary>
179
199
 
180
200
  - **401 Unauthorized** — token expired or wrong. Run `wbox config show` to see the masked token, `wbox config set-token` to update.
201
+ - **`wbox self upgrade` says "run `uv tool upgrade …`" (or pipx / pip)** — expected. `wbox self upgrade` only swaps the standalone bundle the one-line installer drops in place. If you installed via pip / pipx / uv, follow the command the gate prints. See [Upgrading](#upgrading).
181
202
  - **Windows: "execution policy" error during install** — the installer offers to fix this for you. If you skipped it, run `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser` and re-run the installer.
182
203
  - **Linux: command not found after install** — open a new terminal, or run `source ~/.local/bin/env` in your current shell.
183
204
  - **Date format errors** — Wealthbox needs ISO 8601: `"2026-05-01T10:00:00-07:00"` for datetimes, `"YYYY-MM-DD"` for date-only fields.
@@ -20,6 +20,13 @@ The installer:
20
20
  2. Installs the AI agent skill into [Claude Code](https://claude.ai/download) (and Codex, if you have it).
21
21
  3. Asks for your **Wealthbox API token**. *Where to find it:* in Wealthbox, click your initials in the top right → **Settings** → **API Access** → **Create Access Token**. Paste the token when prompted.
22
22
 
23
+ Re-running is idempotent: it keeps your stored token, refreshes the skill template, and leaves PATH alone if already correct. To install just the binary without the AI agent skill, on Windows download install.ps1 and run with `-SkipSkills`:
24
+
25
+ ```powershell
26
+ iwr https://raw.githubusercontent.com/massive-value/wealthbox-cli/main/scripts/install.ps1 -OutFile install.ps1
27
+ .\install.ps1 -SkipSkills
28
+ ```
29
+
23
30
  That's it. Open Claude Code and try one of the prompts below.
24
31
 
25
32
  ------------------------------------------------------------------------
@@ -67,6 +74,19 @@ You stay in control: the agent shows you what it's about to do before it does it
67
74
 
68
75
  The installer drops a single prebuilt `wbox` binary onto your PATH (`~/.local/bin/wbox` on Mac/Linux, `%LOCALAPPDATA%\Programs\wbox\wbox.exe` on Windows), then registers the AI agent skill with Claude Code (and Codex, if it's installed). Firm data lives at `~/.config/wbox/firm/` (Mac/Linux) or `%APPDATA%\wbox\firm\` (Windows) and survives reinstalls and upgrades. Run `wbox doctor` to see your install status anytime, and `wbox skills upgrade` to pull the latest skill template.
69
76
 
77
+ ### Upgrading
78
+
79
+ The upgrade command depends on how you installed:
80
+
81
+ | If you installed via... | Upgrade with |
82
+ |---|---|
83
+ | `install.sh` / `install.ps1` (the one-line installer above) | `wbox self upgrade` |
84
+ | `pip install wealthbox-cli` | `pip install --upgrade wealthbox-cli` |
85
+ | `pipx install wealthbox-cli` | `pipx upgrade wealthbox-cli` |
86
+ | `uv tool install wealthbox-cli` | `uv tool upgrade wealthbox-cli` |
87
+
88
+ `wbox self upgrade` only swaps the standalone bundle the one-line installer puts in place; on pip/pipx/uv installs it refuses with the correct follow-up command rather than half-replacing a venv-managed shim.
89
+
70
90
  ------------------------------------------------------------------------
71
91
 
72
92
  ## For developers and power users
@@ -138,6 +158,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [CLAUDE.md](CLAUDE.md) for the archit
138
158
  <summary><b>Troubleshooting</b></summary>
139
159
 
140
160
  - **401 Unauthorized** — token expired or wrong. Run `wbox config show` to see the masked token, `wbox config set-token` to update.
161
+ - **`wbox self upgrade` says "run `uv tool upgrade …`" (or pipx / pip)** — expected. `wbox self upgrade` only swaps the standalone bundle the one-line installer drops in place. If you installed via pip / pipx / uv, follow the command the gate prints. See [Upgrading](#upgrading).
141
162
  - **Windows: "execution policy" error during install** — the installer offers to fix this for you. If you skipped it, run `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser` and re-run the installer.
142
163
  - **Linux: command not found after install** — open a new terminal, or run `source ~/.local/bin/env` in your current shell.
143
164
  - **Date format errors** — Wealthbox needs ISO 8601: `"2026-05-01T10:00:00-07:00"` for datetimes, `"YYYY-MM-DD"` for date-only fields.
@@ -8,20 +8,26 @@ This guide walks you through installing **wealthbox-cli** and configuring it to
8
8
 
9
9
  ### One-line installer (no Python required)
10
10
 
11
- If you don't have Python installed, the bootstrap script handles everything: it installs [uv](https://github.com/astral-sh/uv) (which provisions Python automatically), installs `wbox`, prompts for your API token, and offers to install the AI agent skill.
11
+ If you don't have Python installed, the bootstrap script handles everything: it downloads a prebuilt `wbox`, places it on your PATH, prompts for your API token, and offers to install the AI agent skill.
12
12
 
13
13
  === "macOS / Linux"
14
14
 
15
+ Installs [uv](https://github.com/astral-sh/uv) (which provisions Python automatically) and uses it to put `wbox` on your PATH.
16
+
15
17
  ```bash
16
18
  curl -LsSf https://raw.githubusercontent.com/massive-value/wealthbox-cli/main/scripts/install.sh | bash
17
19
  ```
18
20
 
19
21
  === "Windows (PowerShell)"
20
22
 
23
+ Downloads a standalone PyInstaller-bundled `wbox.exe` (no Python, no uv) to `%LOCALAPPDATA%\Programs\wbox\`. Pass `-SkipSkills` (via `iwr -OutFile install.ps1; .\install.ps1 -SkipSkills`) to skip the AI agent skill.
24
+
21
25
  ```powershell
22
26
  irm https://raw.githubusercontent.com/massive-value/wealthbox-cli/main/scripts/install.ps1 | iex
23
27
  ```
24
28
 
29
+ Either install supports `wbox self upgrade` for future updates. If you instead install via pip / pipx / uv tool (below), use the corresponding package-manager upgrade command — `wbox self upgrade` will refuse with the right pointer.
30
+
25
31
  ### From PyPI (recommended for Python users)
26
32
 
27
33
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wealthbox-cli"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "CLI and client library for the Wealthbox CRM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -451,6 +451,80 @@ async def resolve_category_id(
451
451
  raise typer.BadParameter(f"No {label} configured in this workspace.")
452
452
 
453
453
 
454
+ def _match_contact_role(roles: list[dict[str, Any]], token: str) -> dict[str, Any]:
455
+ """Find a contact role by numeric id or case-insensitive name."""
456
+ if token.isdigit():
457
+ rid = int(token)
458
+ for r in roles:
459
+ if int(r.get("id", 0)) == rid:
460
+ return r
461
+ raise typer.BadParameter(f"No contact role with id {rid} in this workspace.")
462
+ target = token.casefold()
463
+ for r in roles:
464
+ if str(r.get("name", "")).casefold() == target:
465
+ return r
466
+ available = sorted(str(r["name"]) for r in roles if r.get("name"))
467
+ raise typer.BadParameter(
468
+ f"Unknown contact role '{token}'. Available: {', '.join(available) or '(none configured)'}"
469
+ )
470
+
471
+
472
+ def _role_option_user(option: dict[str, Any]) -> dict[str, Any]:
473
+ return option.get("assigned_to") or {}
474
+
475
+
476
+ def _match_role_option(role: dict[str, Any], token: str) -> int:
477
+ """Resolve a user (numeric id, exact name, or unique substring) to a role-option id."""
478
+ options = role.get("available_options", []) or []
479
+ role_name = role.get("name", "role")
480
+ if token.isdigit():
481
+ uid = int(token)
482
+ for opt in options:
483
+ if int(_role_option_user(opt).get("id", 0)) == uid:
484
+ return int(opt["id"])
485
+ raise typer.BadParameter(f"No user with id {uid} is available for role '{role_name}'.")
486
+ target = token.casefold()
487
+ exact = [o for o in options if str(_role_option_user(o).get("name", "")).casefold() == target]
488
+ matches = exact or [
489
+ o for o in options if target in str(_role_option_user(o).get("name", "")).casefold()
490
+ ]
491
+ if len(matches) == 1:
492
+ return int(matches[0]["id"])
493
+ if not matches:
494
+ names = sorted(str(_role_option_user(o).get("name", "")) for o in options)
495
+ raise typer.BadParameter(
496
+ f"No user matching '{token}' for role '{role_name}'. Available: {', '.join(names)}"
497
+ )
498
+ cand = sorted(str(_role_option_user(o).get("name", "")) for o in matches)
499
+ raise typer.BadParameter(
500
+ f"'{token}' is ambiguous for role '{role_name}': matches {', '.join(cand)}. Be more specific."
501
+ )
502
+
503
+
504
+ async def resolve_contact_roles(client: WealthboxClient, specs: list[str]) -> list[dict[str, int]]:
505
+ """Resolve ``Role:User`` specs to ``[{"id": role_id, "value": option_id}]`` entries.
506
+
507
+ Role may be a contact-role name (case-insensitive) or numeric id. User may be a
508
+ user name (case-insensitive; exact preferred, else unique substring) or numeric
509
+ user id. Both are resolved against ``wbox contacts categories contact-roles`` —
510
+ the option id (``available_options[].id``), not the user id, is the value the
511
+ Wealthbox API stores.
512
+ """
513
+ data = await client.list_all_categories(CategoryType.CONTACT_ROLES)
514
+ roles = data.get("contact_roles", []) or []
515
+ resolved: list[dict[str, int]] = []
516
+ for spec in specs:
517
+ role_token, sep, user_token = spec.partition(":")
518
+ role_token, user_token = role_token.strip(), user_token.strip()
519
+ if not sep or not role_token or not user_token:
520
+ raise typer.BadParameter(
521
+ f"--advisor-role expects 'Role:User' (e.g. 'Associate Advisor:Greg Hyde'); got '{spec}'."
522
+ )
523
+ role = _match_contact_role(roles, role_token)
524
+ resolved.append({"id": int(role["id"]), "value": _match_role_option(role, user_token)})
525
+ return resolved
526
+
527
+
454
528
  def parse_more_fields(more_fields: str, reserved: set[str]) -> dict[str, Any]:
455
529
  """Parse --more-fields JSON and validate against reserved keys.
456
530
 
@@ -25,9 +25,16 @@ from ._util import (
25
25
  make_resource_app,
26
26
  output_result,
27
27
  parse_more_fields,
28
+ resolve_contact_roles,
28
29
  run_client,
29
30
  )
30
31
 
32
+ _ADVISOR_ROLE_HELP = (
33
+ "Assign a contact role as 'Role:User' (repeatable), e.g. "
34
+ "'Associate Advisor:Greg Hyde'. User may be a name or numeric user id. "
35
+ "Resolved via: wbox contacts categories contact-roles"
36
+ )
37
+
31
38
  app = make_resource_app(help="Manage Wealthbox contacts.")
32
39
 
33
40
  _DEFAULT_FIELDS = ["id", "name", "type", "contact_type", "assigned_to", "status"]
@@ -180,6 +187,23 @@ def _build_contact_entry(value: str | None, kind: str | None) -> list[dict[str,
180
187
  return [entry]
181
188
 
182
189
 
190
+ def _finish_create(
191
+ payload: dict[str, Any], advisor_role: list[str] | None, token: str | None, fmt: OutputFormat
192
+ ) -> None:
193
+ """Resolve any --advisor-role specs in-session, build the model, create, and print."""
194
+ if advisor_role and "contact_roles" in payload:
195
+ raise typer.BadParameter(
196
+ "Set contact roles via --advisor-role OR contact_roles in --more-fields, not both."
197
+ )
198
+
199
+ async def _do(client: Any) -> Any:
200
+ if advisor_role:
201
+ payload["contact_roles"] = await resolve_contact_roles(client, advisor_role)
202
+ return await client.create_contact(ContactCreateInput(**payload))
203
+
204
+ output_result(run_client(token, _do), fmt)
205
+
206
+
183
207
  def _create_named_contact(
184
208
  record_type: RecordType,
185
209
  reserved: set[str],
@@ -194,6 +218,7 @@ def _create_named_contact(
194
218
  phone_type: str | None,
195
219
  tags: str | None,
196
220
  more_fields: str | None,
221
+ advisor_role: list[str] | None,
197
222
  token: str | None,
198
223
  fmt: OutputFormat,
199
224
  ) -> None:
@@ -216,8 +241,7 @@ def _create_named_contact(
216
241
  payload["tags"] = tag_list
217
242
  if more_fields:
218
243
  payload.update(parse_more_fields(more_fields, reserved))
219
- input_model = ContactCreateInput(**payload)
220
- output_result(run_client(token, lambda c: c.create_contact(input_model)), fmt)
244
+ _finish_create(payload, advisor_role, token, fmt)
221
245
 
222
246
 
223
247
  @add_app.command("person", help="Create a Person contact.")
@@ -253,6 +277,7 @@ def add_person(
253
277
  more_fields: str | None = typer.Option(
254
278
  None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
255
279
  ),
280
+ advisor_role: list[str] | None = typer.Option(None, "--advisor-role", help=_ADVISOR_ROLE_HELP),
256
281
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
257
282
  fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
258
283
  ) -> None:
@@ -286,8 +311,7 @@ def add_person(
286
311
  payload["tags"] = tag_list
287
312
  if more_fields:
288
313
  payload.update(parse_more_fields(more_fields, _PERSON_RESERVED))
289
- input_model = ContactCreateInput(**payload)
290
- output_result(run_client(token, lambda c: c.create_contact(input_model)), fmt)
314
+ _finish_create(payload, advisor_role, token, fmt)
291
315
 
292
316
 
293
317
  @add_app.command("household", help="Create a Household contact.")
@@ -308,6 +332,7 @@ def add_household(
308
332
  more_fields: str | None = typer.Option(
309
333
  None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
310
334
  ),
335
+ advisor_role: list[str] | None = typer.Option(None, "--advisor-role", help=_ADVISOR_ROLE_HELP),
311
336
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
312
337
  fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
313
338
  ) -> None:
@@ -327,8 +352,7 @@ def add_household(
327
352
  payload["tags"] = tag_list
328
353
  if more_fields:
329
354
  payload.update(parse_more_fields(more_fields, _HOUSEHOLD_RESERVED))
330
- input_model = ContactCreateInput(**payload)
331
- output_result(run_client(token, lambda c: c.create_contact(input_model)), fmt)
355
+ _finish_create(payload, advisor_role, token, fmt)
332
356
 
333
357
 
334
358
  @add_app.command("org", help="Create an Organization contact.")
@@ -353,12 +377,13 @@ def add_org(
353
377
  more_fields: str | None = typer.Option(
354
378
  None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
355
379
  ),
380
+ advisor_role: list[str] | None = typer.Option(None, "--advisor-role", help=_ADVISOR_ROLE_HELP),
356
381
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
357
382
  fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
358
383
  ) -> None:
359
384
  _create_named_contact(
360
385
  RecordType.ORGANIZATION, _ORG_TRUST_RESERVED, name, contact_type, contact_source,
361
- active, assigned_to, email, email_type, phone, phone_type, tags, more_fields, token, fmt,
386
+ active, assigned_to, email, email_type, phone, phone_type, tags, more_fields, advisor_role, token, fmt,
362
387
  )
363
388
 
364
389
 
@@ -384,12 +409,13 @@ def add_trust(
384
409
  more_fields: str | None = typer.Option(
385
410
  None, "--more-fields", help="Extra fields as JSON object (merged with flags; cannot override explicit flags)"
386
411
  ),
412
+ advisor_role: list[str] | None = typer.Option(None, "--advisor-role", help=_ADVISOR_ROLE_HELP),
387
413
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
388
414
  fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
389
415
  ) -> None:
390
416
  _create_named_contact(
391
417
  RecordType.TRUST, _ORG_TRUST_RESERVED, name, contact_type, contact_source,
392
- active, assigned_to, email, email_type, phone, phone_type, tags, more_fields, token, fmt,
418
+ active, assigned_to, email, email_type, phone, phone_type, tags, more_fields, advisor_role, token, fmt,
393
419
  )
394
420
 
395
421
 
@@ -420,13 +446,19 @@ def update_contact(
420
446
  "include existing tags you wish to keep. New tags are auto-created."
421
447
  ),
422
448
  ),
449
+ advisor_role: list[str] | None = typer.Option(None, "--advisor-role", help=_ADVISOR_ROLE_HELP),
423
450
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
424
451
  fmt: OutputFormat = typer.Option(OutputFormat.JSON, "--format"),
425
452
  ) -> None:
453
+ if advisor_role and json_data is not None:
454
+ raise typer.BadParameter(
455
+ "Set contact roles via --advisor-role OR contact_roles in --json, not both."
456
+ )
457
+
426
458
  if json_data is not None:
427
- input_model = ContactUpdateInput(**json.loads(json_data))
459
+ payload: dict[str, Any] = json.loads(json_data)
428
460
  else:
429
- payload: dict[str, Any] = {k: v for k, v in {
461
+ payload = {k: v for k, v in {
430
462
  "first_name": first_name,
431
463
  "middle_name": middle_name,
432
464
  "last_name": last_name,
@@ -441,9 +473,13 @@ def update_contact(
441
473
  tag_list = _parse_tags(tags)
442
474
  if tag_list is not None:
443
475
  payload["tags"] = tag_list
444
- input_model = ContactUpdateInput(**payload)
445
476
 
446
- output_result(run_client(token, lambda c: c.update_contact(contact_id, input_model)), fmt)
477
+ async def _do(client: Any) -> Any:
478
+ if advisor_role:
479
+ payload["contact_roles"] = await resolve_contact_roles(client, advisor_role)
480
+ return await client.update_contact(contact_id, ContactUpdateInput(**payload))
481
+
482
+ output_result(run_client(token, _do), fmt)
447
483
 
448
484
 
449
485
  @app.command("delete", help="Delete an existing contact.")
@@ -37,8 +37,29 @@ def _default_install_root() -> Path:
37
37
  return Path(sys.executable).resolve().parent
38
38
 
39
39
 
40
+ _NON_BUNDLE_UPGRADE_HINTS = {
41
+ "uv-tool": "uv tool upgrade wealthbox-cli",
42
+ "pipx": "pipx upgrade wealthbox-cli",
43
+ "pip": "pip install --upgrade wealthbox-cli",
44
+ }
45
+
46
+
40
47
  @app.command("upgrade", help="Upgrade the `wbox` CLI to the latest GitHub release.")
41
48
  def upgrade_cmd() -> None:
49
+ kind = self_upgrade._install_kind()
50
+ if kind != "bundle":
51
+ # The frozen-bundle swap would either silently no-op (uv copies the
52
+ # shim to ~/.local/bin so replacing the venv-side shim doesn't change
53
+ # what PATH resolves) or wedge the venv. Refuse before downloading.
54
+ hint = _NON_BUNDLE_UPGRADE_HINTS.get(kind, _NON_BUNDLE_UPGRADE_HINTS["pip"])
55
+ typer.echo(
56
+ "wbox self upgrade only swaps the standalone binary from "
57
+ "install.ps1.",
58
+ err=True,
59
+ )
60
+ typer.echo(f"This wbox was installed via {kind} — run: {hint}", err=True)
61
+ raise typer.Exit(code=1)
62
+
42
63
  candidate = self_upgrade.check()
43
64
  if candidate is None:
44
65
  typer.echo("Already on the latest version.")
@@ -105,8 +105,16 @@ class StreetAddress(WealthboxModel):
105
105
 
106
106
 
107
107
  class ContactRoleAssignment(WealthboxModel):
108
- id: int | None = Field(default=None, ge=1)
109
- type: str | None = None
108
+ """A contact-role assignment on a contact's write payload.
109
+
110
+ ``id`` is the contact-role id (e.g. "Associate Advisor") and ``value`` is
111
+ the *role-option* id that maps to a specific user — NOT the user's id.
112
+ Resolve both from ``wbox contacts categories contact-roles``, whose
113
+ ``available_options[].id`` is the value to use here.
114
+ """
115
+
116
+ id: int = Field(ge=1, description="Contact-role id (from contacts categories contact-roles)")
117
+ value: int = Field(ge=1, description="Role-option id mapping to a user (available_options[].id)")
110
118
 
111
119
 
112
120
  class LinkedToRef(WealthboxModel):
@@ -51,6 +51,7 @@ __all__ = [
51
51
  "check_release_staleness",
52
52
  "apply",
53
53
  "_cleanup_stale_backups",
54
+ "_install_kind",
54
55
  ]
55
56
 
56
57
  _RELEASES_LATEST_URL = (
@@ -139,6 +140,35 @@ def _running_version() -> str:
139
140
  return __version__
140
141
 
141
142
 
143
+ def _install_kind() -> str:
144
+ """Classify how the running ``wbox`` was installed.
145
+
146
+ The :func:`apply` swap is only correct for the standalone PyInstaller
147
+ bundle that ``install.ps1`` ships. Under uv / pipx / pip the install is
148
+ a tiny console-script shim around a venv; replacing it with a frozen
149
+ binary would either silently no-op (PATH points at a copy uv made
150
+ elsewhere, e.g. ``~/.local/bin/wbox.exe``) or wedge the venv on the
151
+ next ``uv tool sync``. The gate in :mod:`cli.self_cmd` consults this
152
+ classifier to refuse with a clear "use <installer> instead" message.
153
+
154
+ Returns one of:
155
+
156
+ - ``"bundle"`` — frozen PyInstaller EXE (``sys.frozen`` is set).
157
+ - ``"uv-tool"`` — ``sys.executable`` lives under a ``uv/tools/`` venv.
158
+ - ``"pipx"`` — ``sys.executable`` lives under a ``pipx/`` venv.
159
+ - ``"pip"`` — anything else (ordinary pip/venv install).
160
+ """
161
+ if getattr(sys, "frozen", False):
162
+ return "bundle"
163
+ parts = [p.lower() for p in Path(sys.executable).resolve().parts]
164
+ for i in range(len(parts) - 1):
165
+ if parts[i] == "uv" and parts[i + 1] == "tools":
166
+ return "uv-tool"
167
+ if "pipx" in parts:
168
+ return "pipx"
169
+ return "pip"
170
+
171
+
142
172
  # ---------------------------------------------------------------------------
143
173
  # Internal helpers
144
174
  # ---------------------------------------------------------------------------
@@ -195,3 +195,5 @@ The single-quoted heredoc (`<<'EOF'`) is the recommended default — it disables
195
195
  - **Relative due dates:** tasks accept `--frame today|tomorrow|this-week|next-week|this-month|next-month` instead of `--due-date` (mutually exclusive)
196
196
  - **Activity pagination:** uses `--cursor`, not `--page`
197
197
  - **Don't infer flag names:** read `references/<resource>.md` before invoking — flags like `--name` (not `--search`), `--frame` (not `--due-in`) are easy to guess wrong
198
+ - **Advisor / contact roles:** assign them with `wbox contacts add|update ... --advisor-role "Role:User"` (e.g. `"Associate Advisor:Greg Hyde"`). This covers a firm's "Second Advisor" / Partner assignments. See `references/contacts.md` → "Contact Roles".
199
+ - **Before falling back to raw API, exhaust the CLI:** if a firm-required field has no obvious flag, check `references/<resource>.md`, run `wbox <resource> --help` / `wbox <resource> <sub> --help`, and look for a `wbox <resource> categories <type>` lookup that supplies the needed ids. Reach for raw `curl`/API only after these come up empty — and if they do, that's a CLI gap worth filing as an issue.
@@ -0,0 +1,34 @@
1
+ # firm/contacts.md — Example
2
+
3
+ This is an illustrative template. Replace every value below with your firm's
4
+ real conventions. Valid category values (contact types, sources, roles, etc.)
5
+ come from your workspace — list them with `wbox contacts categories <type>`.
6
+
7
+ ## Required Fields
8
+ - contact-type: one of your workspace's contact types (`wbox contacts categories contact-types`)
9
+ - contact-source: one of your workspace's sources (`wbox contacts categories contact-sources`)
10
+
11
+ ## Defaults
12
+ - assigned-to: <your default user id> (`wbox me user-id`)
13
+ - active: true
14
+
15
+ ## Advisor / Contact Roles
16
+ - If your firm assigns advisors via contact roles (e.g. "Associate Advisor",
17
+ "Partner"), set them with `--advisor-role "Role:User"` on add/update.
18
+ - List the configured roles and their assignable users with
19
+ `wbox contacts categories contact-roles`.
20
+ - Example: a prospect's primary advisor is `--assigned-to`, and the second
21
+ advisor is `--advisor-role "Associate Advisor:<user>"`.
22
+
23
+ ## Person Contacts
24
+ - Always collect: email, phone, birth-date
25
+ - Preferred email-type: Personal
26
+ - Preferred phone-type: Mobile
27
+
28
+ ## Household Contacts
29
+ - Always create a household when onboarding a married couple
30
+ - Naming convention: e.g. "{Last}, {Head First} & {Spouse First}"
31
+ - Add members one at a time (sequential) — see references/households.md
32
+
33
+ ## Organization / Trust Contacts
34
+ - Require EIN in --more-fields for Trusts
@@ -70,6 +70,7 @@ wbox contacts add person [OPTIONS]
70
70
  | `--phone` | STR | Phone number |
71
71
  | `--phone-type` | STR | Work, Mobile, etc. |
72
72
  | `--tags` | STR | Comma-separated tag names (e.g. "VIP,Q1-Outreach"). New tags are auto-created. |
73
+ | `--advisor-role` | STR | Assign a contact role (repeatable): `"Role:User"`, e.g. `"Associate Advisor:Greg Hyde"`. See "Contact Roles" below. |
73
74
  | `--more-fields` | JSON | Additional fields as JSON object |
74
75
  | `--format` | json\|table\|csv\|tsv | Output format |
75
76
 
@@ -77,7 +78,7 @@ wbox contacts add person [OPTIONS]
77
78
  ```bash
78
79
  wbox contacts add household --name <NAME> [OPTIONS]
79
80
  ```
80
- `--name` is required. Supports: `--contact-type`, `--contact-source`, `--active/--inactive`, `--assigned-to`, `--email`, `--email-type`, `--tags`, `--more-fields`, `--format`.
81
+ `--name` is required. Supports: `--contact-type`, `--contact-source`, `--active/--inactive`, `--assigned-to`, `--email`, `--email-type`, `--tags`, `--advisor-role`, `--more-fields`, `--format`.
81
82
 
82
83
  ### Organization
83
84
  ```bash
@@ -112,6 +113,7 @@ Only pass the fields you want to change:
112
113
  | `--active` / `--inactive` | flag | Active status |
113
114
  | `--assigned-to` | INT | Reassign to user ID |
114
115
  | `--tags` | STR | Comma-separated tag names. Replaces all tags — include existing ones to keep. |
116
+ | `--advisor-role` | STR | Assign a contact role (repeatable): `"Role:User"`. See "Contact Roles" below. Mutually exclusive with a `contact_roles` block in `--json`. |
115
117
  | `--json` | STR | Full JSON for nested/complex fields |
116
118
  | `--format` | json\|table\|csv\|tsv | Output format |
117
119
 
@@ -136,6 +138,51 @@ wbox contacts categories website-types
136
138
  wbox contacts categories contact-roles
137
139
  ```
138
140
 
141
+ ## Contact Roles (advisor assignments)
142
+
143
+ Contact roles attach a user to a contact in a named role — e.g. **Associate
144
+ Advisor** ("Second Advisor") or **Partner**. This is how firm advisor
145
+ assignments are stored; they are *separate* from `--assigned-to` (the primary
146
+ owner).
147
+
148
+ **Use the `--advisor-role` flag** (repeatable) on `add person|household|org|trust`
149
+ and on `update`:
150
+
151
+ ```bash
152
+ wbox contacts add person --first-name Mark --last-name Leone \
153
+ --advisor-role "Associate Advisor:Greg Hyde" \
154
+ --advisor-role "Partner:Jane Smith"
155
+
156
+ wbox contacts update 12345 --advisor-role "Associate Advisor:Greg Hyde"
157
+ ```
158
+
159
+ The spec is `Role:User`. The role is matched by name (case-insensitive) or
160
+ numeric id; the user by name (exact, else unique substring) or numeric user id.
161
+ The flag resolves both against `wbox contacts categories contact-roles` for you.
162
+
163
+ **The underlying write shape** (if you ever build it by hand via `--more-fields`
164
+ or `--json`) is `{"id": <role_id>, "value": <option_id>}`:
165
+
166
+ ```bash
167
+ wbox contacts update 12345 --json '{"contact_roles": [{"id": 1338, "value": 4226}]}'
168
+ ```
169
+
170
+ ⚠️ `value` is the **role-option id**, NOT the user id. Each role exposes
171
+ `available_options[].id`, one per assignable user — that option id is the value.
172
+ Look it up with `wbox contacts categories contact-roles`:
173
+
174
+ ```jsonc
175
+ { "id": 1338, "name": "Associate Advisor",
176
+ "available_options": [
177
+ { "id": 4226, "assigned_to": { "id": 154372, "name": "Greg Hyde" } }
178
+ ] }
179
+ // → to set Greg as Associate Advisor: {"id": 1338, "value": 4226}
180
+ ```
181
+
182
+ Notes: roles can only be added/updated, not removed, via the API. You only need
183
+ to include the roles you're changing. `--advisor-role` and a `contact_roles`
184
+ block in `--more-fields`/`--json` are mutually exclusive.
185
+
139
186
  ## Generated Flag Reference
140
187
 
141
188
  The following section is auto-generated from the Typer command tree by
@@ -150,6 +197,7 @@ Create a Household contact.
150
197
  | Flag | Type | Default | Description |
151
198
  |------|------|---------|-------------|
152
199
  | `--active` / `--inactive` | `BOOLEAN` | `-` | Set contact status to Active or Inactive |
200
+ | `--advisor-role` | `TEXT` | `-` | Assign a contact role as 'Role:User' (repeatable), e.g. 'Associate Advisor:Greg Hyde'. User may be a name or numeric user id. Resolved via: wbox contacts categories contact-roles |
153
201
  | `--assigned-to` | `INTEGER` | `-` | Assign to a user by ID |
154
202
  | `--contact-source` | `TEXT` | `-` | |
155
203
  | `--contact-type` | `TEXT` | `-` | e.g. Client, Prospect |
@@ -174,6 +222,7 @@ Create an Organization contact.
174
222
  | Flag | Type | Default | Description |
175
223
  |------|------|---------|-------------|
176
224
  | `--active` / `--inactive` | `BOOLEAN` | `-` | Set contact status to Active or Inactive |
225
+ | `--advisor-role` | `TEXT` | `-` | Assign a contact role as 'Role:User' (repeatable), e.g. 'Associate Advisor:Greg Hyde'. User may be a name or numeric user id. Resolved via: wbox contacts categories contact-roles |
177
226
  | `--assigned-to` | `INTEGER` | `-` | Assign to a user by ID |
178
227
  | `--contact-source` | `TEXT` | `-` | |
179
228
  | `--contact-type` | `TEXT` | `-` | e.g. Client, Prospect |
@@ -200,6 +249,7 @@ Create a Person contact.
200
249
  | Flag | Type | Default | Description |
201
250
  |------|------|---------|-------------|
202
251
  | `--active` / `--inactive` | `BOOLEAN` | `-` | Set contact status to Active or Inactive |
252
+ | `--advisor-role` | `TEXT` | `-` | Assign a contact role as 'Role:User' (repeatable), e.g. 'Associate Advisor:Greg Hyde'. User may be a name or numeric user id. Resolved via: wbox contacts categories contact-roles |
203
253
  | `--anniversary` | `TEXT` | `-` | Format: YYYY-MM-DD |
204
254
  | `--assigned-to` | `INTEGER` | `-` | Assign to a user by ID |
205
255
  | `--birth-date` | `TEXT` | `-` | Format: YYYY-MM-DD |
@@ -254,6 +304,7 @@ Create a Trust contact.
254
304
  | Flag | Type | Default | Description |
255
305
  |------|------|---------|-------------|
256
306
  | `--active` / `--inactive` | `BOOLEAN` | `-` | Set contact status to Active or Inactive |
307
+ | `--advisor-role` | `TEXT` | `-` | Assign a contact role as 'Role:User' (repeatable), e.g. 'Associate Advisor:Greg Hyde'. User may be a name or numeric user id. Resolved via: wbox contacts categories contact-roles |
257
308
  | `--assigned-to` | `INTEGER` | `-` | Assign to a user by ID |
258
309
  | `--contact-source` | `TEXT` | `-` | |
259
310
  | `--contact-type` | `TEXT` | `-` | e.g. Client, Prospect |
@@ -479,6 +530,7 @@ Update an existing contact. Pass only the fields you want to change.
479
530
  | Flag | Type | Default | Description |
480
531
  |------|------|---------|-------------|
481
532
  | `--active` / `--inactive` | `BOOLEAN` | `-` | Set contact status to Active or Inactive |
533
+ | `--advisor-role` | `TEXT` | `-` | Assign a contact role as 'Role:User' (repeatable), e.g. 'Associate Advisor:Greg Hyde'. User may be a name or numeric user id. Resolved via: wbox contacts categories contact-roles |
482
534
  | `--assigned-to` | `INTEGER` | `-` | Reassign to a user by ID |
483
535
  | `--company-name` | `TEXT` | `-` | |
484
536
  | `--contact-source` | `TEXT` | `-` | |
@@ -512,7 +564,12 @@ wbox contacts add person --first-name Jane --last-name Doe --contact-type Client
512
564
  # Update job title
513
565
  wbox contacts update 12345 --job-title "CFO"
514
566
 
515
- # Create a household and add members (two-step)
567
+ # Create a prospect with primary owner + advisor role in one call
568
+ wbox contacts add person --first-name Jane --last-name Doe \
569
+ --contact-type Prospect --assigned-to 154372 \
570
+ --advisor-role "Associate Advisor:Greg Hyde"
571
+
572
+ # Create a household and add members (two-step — add members SEQUENTIALLY)
516
573
  wbox contacts add household --name "The Smith Family"
517
574
  # note the returned ID, then:
518
575
  wbox households add-member <HOUSEHOLD_ID> <MEMBER_ID> --title Head
@@ -2,6 +2,11 @@
2
2
 
3
3
  Manage membership of household contacts. Household contacts are created via `wbox contacts add household`.
4
4
 
5
+ > ⚠️ **Add members one at a time, sequentially.** Each `add-member` call
6
+ > rewrites the household's member list, so concurrent/parallel calls race and
7
+ > the last write clobbers the earlier ones. Run them in series and wait for
8
+ > each to return before issuing the next.
9
+
5
10
  ## Add Member
6
11
 
7
12
  ```bash
@@ -1,21 +0,0 @@
1
- # firm/contacts.md — Example
2
-
3
- ## Required Fields
4
- - contact-type: always one of: Client, Prospect, Center of Influence, Vendor
5
- - contact-source: always one of: Referral, Website, Event, Cold Call
6
-
7
- ## Defaults
8
- - assigned-to: 12345
9
- - active: true
10
-
11
- ## Person Contacts
12
- - Always collect: email, phone, birth-date
13
- - Preferred email-type: Personal
14
- - Preferred phone-type: Mobile
15
-
16
- ## Household Contacts
17
- - Always create a household when onboarding a married couple
18
- - Naming convention: "The {LastName} Family"
19
-
20
- ## Organization / Trust Contacts
21
- - Require EIN in more-fields for Trusts
File without changes
File without changes
File without changes
File without changes
File without changes