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.
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/PKG-INFO +4 -1
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/README.md +3 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/changelog.md +10 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/cli-reference.md +12 -1
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/pyproject.toml +1 -1
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_skill_bootstrap.py +59 -15
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_skill_platforms.py +30 -4
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/categories.py +21 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/skills.py +94 -33
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +24 -4
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +10 -15
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +2 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +14 -9
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.env.example +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.github/workflows/ci.yml +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.github/workflows/docs.yml +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/.gitignore +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/CONTRIBUTING.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/LICENSE +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/SECURITY.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/getting-started.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/index.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/overrides/main.html +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/docs/promotion.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/mkdocs.yml +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/__init__.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/__init__.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_config.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/_util.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/activity.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/config.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/contacts.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/events.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/households.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/main.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/me.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/notes.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/opportunities.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/projects.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/tasks.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/users.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/cli/workflows.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/__init__.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/activity.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/base.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/categories.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/comments.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/contacts.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/events.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/households.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/me.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/notes.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/opportunities.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/projects.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/tasks.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/users.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/client/workflows.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/__init__.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/activity.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/comments.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/common.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/contacts.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/custom_fields.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/enums.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/events.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/households.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/notes.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/opportunities.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/projects.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/tasks.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/models/workflows.py +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/py.typed +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
- {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
156
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
37
|
+
template_version = "-"
|
|
36
38
|
last_bootstrap = "-"
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
227
|
-
bootstrapped = "bootstrapped" if
|
|
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}
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.")
|
{wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md
RENAMED
|
@@ -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
|
|
11
|
+
## First Run
|
|
12
12
|
|
|
13
|
-
If `
|
|
14
|
-
|
|
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.
|
|
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
|
|
{wealthbox_cli-1.1.1 → wealthbox_cli-1.1.4}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md
RENAMED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# First-Run Bootstrap
|
|
2
2
|
|
|
3
|
-
You were told to read this because `
|
|
4
|
-
|
|
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
|
-
- `
|
|
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
|
|
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 —
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
77
|
+
wbox categories contact-types
|
|
73
78
|
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|