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.
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/PKG-INFO +7 -4
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/README.md +6 -3
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/changelog.md +24 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/pyproject.toml +1 -1
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_skill_bootstrap.py +32 -6
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_skill_platforms.py +1 -16
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_util.py +6 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/skills.py +51 -6
- wealthbox_cli-1.1.6/src/wealthbox_tools/client/categories.py +30 -0
- wealthbox_cli-1.1.6/src/wealthbox_tools/client/users.py +28 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +11 -5
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +29 -10
- wealthbox_cli-1.1.4/src/wealthbox_tools/client/categories.py +0 -14
- wealthbox_cli-1.1.4/src/wealthbox_tools/client/users.py +0 -14
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.env.example +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.github/workflows/ci.yml +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.github/workflows/docs.yml +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/.gitignore +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/CONTRIBUTING.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/LICENSE +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/SECURITY.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/cli-reference.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/getting-started.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/index.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/overrides/main.html +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/docs/promotion.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/mkdocs.yml +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/__init__.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/__init__.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/_config.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/activity.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/categories.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/config.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/contacts.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/events.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/households.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/main.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/me.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/notes.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/opportunities.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/projects.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/tasks.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/users.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/cli/workflows.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/__init__.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/activity.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/base.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/comments.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/contacts.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/events.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/households.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/me.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/notes.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/opportunities.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/projects.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/tasks.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/client/workflows.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/__init__.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/activity.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/comments.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/common.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/contacts.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/custom_fields.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/enums.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/events.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/households.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/notes.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/opportunities.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/projects.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/tasks.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/models/workflows.py +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/py.typed +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
- {wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +0 -0
- {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.
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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="
|
|
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=
|
|
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
|
-
|
|
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} {
|
|
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
|
+
)
|
{wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md
RENAMED
|
@@ -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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
|
{wealthbox_cli-1.1.4 → wealthbox_cli-1.1.6}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md
RENAMED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# First-Run Bootstrap
|
|
2
2
|
|
|
3
|
-
You were told to read this because `_meta.json`
|
|
4
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
|
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
|
|
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
|