wealthbox-cli 1.1.4__tar.gz → 1.1.6__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 (88) hide show
  1. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/PKG-INFO +7 -4
  2. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/README.md +6 -3
  3. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/changelog.md +24 -0
  4. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/pyproject.toml +1 -1
  5. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_skill_bootstrap.py +32 -6
  6. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_skill_platforms.py +1 -16
  7. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_util.py +6 -0
  8. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/skills.py +51 -6
  9. wealthbox_cli-1.1.6/src/wealthbox_tools/client/categories.py +30 -0
  10. wealthbox_cli-1.1.6/src/wealthbox_tools/client/users.py +28 -0
  11. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +11 -5
  12. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +29 -10
  13. wealthbox_cli-1.1.4/src/wealthbox_tools/client/categories.py +0 -14
  14. wealthbox_cli-1.1.4/src/wealthbox_tools/client/users.py +0 -14
  15. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.env.example +0 -0
  16. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.github/workflows/ci.yml +0 -0
  17. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.github/workflows/docs.yml +0 -0
  18. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.gitignore +0 -0
  19. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/CONTRIBUTING.md +0 -0
  20. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/LICENSE +0 -0
  21. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/SECURITY.md +0 -0
  22. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/cli-reference.md +0 -0
  23. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/getting-started.md +0 -0
  24. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/index.md +0 -0
  25. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/overrides/main.html +0 -0
  26. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/promotion.md +0 -0
  27. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/mkdocs.yml +0 -0
  28. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/__init__.py +0 -0
  29. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/__init__.py +0 -0
  30. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_config.py +0 -0
  31. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/activity.py +0 -0
  32. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/categories.py +0 -0
  33. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/config.py +0 -0
  34. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/contacts.py +0 -0
  35. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/events.py +0 -0
  36. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/households.py +0 -0
  37. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/main.py +0 -0
  38. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/me.py +0 -0
  39. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/notes.py +0 -0
  40. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/opportunities.py +0 -0
  41. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/projects.py +0 -0
  42. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/tasks.py +0 -0
  43. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/users.py +0 -0
  44. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/workflows.py +0 -0
  45. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/__init__.py +0 -0
  46. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/activity.py +0 -0
  47. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/base.py +0 -0
  48. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/comments.py +0 -0
  49. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/contacts.py +0 -0
  50. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/events.py +0 -0
  51. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/households.py +0 -0
  52. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/me.py +0 -0
  53. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/notes.py +0 -0
  54. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/opportunities.py +0 -0
  55. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/projects.py +0 -0
  56. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/tasks.py +0 -0
  57. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/workflows.py +0 -0
  58. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/__init__.py +0 -0
  59. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/activity.py +0 -0
  60. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/comments.py +0 -0
  61. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/common.py +0 -0
  62. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/contacts.py +0 -0
  63. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/custom_fields.py +0 -0
  64. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/enums.py +0 -0
  65. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/events.py +0 -0
  66. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/households.py +0 -0
  67. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/notes.py +0 -0
  68. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/opportunities.py +0 -0
  69. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/projects.py +0 -0
  70. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/tasks.py +0 -0
  71. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/workflows.py +0 -0
  72. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/py.typed +0 -0
  73. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
  74. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
  75. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
  76. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
  77. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
  78. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
  79. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
  80. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +0 -0
  81. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
  82. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
  83. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +0 -0
  84. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
  85. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
  86. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
  87. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +0 -0
  88. {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/workflows.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wealthbox-cli
3
- Version: 1.1.4
3
+ Version: 1.1.6
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/
@@ -144,11 +144,13 @@ wbox config set-token # paste your Wealthbox API token
144
144
  wbox skills install # interactive: pick Claude Code, Codex, or both
145
145
  ```
146
146
 
147
- The installer asks which platforms to target, copies the skill into the right directory (`~/.claude/skills/` for Claude Code, `~/.codex/skills/` for Codex — with `SKILL.md` renamed to `AGENTS.md` automatically), and offers to bootstrap your firm's customizations from the Wealthbox API.
147
+ The installer asks which platforms to target, copies the skill into the right directory (`~/.claude/skills/` for Claude Code, `~/.codex/skills/` for Codex — both use `SKILL.md`), and offers to bootstrap your firm's customizations from the Wealthbox API.
148
148
 
149
- ### First agent run bootstraps itself
149
+ > **Upgrading from < 1.1.6?** Earlier versions installed the codex skill as `AGENTS.md`. That was wrong — codex uses `SKILL.md` for skills (and `AGENTS.md` only as the project-level instructions file, in place of `CLAUDE.md`). If your codex install still has `AGENTS.md`, reinstall with `wbox skills install --platform codex --force`.
150
150
 
151
- The first time an agent invokes the skill, it walks you through populating firm-specific conventions — category values, custom fields, users, defaults, required fields, household naming, named workflows. No further CLI commands needed. After that first run, the bootstrap artifacts self-delete from the skill.
151
+ ### First agent run captures firm conventions
152
+
153
+ `wbox skills bootstrap` populates the API-derived files (categories, custom fields, users) — that's the *quantitative* half. The *qualitative* half (firm defaults, naming conventions, named workflows, required fields) happens the first time an agent invokes the skill: it walks you through the questions in `bootstrap.md` and stamps `firm.onboarded_at` in `_meta.json` when done. On subsequent runs the bootstrap path is skipped automatically.
152
154
 
153
155
  ### Refresh after firm changes
154
156
 
@@ -167,6 +169,7 @@ wbox skills list # show where the skill is installed + last bootstra
167
169
  wbox skills doctor # diagnose install state + token
168
170
  wbox skills sync # copy firm/ from one install to others (Claude Code <-> Codex)
169
171
  wbox skills upgrade # refresh template files (SKILL.md, references/, ...) preserving firm/
172
+ wbox skills mark-onboarded # stamp firm.onboarded_at after qualitative Q&A (the agent calls this for you)
170
173
  wbox skills uninstall # remove the skill
171
174
  ```
172
175
 
@@ -103,11 +103,13 @@ wbox config set-token # paste your Wealthbox API token
103
103
  wbox skills install # interactive: pick Claude Code, Codex, or both
104
104
  ```
105
105
 
106
- The installer asks which platforms to target, copies the skill into the right directory (`~/.claude/skills/` for Claude Code, `~/.codex/skills/` for Codex — with `SKILL.md` renamed to `AGENTS.md` automatically), and offers to bootstrap your firm's customizations from the Wealthbox API.
106
+ The installer asks which platforms to target, copies the skill into the right directory (`~/.claude/skills/` for Claude Code, `~/.codex/skills/` for Codex — both use `SKILL.md`), and offers to bootstrap your firm's customizations from the Wealthbox API.
107
107
 
108
- ### First agent run bootstraps itself
108
+ > **Upgrading from < 1.1.6?** Earlier versions installed the codex skill as `AGENTS.md`. That was wrong — codex uses `SKILL.md` for skills (and `AGENTS.md` only as the project-level instructions file, in place of `CLAUDE.md`). If your codex install still has `AGENTS.md`, reinstall with `wbox skills install --platform codex --force`.
109
109
 
110
- The first time an agent invokes the skill, it walks you through populating firm-specific conventions — category values, custom fields, users, defaults, required fields, household naming, named workflows. No further CLI commands needed. After that first run, the bootstrap artifacts self-delete from the skill.
110
+ ### First agent run captures firm conventions
111
+
112
+ `wbox skills bootstrap` populates the API-derived files (categories, custom fields, users) — that's the *quantitative* half. The *qualitative* half (firm defaults, naming conventions, named workflows, required fields) happens the first time an agent invokes the skill: it walks you through the questions in `bootstrap.md` and stamps `firm.onboarded_at` in `_meta.json` when done. On subsequent runs the bootstrap path is skipped automatically.
111
113
 
112
114
  ### Refresh after firm changes
113
115
 
@@ -126,6 +128,7 @@ wbox skills list # show where the skill is installed + last bootstra
126
128
  wbox skills doctor # diagnose install state + token
127
129
  wbox skills sync # copy firm/ from one install to others (Claude Code <-> Codex)
128
130
  wbox skills upgrade # refresh template files (SKILL.md, references/, ...) preserving firm/
131
+ wbox skills mark-onboarded # stamp firm.onboarded_at after qualitative Q&A (the agent calls this for you)
129
132
  wbox skills uninstall # remove the skill
130
133
  ```
131
134
 
@@ -6,6 +6,30 @@ This project uses [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.1.6](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.1.6) — 2026-05-01
10
+
11
+ ### Fixed
12
+ - `wbox skills bootstrap` and `wbox skills refresh` now paginate every API call. Previously they fetched a single page (default `per_page=25`) for each category type, custom-field document type, and the user list, silently truncating any workspace with more than 25 entries per group. Generated `firm/categories.md`, `firm/custom-fields.md`, and `firm/users.md` are now complete.
13
+ - The codex skill installs as `SKILL.md`, not `AGENTS.md`. Codex uses `SKILL.md` for skills (same as Claude Code); `AGENTS.md` is the project-level instructions file (the codex equivalent of `CLAUDE.md`) and was never the right name for a skill. **Migration:** if you installed the skill under codex on 1.1.5 or earlier, reinstall with `wbox skills install --platform codex --force` to drop the stale `AGENTS.md`.
14
+ - The skill's First Run check now keys on `firm.onboarded_at` instead of the mere presence of a `firm` section. Previously, running `wbox skills bootstrap` populated the `firm` section, which made the agent skip the qualitative Q&A in `bootstrap.md` on first invocation. The CLI bootstrap is the *quantitative* half (categories, custom fields, users); the *qualitative* half (firm defaults, naming conventions, named workflows) happens at first agent invocation, and the agent now stamps `firm.onboarded_at` when it finishes.
15
+
16
+ ### Added
17
+ - `wbox skills mark-onboarded` — stamps `firm.onboarded_at` in the skill's `_meta.json`. The agent invokes this as the last step of `bootstrap.md`; users typically don't need to run it manually.
18
+ - `wbox skills list` and `wbox skills doctor` now report onboarded status (`bootstrapped (qualitative pending)` vs `onboarded`).
19
+
20
+ ---
21
+
22
+ ## [1.1.5](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.1.5) — 2026-05-01
23
+
24
+ ### Fixed
25
+ - `wbox skills install`, `wbox skills bootstrap`, and `wbox skills refresh` now honor every token source the rest of the CLI does. Previously these commands only looked at the `--token` flag and `WEALTHBOX_TOKEN` env var, ignoring tokens stored via `wbox config set-token` and `.env` files. Users running `wbox skills install` after `wbox config set-token` saw "Wealthbox token required" errors despite having a token set.
26
+ - The "token required" error message now lists every supported source (flag, env var, config file, `.env`).
27
+
28
+ ### Added
29
+ - `wbox skills install` accepts `--token` (matching `bootstrap`, `refresh`, and `doctor`), so a token can be supplied inline for the post-install bootstrap step.
30
+
31
+ ---
32
+
9
33
  ## [1.1.4](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.1.4) — 2026-05-01
10
34
 
11
35
  ### Added
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wealthbox-cli"
7
- version = "1.1.4"
7
+ version = "1.1.6"
8
8
  description = "CLI and client library for the Wealthbox CRM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -176,17 +176,41 @@ def update_firm_meta(
176
176
  cli_version: str,
177
177
  generated_files: list[str],
178
178
  ) -> None:
