wealthbox-cli 1.1.1__tar.gz → 1.1.4__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 (86) hide show
  1. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/PKG-INFO +4 -1
  2. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/README.md +3 -0
  3. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/changelog.md +10 -0
  4. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/cli-reference.md +12 -1
  5. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/pyproject.toml +1 -1
  6. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_skill_bootstrap.py +59 -15
  7. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_skill_platforms.py +30 -4
  8. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/categories.py +21 -0
  9. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/skills.py +94 -33
  10. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +24 -4
  11. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +10 -15
  12. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +2 -0
  13. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +14 -9
  14. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.env.example +0 -0
  15. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.github/workflows/ci.yml +0 -0
  16. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.github/workflows/docs.yml +0 -0
  17. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.gitignore +0 -0
  18. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/CONTRIBUTING.md +0 -0
  19. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/LICENSE +0 -0
  20. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/SECURITY.md +0 -0
  21. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/getting-started.md +0 -0
  22. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/index.md +0 -0
  23. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/overrides/main.html +0 -0
  24. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/promotion.md +0 -0
  25. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/mkdocs.yml +0 -0
  26. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/__init__.py +0 -0
  27. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/__init__.py +0 -0
  28. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_config.py +0 -0
  29. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_util.py +0 -0
  30. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/activity.py +0 -0
  31. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/config.py +0 -0
  32. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/contacts.py +0 -0
  33. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/events.py +0 -0
  34. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/households.py +0 -0
  35. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/main.py +0 -0
  36. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/me.py +0 -0
  37. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/notes.py +0 -0
  38. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/opportunities.py +0 -0
  39. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/projects.py +0 -0
  40. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/tasks.py +0 -0
  41. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/users.py +0 -0
  42. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/workflows.py +0 -0
  43. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/__init__.py +0 -0
  44. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/activity.py +0 -0
  45. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/base.py +0 -0
  46. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/categories.py +0 -0
  47. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/comments.py +0 -0
  48. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/contacts.py +0 -0
  49. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/events.py +0 -0
  50. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/households.py +0 -0
  51. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/me.py +0 -0
  52. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/notes.py +0 -0
  53. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/opportunities.py +0 -0
  54. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/projects.py +0 -0
  55. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/tasks.py +0 -0
  56. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/users.py +0 -0
  57. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/workflows.py +0 -0
  58. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/__init__.py +0 -0
  59. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/activity.py +0 -0
  60. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/comments.py +0 -0
  61. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/common.py +0 -0
  62. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/contacts.py +0 -0
  63. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/custom_fields.py +0 -0
  64. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/enums.py +0 -0
  65. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/events.py +0 -0
  66. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/households.py +0 -0
  67. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/notes.py +0 -0
  68. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/opportunities.py +0 -0
  69. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/projects.py +0 -0
  70. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/tasks.py +0 -0
  71. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/workflows.py +0 -0
  72. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/py.typed +0 -0
  73. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
  74. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
  75. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
  76. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
  77. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
  78. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
  79. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
  80. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
  81. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
  82. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
  83. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
  84. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
  85. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +0 -0
  86. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/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.1
3
+ Version: 1.1.4
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/
@@ -166,9 +166,12 @@ This updates the generated half of the skill's firm files; hand-edited policy is
166
166
  wbox skills list # show where the skill is installed + last bootstrap time
167
167
  wbox skills doctor # diagnose install state + token
168
168
  wbox skills sync # copy firm/ from one install to others (Claude Code <-> Codex)
169
+ wbox skills upgrade # refresh template files (SKILL.md, references/, ...) preserving firm/
169
170
  wbox skills uninstall # remove the skill
170
171
  ```
171
172
 
173
+ When the CLI ships a newer template (improved SKILL.md, new reference docs, etc.), `wbox skills upgrade` re-copies the bundled template into every installed platform without touching your firm-specific data in `firm/`. Each install records the template's CLI version in `_meta.json`, so the agent can detect drift and prompt you to upgrade when needed.
174
+
172
175
  `wbox skills sync` is useful if you maintain firm conventions in one platform's install (e.g., your Claude Code user scope) and want the same `firm/` files mirrored to Codex or to a project-scoped `.claude/skills/` folder. The command runs as a wizard by default; pass `--source`, `--target`, and `--yes` for non-interactive use.
173
176
 
174
177
  ### Works with other agents too
@@ -125,9 +125,12 @@ This updates the generated half of the skill's firm files; hand-edited policy is
125
125
  wbox skills list # show where the skill is installed + last bootstrap time
126
126
  wbox skills doctor # diagnose install state + token
127
127
  wbox skills sync # copy firm/ from one install to others (Claude Code <-> Codex)
128
+ wbox skills upgrade # refresh template files (SKILL.md, references/, ...) preserving firm/
128
129
  wbox skills uninstall # remove the skill
129
130
  ```