179
- """Set the firm section of _meta.json. Preserves any existing template section."""
179
+ """Set the firm section of _meta.json. Preserves the qualitative onboarded_at marker
180
+ (set by the agent via `wbox skills mark-onboarded`) and any existing template section."""
180
181
  now = datetime.now(timezone.utc).isoformat()
181
182
  meta = read_meta(skill_dir)
182
- meta["firm"] = {
183
+ existing_firm = meta.get("firm") or {}
184
+ firm_section: dict[str, Any] = {
183
185
  "identity": identity,
184
186
  "cli_version": cli_version,
185
187
  "files": {name: now for name in generated_files},
186
188
  }
189
+ onboarded_at = existing_firm.get("onboarded_at")
190
+ if onboarded_at:
191
+ firm_section["onboarded_at"] = onboarded_at
192
+ meta["firm"] = firm_section
187
193
  write_meta(skill_dir, meta)
188
194
 
189
195
 
196
+ def mark_firm_onboarded(skill_dir: Path) -> str:
197
+ """Stamp `firm.onboarded_at` with the current UTC time. Returns the timestamp written.
198
+
199
+ Raises FileNotFoundError if no `firm` section exists yet (caller must run
200
+ `wbox skills bootstrap` before marking onboarded)."""
201
+ meta = read_meta(skill_dir)
202
+ firm = meta.get("firm")
203
+ if not firm:
204
+ raise FileNotFoundError(
205
+ f"{meta_path(skill_dir)} has no 'firm' section; run 'wbox skills bootstrap' first."
206
+ )
207
+ now = datetime.now(timezone.utc).isoformat()
208
+ firm["onboarded_at"] = now
209
+ meta["firm"] = firm
210
+ write_meta(skill_dir, meta)
211
+ return now
212
+
213
+
190
214
  def copy_firm_meta(src_skill_dir: Path, dst_skill_dir: Path) -> bool:
191
215
  """Copy the firm section of _meta.json from src to dst, preserving dst's template.
192
216
 
@@ -212,6 +236,8 @@ from importlib.metadata import version as _pkg_version # noqa: E402
212
236
  from wealthbox_tools.client import WealthboxClient # noqa: E402
213
237
  from wealthbox_tools.models import CategoryListQuery, CategoryType, DocumentType # noqa: E402
214
238
 
239
+ from ._util import get_client # noqa: E402
240
+
215
241
 
216
242
  @dataclass
217
243
  class BootstrapResult:
@@ -228,18 +254,18 @@ async def _fetch_all(
228
254
  for ct in CategoryType:
229
255
  if ct is CategoryType.CUSTOM_FIELDS:
230
256
  continue
231
- resp = await client.list_categories(ct)
257
+ resp = await client.list_all_categories(ct)
232
258
  categories[ct.value] = resp.get(ct.value, [])
233
259
 
234
260
  custom_fields: dict[str, list[dict[str, Any]]] = {}
235
261
  for dt in DocumentType:
236
- resp = await client.list_categories(
262
+ resp = await client.list_all_categories(
237
263
  CategoryType.CUSTOM_FIELDS,
238
264
  CategoryListQuery(document_type=dt),
239
265
  )
240
266
  custom_fields[dt.value] = resp.get("custom_fields", [])
241
267
 
242
- users_resp = await client.list_users()
268
+ users_resp = await client.list_all_users()
243
269
  users = users_resp.get("users", [])
244
270
 
245
271
  me = await client.get_me()
@@ -264,7 +290,7 @@ def bootstrap_skill_dir(
264
290
  firm_dir.mkdir(parents=True, exist_ok=True)
265
291
 
266
292
  async def _run() -> tuple[dict, dict, list, dict]:
267
- async with WealthboxClient(token=token) as client:
293
+ async with get_client(token) as client:
268
294
  return await _fetch_all(client)
269
295
 
270
296
  categories, custom_fields, users, firm_identity = asyncio.run(_run())
@@ -40,7 +40,7 @@ def detect_platforms() -> list[Platform]:
40
40
  id="codex",
41
41
  label="Codex",
42
42
  root_dir=home / ".codex" / "skills",
43
- skill_filename="AGENTS.md",
43
+ skill_filename="SKILL.md",
44
44
  requires_project_cwd=False,
45
45
  ),
46
46
  ]
@@ -69,7 +69,6 @@ def install_skill(platform: Platform, src: Path, *, force: bool) -> None:
69
69
  _safe_rmtree(platform, dest)
70
70
  dest.parent.mkdir(parents=True, exist_ok=True)
71
71
  shutil.copytree(src, dest)
72
- _rename_skill_md(platform, dest)
73
72
 
74
73
 
75
74
  def upgrade_skill(platform: Platform, src: Path) -> None:
@@ -85,20 +84,6 @@ def upgrade_skill(platform: Platform, src: Path) -> None:
85
84
  f"Cannot upgrade: nothing installed at {dest}"
86
85
  )
87
86
  shutil.copytree(src, dest, dirs_exist_ok=True)
88
- _rename_skill_md(platform, dest)
89
-
90
-
91
- def _rename_skill_md(platform: Platform, dest: Path) -> None:
92
- """Rename SKILL.md → platform.skill_filename if needed (e.g. AGENTS.md for codex)."""
93
- if platform.skill_filename == "SKILL.md":
94
- return
95
- skill_md = dest / "SKILL.md"
96
- target = dest / platform.skill_filename
97
- if not skill_md.exists():
98
- return
99
- if target.exists():
100
- target.unlink()
101
- skill_md.rename(target)
102
87
 
103
88
 
104
89
  def uninstall_skill(platform: Platform) -> None:
@@ -62,6 +62,12 @@ def get_client(token: str | None = None) -> WealthboxClient:
62
62
  token = os.environ.get("WEALTHBOX_TOKEN")
63
63
  except ImportError:
64
64
  pass
65
+ if not token:
66
+ raise ValueError(
67
+ "Wealthbox token required. Provide one via any of: "
68
+ "--token flag, WEALTHBOX_TOKEN env var, "
69
+ "`wbox config set-token`, or a .env file in the working directory."
70
+ )
65
71
  return WealthboxClient(token=token)
66
72
 
67
73
 
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
 
9
9
  import typer
10
10
 
11
- from ._skill_bootstrap import copy_firm_meta, read_meta, update_template_meta
11
+ from ._skill_bootstrap import copy_firm_meta, mark_firm_onboarded, read_meta, update_template_meta
12
12
  from ._skill_platforms import (
13
13
  Platform,
14
14
  SkillInstallError,
@@ -29,13 +29,14 @@ app = typer.Typer(
29
29
 
30
30
  @app.command("list", help="Show where the skill is installed per platform.")
31
31
  def list_platforms() -> None:
32
- header = ("platform", "path", "status", "template-version", "last-bootstrap")
33
- rows: list[tuple[str, str, str, str, str]] = []
32
+ header = ("platform", "path", "status", "template-version", "last-bootstrap", "onboarded")
33
+ rows: list[tuple[str, str, str, str, str, str]] = []
34
34
  for p in detect_platforms():
35
35
  dest = skill_dir(p)
36
36
  installed = is_installed(p)
37
37
  template_version = "-"
38
38
  last_bootstrap = "-"
39
+ onboarded = "-"
39
40
  if installed:
40
41
  meta = read_meta(dest)
41
42
  template_version = meta.get("template", {}).get("cli_version", "-")
@@ -43,12 +44,14 @@ def list_platforms() -> None:
43
44
  firm_files = firm_section.get("files") or {}
44
45
  if firm_files:
45
46
  last_bootstrap = max(firm_files.values())
47
+ onboarded = firm_section.get("onboarded_at") or "no"
46
48
  rows.append((
47
49
  p.id,
48
50
  str(dest),
49
51
  "installed" if installed else "not installed",
50
52
  template_version,
51
53
  last_bootstrap,
54
+ onboarded,
52
55
  ))
53
56
 
54
57
  widths = [
@@ -98,6 +101,7 @@ def install_cmd(
98
101
  ),
99
102
  force: bool = typer.Option(False, "--force", help="Overwrite existing install."),
100
103
  no_bootstrap: bool = typer.Option(False, "--no-bootstrap", help="Skip the post-install bootstrap prompt."),
104
+ token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
101
105
  ) -> None:
102
106
  targets = _resolve_platforms(platforms_flag) if platforms_flag else _prompt_platforms()
103
107
 
@@ -122,7 +126,7 @@ def install_cmd(
122
126
  if typer.confirm("Run 'wbox skills bootstrap' now?", default=True):
123
127
  from ._skill_bootstrap import bootstrap_skill_dir
124
128
  for target in targets:
125
- bootstrap_skill_dir(skill_dir(target), token=None, generated_only=False)
129
+ bootstrap_skill_dir(skill_dir(target), token=token, generated_only=False)
126
130
  typer.echo(f"OK bootstrapped {target.id}")
127
131
 
128
132
 
@@ -267,13 +271,19 @@ def doctor_cmd(
267
271
  for p in detect_platforms():
268
272
  status = "installed" if is_installed(p) else "not installed"
269
273
  meta = read_meta(skill_dir(p)) if is_installed(p) else {}
270
- bootstrapped = "bootstrapped" if meta.get("firm") else "not bootstrapped"
274
+ firm_section = meta.get("firm") or {}
275
+ if not firm_section:
276
+ firm_state = "not bootstrapped"
277
+ elif firm_section.get("onboarded_at"):
278
+ firm_state = "onboarded"
279
+ else:
280
+ firm_state = "bootstrapped (qualitative pending)"
271
281
  template_v = (meta.get("template") or {}).get("cli_version", "-")
272
282
  upgrade_hint = ""
273
283
  if template_v not in ("-", current):
274
284
  upgrade_hint = f" [upgrade available: {template_v} -> {current}]"
275
285
  typer.echo(
276
- f" {p.id:<22} {status:<16} {bootstrapped:<18} "
286
+ f" {p.id:<22} {status:<16} {firm_state:<36} "
277
287
  f"template={template_v:<10} {skill_dir(p)}{upgrade_hint}"
278
288
  )
279
289
 
@@ -441,6 +451,41 @@ def sync_cmd(
441
451
  )
442
452
 
443
453
 
454
+ @app.command(
455
+ "mark-onboarded",
456
+ help="Stamp firm.onboarded_at in _meta.json. Run after qualitative firm Q&A is captured.",
457
+ )
458
+ def mark_onboarded_cmd(
459
+ platforms_flag: list[str] = typer.Option(
460
+ [], "--platform", "-p",
461
+ help="Platform id. Default: every installed platform.",
462
+ ),
463
+ ) -> None:
464
+ if platforms_flag:
465
+ targets = _resolve_platforms(platforms_flag)
466
+ for t in targets:
467
+ if not is_installed(t):
468
+ typer.echo(f"Error: {t.id!r} is not installed.", err=True)
469
+ raise typer.Exit(code=2)
470
+ else:
471
+ targets = [p for p in detect_platforms() if is_installed(p)]
472
+
473
+ if not targets:
474
+ typer.echo(
475
+ "No installed platforms found. Run 'wbox skills install' first.",
476
+ err=True,
477
+ )
478
+ raise typer.Exit(code=2)
479
+
480
+ for t in targets:
481
+ try:
482
+ ts = mark_firm_onboarded(skill_dir(t))
483
+ except FileNotFoundError as e:
484
+ typer.echo(f"Error: {t.id}: {e}", err=True)
485
+ raise typer.Exit(code=2) from e
486
+ typer.echo(f"OK marked {t.id} onboarded at {ts}")
487
+
488
+
444
489
  @app.command("uninstall", help="Remove the wealthbox-crm skill from a platform.")
445
490
  def uninstall_cmd(
446
491
  platforms_flag: list[str] = typer.Option([], "--platform", "-p"),
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from wealthbox_tools.models import CategoryListQuery, CategoryType
7
+
8
+
9
+ class CategoriesMixin:
10
+ """Categories Resources"""
11
+
12
+ async def list_categories(self, category: CategoryType, query: CategoryListQuery | None = None) -> dict[str, Any]:
13
+ params = query.model_dump(exclude_none=True) if query else None
14
+ resp = await self._request("GET", f"/categories/{category}", params=params) # type: ignore[attr-defined]
15
+ return resp.json()
16
+
17
+ async def list_all_categories(
18
+ self,
19
+ category: CategoryType,
20
+ query: CategoryListQuery | None = None,
21
+ on_progress: Callable[[int, int], None] | None = None,
22
+ ) -> dict[str, Any]:
23
+ """Fetch every page of a category type. Custom fields use collection key 'custom_fields'."""
24
+ params = query.model_dump(exclude_none=True) if query else {}
25
+ params.pop("page", None)
26
+ params.pop("per_page", None)
27
+ collection_key = "custom_fields" if category is CategoryType.CUSTOM_FIELDS else category.value
28
+ return await self.fetch_all_pages( # type: ignore[attr-defined]
29
+ f"/categories/{category}", params, collection_key, on_progress=on_progress
30
+ )
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from wealthbox_tools.models.common import PaginationQuery
7
+
8
+
9
+ class UsersMixin:
10
+ """Users Resource"""
11
+
12
+ async def list_users(self, query: PaginationQuery | None = None) -> dict[str, Any]:
13
+ params = query.model_dump(exclude_none=True) if query else None
14
+ resp = await self._request("GET", "/users", params=params) # type: ignore[attr-defined]
15
+ return resp.json()
16
+
17
+ async def list_all_users(
18
+ self,
19
+ query: PaginationQuery | None = None,
20
+ on_progress: Callable[[int, int], None] | None = None,
21
+ ) -> dict[str, Any]:
22
+ """Fetch every page of users. Use when full enumeration is required (e.g. firm bootstrap)."""
23
+ params = query.model_dump(exclude_none=True) if query else {}
24
+ params.pop("page", None)
25
+ params.pop("per_page", None)
26
+ return await self.fetch_all_pages( # type: ignore[attr-defined]
27
+ "/users", params, "users", on_progress=on_progress
28
+ )
@@ -10,14 +10,20 @@ opportunities, projects, workflows, households, and read-only lookups (categorie
10
10
 
11
11
  ## First Run
12
12
 
13
- If `_meta.json` in this skill's directory does **not** contain a `firm`
14
- section, the firm has not been onboarded yet on this machine. Before
15
- handling the user's request:
13
+ If `_meta.json.firm.onboarded_at` is missing in this skill's directory,
14
+ the firm has not been onboarded yet on this machine. Before handling the
15
+ user's request:
16
16
 
17
17
  1. Read `bootstrap.md` in this skill's directory.
18
18
  2. Follow it end-to-end.
19
- 3. The bootstrap will populate `_meta.json.firm` so this check passes
20
- on subsequent runs.
19
+ 3. The final step writes `_meta.json.firm.onboarded_at`, which makes
20
+ this check pass on subsequent runs.
21
+
22
+ `firm.onboarded_at` is the qualitative-onboarding marker. The CLI
23
+ bootstrap (`wbox skills bootstrap`) only populates the API-derived
24
+ parts of `firm` (identity, generated files); it intentionally does
25
+ *not* set `onboarded_at`, because that requires the per-firm Q&A in
26
+ `bootstrap.md`.
21
27
 
22
28
  ## Staleness Check
23
29
 
@@ -1,11 +1,14 @@
1
1
  # First-Run Bootstrap
2
2
 
3
- You were told to read this because `_meta.json` in the skill root does not
4
- contain a `firm` section. Follow every step below.
3
+ You were told to read this because `_meta.json.firm.onboarded_at` is missing
4
+ in the skill root. Follow every step below, then run the marker command in
5
+ Step 5 so this bootstrap path is skipped on future invocations.
5
6
 
6
7
  ## Step 1 — Fetch API-derived firm data
7
8
 
8
- Run this command:
9
+ Check `_meta.json` first. If it already has a `firm` section with `identity`
10
+ and a `files` map, the CLI bootstrap has already run on this machine and
11
+ you can skip to Step 2. Otherwise, run:
9
12
 
10
13
  ```bash
11
14
  wbox skills bootstrap
@@ -13,11 +16,12 @@ wbox skills bootstrap
13
16
 
14
17
  It populates these files per installed platform:
15
18
 
16
- - `firm/categories.md` — every category type + valid values
17
- - `firm/custom-fields.md` — custom fields per document type + valid values
18
- - `firm/users.md` — user id → name → email
19
+ - `firm/categories.md` — every category type + valid values (paginated; complete)
20
+ - `firm/custom-fields.md` — custom fields per document type + valid values (paginated; complete)
21
+ - `firm/users.md` — user id → name → email (paginated; complete)
19
22
  - `_meta.json` (skill root) — adds the `firm` section: identity, refresh
20
- timestamps, and the CLI version that ran the bootstrap
23
+ timestamps, and the CLI version that ran the bootstrap. Does **not**
24
+ set `onboarded_at` — that's Step 5.
21
25
  - Stub files: `firm/contacts.md`, `firm/tasks.md`, `firm/notes.md`,
22
26
  `firm/events.md`, `firm/opportunities.md`, `firm/projects.md`,
23
27
  `firm/workflows.md` — one-line TODOs you will fill in next.
@@ -32,6 +36,10 @@ in `firm/<resource>.md`, replacing the stub TODO.
32
36
 
33
37
  Read `firm/categories.md` and `firm/custom-fields.md` first so every
34
38
  multiple-choice prompt offers **real** values from the firm's workspace.
39
+ If the advisor mentions a tag, custom-field option, or user that you can't
40
+ find in those files, flag it — they're generated from a paginated fetch and
41
+ *should* be complete. A missing entry usually means the value doesn't exist
42
+ in the workspace yet (rather than a pagination gap).
35
43
 
36
44
  ### firm/contacts.md
37
45
 
@@ -90,11 +98,22 @@ Ask the advisor: "Is there any firm convention, custom process, or preference
90
98
  I haven't asked about that I should follow?" Append freeform answers to
91
99
  `firm/workflows.md` under a `## Other Conventions` heading.
92
100
 
93
- ## Step 4 — Confirm and continue
101
+ ## Step 4 — Mark the firm onboarded
102
+
103
+ Run:
104
+
105
+ ```bash
106
+ wbox skills mark-onboarded
107
+ ```
108
+
109
+ This stamps `firm.onboarded_at` in `_meta.json`. The First Run check in
110
+ `SKILL.md` keys off this field, so future invocations will skip the
111
+ bootstrap path automatically.
112
+
113
+ ## Step 5 — Confirm and continue
94
114
 
95
115
  Tell the advisor: "First-run setup complete — `firm/` populated for <firm
96
116
  name>." Then return to the user's original request.
97
117
 
98
118
  You don't need to delete this file or trim `SKILL.md`. The first-run check
99
- keys on `_meta.json.firm`, which now exists, so the bootstrap path will be
100
- skipped on every subsequent invocation automatically.
119
+ keys on `_meta.json.firm.onboarded_at`, which now exists.
@@ -1,14 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from wealthbox_tools.models import CategoryListQuery, CategoryType
6
-
7
-
8
- class CategoriesMixin:
9
- """Categories Resources"""
10
-
11
- async def list_categories(self, category: CategoryType, query: CategoryListQuery | None = None) -> dict[str, Any]:
12
- params = query.model_dump(exclude_none=True) if query else None
13
- resp = await self._request("GET", f"/categories/{category}", params=params) # type: ignore[attr-defined]
14
- return resp.json()
@@ -1,14 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from wealthbox_tools.models.common import PaginationQuery
6
-
7
-
8
- class UsersMixin:
9
- """Users Resource"""
10
-
11
- async def list_users(self, query: PaginationQuery | None = None) -> dict[str, Any]:
12
- params = query.model_dump(exclude_none=True) if query else None
13
- resp = await self._request("GET", "/users", params=params) # type: ignore[attr-defined]
14
- return resp.json()
File without changes
File without changes
File without changes
File without changes