130
131
 
132
+ When the CLI ships a newer template (improved SKILL.md, new reference docs, etc.), `wbox skills upgrade` re-copies the bundled template into every installed platform without touching your firm-specific data in `firm/`. Each install records the template's CLI version in `_meta.json`, so the agent can detect drift and prompt you to upgrade when needed.
133
+
131
134
  `wbox skills sync` is useful if you maintain firm conventions in one platform's install (e.g., your Claude Code user scope) and want the same `firm/` files mirrored to Codex or to a project-scoped `.claude/skills/` folder. The command runs as a wizard by default; pass `--source`, `--target`, and `--yes` for non-interactive use.
132
135
 
133
136
  ### Works with other agents too
@@ -6,6 +6,16 @@ This project uses [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.1.4](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.1.4) — 2026-05-01
10
+
11
+ ### Added
12
+ - Workspace-level aliases for every resource-scoped category lookup. `wbox categories contact-types`, `wbox categories contact-roles`, `wbox categories event-categories`, `wbox categories task-categories`, and the rest now work alongside their existing `wbox <resource> categories` forms. Both routes call the same API endpoint.
13
+
14
+ ### Docs
15
+ - Skill references (`lookups.md`, `contacts.md`) and `docs/cli-reference.md` updated to surface the workspace-level form as the primary listing.
16
+
17
+ ---
18
+
9
19
  ## [1.0.2](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.0.2) — 2026-03-30
10
20
 
11
21
  ### Changed
@@ -343,7 +343,18 @@ wbox categories opportunity-stages
343
343
  wbox categories opportunity-pipelines
344
344
  wbox categories investment-objectives
345
345
  wbox categories financial-account-types
346
- ```
346
+ wbox categories contact-types
347
+ wbox categories contact-sources
348
+ wbox categories email-types
349
+ wbox categories phone-types
350
+ wbox categories address-types
351
+ wbox categories website-types
352
+ wbox categories contact-roles
353
+ wbox categories event-categories
354
+ wbox categories task-categories
355
+ ```
356
+
357
+ The contact category types (contact-types, contact-sources, email-types, phone-types, address-types, website-types, contact-roles) are also reachable under `wbox contacts categories <name>`. `wbox events categories` and `wbox tasks categories` are aliases for `wbox categories event-categories` and `wbox categories task-categories`. Both forms call the same API endpoint.
347
358
 
348
359
  ### Custom Fields
349
360
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wealthbox-cli"
7
- version = "1.1.1"
7
+ version = "1.1.4"
8
8
  description = "CLI and client library for the Wealthbox CRM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -86,11 +86,11 @@ def render_users_md(users: list[dict[str, Any]]) -> str:
86
86
  FIRM_README = """# firm/ — Firm Customizations
87
87
 
88
88
  This folder holds everything the agent needs to know about your firm's
89
- Wealthbox conventions.
89
+ Wealthbox conventions. Install metadata lives in `../_meta.json` (skill
90
+ root), not here.
90
91
 
91
92
  ## Generated files (overwritten by `wbox skills refresh`)
92
93
 
93
- - `_meta.json` — refresh timestamps, firm identity, CLI version
94
94
  - `categories.md` — every category type + valid values
95
95
  - `custom-fields.md` — custom fields per document type + valid values
96
96
  - `users.md` — user id → name → email
@@ -139,22 +139,66 @@ def write_stubs(firm_dir: Path) -> list[str]:
139
139
  return written
140
140
 
141
141
 
142
- def write_meta_json(
143
- firm_dir: Path,
142
+ def meta_path(skill_dir: Path) -> Path:
143
+ """Location of _meta.json relative to a skill install root."""
144
+ return skill_dir / "_meta.json"
145
+
146
+
147
+ def read_meta(skill_dir: Path) -> dict[str, Any]:
148
+ """Read _meta.json. Returns {} if missing or unreadable."""
149
+ p = meta_path(skill_dir)
150
+ if not p.exists():
151
+ return {}
152
+ try:
153
+ loaded = json.loads(p.read_text(encoding="utf-8"))
154
+ except (json.JSONDecodeError, OSError):
155
+ return {}
156
+ return loaded if isinstance(loaded, dict) else {}
157
+
158
+
159
+ def write_meta(skill_dir: Path, data: dict[str, Any]) -> None:
160
+ meta_path(skill_dir).write_text(
161
+ json.dumps(data, indent=2) + "\n", encoding="utf-8"
162
+ )
163
+
164
+
165
+ def update_template_meta(skill_dir: Path, *, cli_version: str) -> None:
166
+ """Set the template section of _meta.json. Preserves any existing firm section."""
167
+ meta = read_meta(skill_dir)
168
+ meta["template"] = {"cli_version": cli_version}
169
+ write_meta(skill_dir, meta)
170
+
171
+
172
+ def update_firm_meta(
173
+ skill_dir: Path,
144
174
  *,
145
- generated_files: list[str],
146
- firm_identity: dict[str, Any],
175
+ identity: dict[str, Any],
147
176
  cli_version: str,
177
+ generated_files: list[str],
148
178
  ) -> None:
179
+ """Set the firm section of _meta.json. Preserves any existing template section."""
149
180
  now = datetime.now(timezone.utc).isoformat()
150
- meta = {
151
- "firm": firm_identity,
181
+ meta = read_meta(skill_dir)
182
+ meta["firm"] = {
183
+ "identity": identity,
152
184
  "cli_version": cli_version,
153
185
  "files": {name: now for name in generated_files},
154
186
  }
155
- (firm_dir / "_meta.json").write_text(
156
- json.dumps(meta, indent=2) + "\n", encoding="utf-8"
157
- )
187
+ write_meta(skill_dir, meta)
188
+
189
+
190
+ def copy_firm_meta(src_skill_dir: Path, dst_skill_dir: Path) -> bool:
191
+ """Copy the firm section of _meta.json from src to dst, preserving dst's template.
192
+
193
+ Returns True if a firm section was copied, False if src had none.
194
+ """
195
+ src_firm = read_meta(src_skill_dir).get("firm")
196
+ if not src_firm:
197
+ return False
198
+ dst_meta = read_meta(dst_skill_dir)
199
+ dst_meta["firm"] = src_firm
200
+ write_meta(dst_skill_dir, dst_meta)
201
+ return True
158
202
 
159
203
 
160
204
  # --------------------------------------------------------------------------- #
@@ -230,11 +274,11 @@ def bootstrap_skill_dir(
230
274
  (firm_dir / "users.md").write_text(render_users_md(users), encoding="utf-8")
231
275
 
232
276
  generated = ["categories.md", "custom-fields.md", "users.md"]
233
- write_meta_json(
234
- firm_dir,
235
- generated_files=generated,
236
- firm_identity=firm_identity,
277
+ update_firm_meta(
278
+ skill_dir,
279
+ identity=firm_identity,
237
280
  cli_version=_pkg_version("wealthbox-cli"),
281
+ generated_files=generated,
238
282
  )
239
283
 
240
284
  wrote_stubs = 0
@@ -69,10 +69,36 @@ 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
- if platform.skill_filename != "SKILL.md":
73
- skill_md = dest / "SKILL.md"
74
- if skill_md.exists():
75
- skill_md.rename(dest / platform.skill_filename)
72
+ _rename_skill_md(platform, dest)
73
+
74
+
75
+ def upgrade_skill(platform: Platform, src: Path) -> None:
76
+ """Re-copy the bundled template over an existing install.
77
+
78
+ Preserves the user's `firm/` directory and root `_meta.json` (the
79
+ bundled template ships neither). Refreshes SKILL.md, references/,
80
+ firm-examples/, and bootstrap.md.
81
+ """
82
+ dest = skill_dir(platform)
83
+ if not dest.is_dir() or not (dest / platform.skill_filename).is_file():
84
+ raise SkillInstallError(
85
+ f"Cannot upgrade: nothing installed at {dest}"
86
+ )
87
+ 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)
76
102
 
77
103
 
78
104
  def uninstall_skill(platform: Platform) -> None:
@@ -26,6 +26,27 @@ app.command("investment-objectives", help="List investment objective options.")(
26
26
  app.command("financial-account-types", help="List financial account type options.")(
27
27
  make_category_command(CategoryType.FINANCIAL_ACCOUNT_TYPES)
28
28
  )
29
+ app.command("contact-types", help="List contact type options.")(make_category_command(CategoryType.CONTACT_TYPES))
30
+ app.command("contact-sources", help="List contact source options.")(
31
+ make_category_command(CategoryType.CONTACT_SOURCES)
32
+ )
33
+ app.command("email-types", help="List email type options.")(make_category_command(CategoryType.EMAIL_TYPES))
34
+ app.command("phone-types", help="List phone type options.")(make_category_command(CategoryType.PHONE_TYPES))
35
+ app.command("address-types", help="List address type options.")(
36
+ make_category_command(CategoryType.ADDRESS_TYPES)
37
+ )
38
+ app.command("website-types", help="List website type options.")(
39
+ make_category_command(CategoryType.WEBSITE_TYPES)
40
+ )
41
+ app.command("contact-roles", help="List contact role options.")(
42
+ make_category_command(CategoryType.CONTACT_ROLES)
43
+ )
44
+ app.command("event-categories", help="List event category options.")(
45
+ make_category_command(CategoryType.EVENT_CATEGORIES)
46
+ )
47
+ app.command("task-categories", help="List task category options.")(
48
+ make_category_command(CategoryType.TASK_CATEGORIES)
49
+ )
29
50
 
30
51
 
31
52
  @app.command("custom-fields", help="List custom field categories. Optionally filter by document type.")
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  import shutil
5
4
  from datetime import datetime, timezone
5
+ from importlib.metadata import version as _pkg_version
6
6
  from importlib.resources import as_file, files
7
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
12
  from ._skill_platforms import (
12
13
  Platform,
13
14
  SkillInstallError,
@@ -16,6 +17,7 @@ from ._skill_platforms import (
16
17
  is_installed,
17
18
  skill_dir,
18
19
  uninstall_skill,
20
+ upgrade_skill,
19
21
  )
20
22
 
21
23
  app = typer.Typer(
@@ -27,31 +29,31 @@ app = typer.Typer(
27
29
 
28
30
  @app.command("list", help="Show where the skill is installed per platform.")
29
31
  def list_platforms() -> None:
30
- header = ("platform", "path", "status", "last-bootstrap")
31
- rows: list[tuple[str, str, str, str]] = []
32
+ header = ("platform", "path", "status", "template-version", "last-bootstrap")
33
+ rows: list[tuple[str, str, str, str, str]] = []
32
34
  for p in detect_platforms():
33
35
  dest = skill_dir(p)
34
36
  installed = is_installed(p)
35
- meta_file = dest / "firm" / "_meta.json"
37
+ template_version = "-"
36
38
  last_bootstrap = "-"
37
- if meta_file.exists():
38
- try:
39
- meta = json.loads(meta_file.read_text())
40
- files = meta.get("files", {})
41
- if files:
42
- last_bootstrap = max(files.values())
43
- except (json.JSONDecodeError, OSError):
44
- last_bootstrap = "(unreadable)"
39
+ if installed:
40
+ meta = read_meta(dest)
41
+ template_version = meta.get("template", {}).get("cli_version", "-")
42
+ firm_section = meta.get("firm") or {}
43
+ firm_files = firm_section.get("files") or {}
44
+ if firm_files:
45
+ last_bootstrap = max(firm_files.values())
45
46
  rows.append((
46
47
  p.id,
47
48
  str(dest),
48
49
  "installed" if installed else "not installed",
50
+ template_version,
49
51
  last_bootstrap,
50
52
  ))
51
53
 
52
54
  widths = [
53
55
  max(len(r[i]) for r in ([header] + rows))
54
- for i in range(4)
56
+ for i in range(len(header))
55
57
  ]
56
58
  fmt = " ".join(f"{{:<{w}}}" for w in widths)
57
59
  typer.echo(fmt.format(*header))
@@ -113,6 +115,7 @@ def install_cmd(
113
115
  except SkillInstallError as e:
114
116
  typer.echo(f"Error installing to {target.id}: {e}", err=True)
115
117
  raise typer.Exit(code=1) from e
118
+ update_template_meta(skill_dir(target), cli_version=_pkg_version("wealthbox-cli"))
116
119
  typer.echo(f"OK installed to {skill_dir(target)}")
117
120
 
118
121
  if not no_bootstrap:
@@ -192,26 +195,65 @@ def refresh_cmd(
192
195
  raise typer.Exit(code=2)
193
196
 
194
197
  for t in targets:
195
- meta_path = skill_dir(t) / "firm" / "_meta.json"
196
- if meta_path.exists():
198
+ meta = read_meta(skill_dir(t))
199
+ firm_files = (meta.get("firm") or {}).get("files") or {}
200
+ if firm_files:
197
201
  try:
198
- meta = json.loads(meta_path.read_text())
199
- ts_values = list(meta.get("files", {}).values())
200
- if ts_values:
201
- oldest = min(datetime.fromisoformat(ts) for ts in ts_values)
202
- age_days = (datetime.now(timezone.utc) - oldest).days
203
- if age_days > staleness_days:
204
- typer.echo(
205
- f"! {t.id}: generated files were {age_days} days stale - refreshing now",
206
- err=True,
207
- )
208
- except (json.JSONDecodeError, OSError, ValueError):
202
+ oldest = min(datetime.fromisoformat(ts) for ts in firm_files.values())
203
+ age_days = (datetime.now(timezone.utc) - oldest).days
204
+ if age_days > staleness_days:
205
+ typer.echo(
206
+ f"! {t.id}: generated files were {age_days} days stale - refreshing now",
207
+ err=True,
208
+ )
209
+ except ValueError:
209
210
  pass
210
211
 
211
212
  bootstrap_skill_dir(skill_dir(t), token=token, generated_only=True)
212
213
  typer.echo(f"OK refreshed {t.id}")
213
214
 
214
215
 
216
+ @app.command(
217
+ "upgrade",
218
+ help="Refresh template files (SKILL.md, references/, firm-examples/, bootstrap.md) "
219
+ "in every installed platform. Preserves firm/ and _meta.json firm section.",
220
+ )
221
+ def upgrade_cmd(
222
+ platforms_flag: list[str] = typer.Option(
223
+ [], "--platform", "-p",
224
+ help="Platform id. Default: every installed platform.",
225
+ ),
226
+ ) -> None:
227
+ if platforms_flag:
228
+ targets = _resolve_platforms(platforms_flag)
229
+ for t in targets:
230
+ if not is_installed(t):
231
+ typer.echo(f"Error: {t.id!r} is not installed.", err=True)
232
+ raise typer.Exit(code=2)
233
+ else:
234
+ targets = [p for p in detect_platforms() if is_installed(p)]
235
+
236
+ if not targets:
237
+ typer.echo(
238
+ "No installed platforms found. Run 'wbox skills install' first.",
239
+ err=True,
240
+ )
241
+ raise typer.Exit(code=2)
242
+
243
+ current = _pkg_version("wealthbox-cli")
244
+
245
+ with as_file(files("wealthbox_tools").joinpath("skills/wealthbox-crm")) as src:
246
+ for t in targets:
247
+ existing = (read_meta(skill_dir(t)).get("template") or {}).get("cli_version", "-")
248
+ try:
249
+ upgrade_skill(t, Path(src))
250
+ except SkillInstallError as e:
251
+ typer.echo(f"Error upgrading {t.id}: {e}", err=True)
252
+ raise typer.Exit(code=1) from e
253
+ update_template_meta(skill_dir(t), cli_version=current)
254
+ typer.echo(f"OK upgraded {t.id}: {existing} -> {current}")
255
+
256
+
215
257
  @app.command("doctor", help="Diagnose install state and token.")
216
258
  def doctor_cmd(
217
259
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
@@ -220,13 +262,19 @@ def doctor_cmd(
220
262
 
221
263
  from ._util import run_client
222
264
 
265
+ current = _pkg_version("wealthbox-cli")
223
266
  typer.echo("# Skill install state")
224
267
  for p in detect_platforms():
225
268
  status = "installed" if is_installed(p) else "not installed"
226
- meta_path = skill_dir(p) / "firm" / "_meta.json"
227
- bootstrapped = "bootstrapped" if meta_path.exists() else "not bootstrapped"
269
+ meta = read_meta(skill_dir(p)) if is_installed(p) else {}
270
+ bootstrapped = "bootstrapped" if meta.get("firm") else "not bootstrapped"
271
+ template_v = (meta.get("template") or {}).get("cli_version", "-")
272
+ upgrade_hint = ""
273
+ if template_v not in ("-", current):
274
+ upgrade_hint = f" [upgrade available: {template_v} -> {current}]"
228
275
  typer.echo(
229
- f" {p.id:<22} {status:<16} {bootstrapped:<18} {skill_dir(p)}"
276
+ f" {p.id:<22} {status:<16} {bootstrapped:<18} "
277
+ f"template={template_v:<10} {skill_dir(p)}{upgrade_hint}"
230
278
  )
231
279
 
232
280
  typer.echo("\n# Token")
@@ -328,7 +376,12 @@ def sync_cmd(
328
376
  )
329
377
  raise typer.Exit(code=2)
330
378
 
331
- others = [p for p in installed if p.id != source.id]
379
+ src_resolved = skill_dir(source).resolve()
380
+
381
+ def _is_same_path(p: Platform) -> bool:
382
+ return skill_dir(p).resolve() == src_resolved
383
+
384
+ others = [p for p in installed if p.id != source.id and not _is_same_path(p)]
332
385
 
333
386
  if all_targets and target_ids:
334
387
  typer.echo("Error: --all-targets and --target are mutually exclusive.", err=True)
@@ -339,8 +392,12 @@ def sync_cmd(
339
392
  elif target_ids:
340
393
  targets = _resolve_platforms(target_ids)
341
394
  for t in targets:
342
- if t.id == source.id:
343
- typer.echo(f"Error: target {t.id!r} is the same as the source.", err=True)
395
+ if t.id == source.id or _is_same_path(t):
396
+ typer.echo(
397
+ f"Error: target {t.id!r} is the same as the source "
398
+ f"(resolves to {src_resolved}).",
399
+ err=True,
400
+ )
344
401
  raise typer.Exit(code=2)
345
402
  if not is_installed(t):
346
403
  typer.echo(f"Error: target {t.id!r} is not installed.", err=True)
@@ -377,7 +434,11 @@ def sync_cmd(
377
434
  tgt_firm = skill_dir(t) / "firm"
378
435
  tgt_firm.mkdir(parents=True, exist_ok=True)
379
436
  shutil.copytree(src_firm, tgt_firm, dirs_exist_ok=True)
380
- typer.echo(f"OK synced {len(src_files)} files: {source.id} -> {t.id}")
437
+ meta_copied = copy_firm_meta(skill_dir(source), skill_dir(t))
438
+ meta_note = "" if meta_copied else " (no _meta.json firm section on source)"
439
+ typer.echo(
440
+ f"OK synced {len(src_files)} files: {source.id} -> {t.id}{meta_note}"
441
+ )
381
442
 
382
443
 
383
444
  @app.command("uninstall", help="Remove the wealthbox-crm skill from a platform.")
@@ -8,14 +8,34 @@ description: Use when the user wants to interact with Wealthbox CRM — creating
8
8
  Execute Wealthbox CRM operations via the `wbox` CLI. Supports contacts, tasks, notes, events,
9
9
  opportunities, projects, workflows, households, and read-only lookups (categories, users, activity, me).
10
10
 
11
- ## First Run (self-delete after completion)
11
+ ## First Run
12
12
 
13
- If `firm/_meta.json` does not exist in this skill's directory, this is the
14
- first invocation on this machine. Before handling the user's request:
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:
15
16
 
16
17
  1. Read `bootstrap.md` in this skill's directory.
17
18
  2. Follow it end-to-end.
18
- 3. When done, this section and `bootstrap.md` will have been deleted.
19
+ 3. The bootstrap will populate `_meta.json.firm` so this check passes
20
+ on subsequent runs.
21
+
22
+ ## Staleness Check
23
+
24
+ `_meta.json.template.cli_version` records which `wbox` version installed
25
+ the template files. Compare it to the running CLI version:
26
+
27
+ ```bash
28
+ wbox --version
29
+ ```
30
+
31
+ If the template version is older, suggest the user run:
32
+
33
+ ```bash
34
+ wbox skills upgrade
35
+ ```
36
+
37
+ This refreshes SKILL.md, references/, firm-examples/, and bootstrap.md
38
+ across every installed platform while preserving `firm/`.
19
39
 
20
40
  ## Prerequisites
21
41
 
@@ -1,8 +1,7 @@
1
1
  # First-Run Bootstrap
2
2
 
3
- You were told to read this because `firm/_meta.json` does not exist in the skill
4
- directory. Follow every step below. When you finish, delete this file and trim
5
- the `## First Run` section out of `SKILL.md`.
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.
6
5
 
7
6
  ## Step 1 — Fetch API-derived firm data
8
7
 
@@ -17,12 +16,13 @@ It populates these files per installed platform:
17
16
  - `firm/categories.md` — every category type + valid values
18
17
  - `firm/custom-fields.md` — custom fields per document type + valid values
19
18
  - `firm/users.md` — user id → name → email
20
- - `firm/_meta.json` — refresh timestamps + firm identity
19
+ - `_meta.json` (skill root) — adds the `firm` section: identity, refresh
20
+ timestamps, and the CLI version that ran the bootstrap
21
21
  - Stub files: `firm/contacts.md`, `firm/tasks.md`, `firm/notes.md`,
22
22
  `firm/events.md`, `firm/opportunities.md`, `firm/projects.md`,
23
23
  `firm/workflows.md` — one-line TODOs you will fill in next.
24
24
 
25
- Verify `firm/_meta.json` now exists before continuing.
25
+ Verify the `firm` section now exists in `_meta.json` before continuing.
26
26
 
27
27
  ## Step 2 — Gather firm policy (per-resource)
28
28
 
@@ -90,16 +90,11 @@ Ask the advisor: "Is there any firm convention, custom process, or preference
90
90
  I haven't asked about that I should follow?" Append freeform answers to
91
91
  `firm/workflows.md` under a `## Other Conventions` heading.
92
92
 
93
- ## Step 4 — Self-trim
94
-
95
- Now that all firm files are filled in:
96
-
97
- 1. Delete `bootstrap.md` (this file).
98
- 2. Open `SKILL.md`. Find the `## First Run (self-delete after completion)`
99
- section. Delete that heading and everything under it through (but not
100
- including) the next `## ` heading.
101
-
102
- ## Step 5 — Confirm and continue
93
+ ## Step 4 — Confirm and continue
103
94
 
104
95
  Tell the advisor: "First-run setup complete — `firm/` populated for <firm
105
96
  name>." Then return to the user's original request.
97
+
98
+ 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.
@@ -122,6 +122,8 @@ No output on success.
122
122
 
123
123
  ## Contact Categories
124
124
 
125
+ Each of these is also available at the workspace level via `wbox categories <name>` — both forms return the same data.
126
+
125
127
  ```bash
126
128
  wbox contacts categories contact-types
127
129
  wbox contacts categories contact-sources
@@ -36,7 +36,9 @@ wbox activity list [OPTIONS]
36
36
 
37
37
  **Important:** Activity uses cursor-based pagination, NOT `--page`/`--per-page`. Use the cursor value from the previous response for the next page.
38
38
 
39
- ## Workspace-Level Categories
39
+ ## Categories
40
+
41
+ Every category type is available under `wbox categories <name>`. The full set:
40
42
 
41
43
  ```bash
42
44
  wbox categories tags
@@ -45,16 +47,19 @@ wbox categories opportunity-stages
45
47
  wbox categories opportunity-pipelines
46
48
  wbox categories investment-objectives
47
49
  wbox categories financial-account-types
50
+ wbox categories contact-types
51
+ wbox categories contact-sources
52
+ wbox categories email-types
53
+ wbox categories phone-types
54
+ wbox categories address-types
55
+ wbox categories website-types
56
+ wbox categories contact-roles
57
+ wbox categories event-categories
58
+ wbox categories task-categories
48
59
  wbox categories custom-fields [--document-type STR] [--page INT] [--per-page INT] [--format ...]
49
60
  ```
50
61
 
51
- ## Resource-Scoped Categories
52
-
53
- ```bash
54
- wbox contacts categories {contact-types|contact-sources|email-types|phone-types|address-types|website-types|contact-roles}
55
- wbox events categories
56
- wbox tasks categories
57
- ```
62
+ The contact-related types are also reachable as aliases under `wbox contacts categories <name>` (contact-types, contact-sources, email-types, phone-types, address-types, website-types, contact-roles). Likewise, `wbox events categories` and `wbox tasks categories` are aliases for `wbox categories event-categories` and `wbox categories task-categories`. Either form returns the same data.
58
63
 
59
64
  ## Examples
60
65
 
@@ -69,5 +74,5 @@ wbox users list --format table
69
74
  wbox activity list --contact 12345 --format table
70
75
 
71
76
  # Look up valid contact types
72
- wbox contacts categories contact-types
77
+ wbox categories contact-types
73
78
  ```
File without changes
File without changes
File without changes
File without changes