wealthbox-cli 1.2.0__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.claude-plugin/marketplace.json +1 -1
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.codex-plugin/plugin.json +1 -1
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/PKG-INFO +1 -1
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/changelog.md +16 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/.claude-plugin/plugin.json +1 -1
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/pyproject.toml +1 -1
- wealthbox_cli-1.4.0/src/wealthbox_tools/cli/_skill_platforms.py +235 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_util.py +26 -0
- wealthbox_cli-1.4.0/src/wealthbox_tools/cli/doctor.py +203 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/main.py +7 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/skills.py +28 -54
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/tasks.py +28 -7
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +4 -1
- wealthbox_cli-1.2.0/src/wealthbox_tools/cli/_skill_platforms.py +0 -107
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.env.example +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.github/workflows/ci.yml +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.github/workflows/docs.yml +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/.gitignore +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/CONTRIBUTING.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/LICENSE +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/README.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/SECURITY.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/SKILL.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/bootstrap.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/contacts.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/events.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/notes.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/opportunities.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/projects.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/tasks.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/firm-examples/workflows.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/contacts.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/events.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/households.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/lookups.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/notes.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/opportunities.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/projects.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/tasks.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/codex-skill/wealthbox-crm/references/workflows.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/cli-reference.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/getting-started.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/index.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/overrides/main.html +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/docs/promotion.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/mkdocs.yml +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/SKILL.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/bootstrap.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/events.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/notes.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/projects.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/contacts.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/events.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/households.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/lookups.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/notes.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/opportunities.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/projects.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/tasks.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/skills/wealthbox-crm/references/workflows.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/__init__.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/__init__.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_config.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_skill_bootstrap.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/_skill_paths.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/activity.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/categories.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/config.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/contacts.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/events.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/households.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/me.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/notes.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/opportunities.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/projects.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/users.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/cli/workflows.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/__init__.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/activity.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/base.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/categories.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/comments.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/contacts.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/events.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/households.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/me.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/notes.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/opportunities.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/projects.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/tasks.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/users.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/client/workflows.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/__init__.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/activity.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/comments.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/common.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/contacts.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/custom_fields.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/enums.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/events.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/households.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/notes.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/opportunities.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/projects.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/tasks.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/models/workflows.py +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/py.typed +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
- {wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/src/wealthbox_tools/skills/wealthbox-crm/references/workflows.md +0 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "wealthbox-crm",
|
|
11
11
|
"source": "./plugins/wealthbox-crm",
|
|
12
12
|
"description": "Wealthbox CRM agent skill — natural-language CRM automation backed by the `wbox` CLI.",
|
|
13
|
-
"version": "1.
|
|
13
|
+
"version": "1.3.0",
|
|
14
14
|
"category": "productivity",
|
|
15
15
|
"tags": [
|
|
16
16
|
"wealthbox",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wealthbox-crm",
|
|
3
3
|
"description": "Wealthbox CRM agent skill — drive contacts, tasks, notes, events, and households via natural language. Bundles SKILL.md, references, and firm-bootstrap workflow. Requires the `wbox` CLI (auto-install instructions in SKILL.md).",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"author": "Kadin Bullock",
|
|
6
6
|
"homepage": "https://github.com/massive-value/wealthbox-cli",
|
|
7
7
|
"repository": "https://github.com/massive-value/wealthbox-cli",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wealthbox-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: CLI and client library for the Wealthbox CRM API
|
|
5
5
|
Project-URL: Homepage, https://github.com/massive-value/wealthbox-cli
|
|
6
6
|
Project-URL: Documentation, https://massive-value.github.io/wealthbox-cli/
|
|
@@ -6,6 +6,22 @@ This project uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [1.3.0](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.3.0) — 2026-05-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`wbox doctor`** — comprehensive top-level health check at the CLI root. Reports the wbox CLI version + Python version + binary location; authentication source detection (flag / env var / config file / `.env`) plus a smoke test against `/me`; agent CLI presence (`claude` / `codex` on PATH); legacy skill installs; plugin installs (managed via `claude plugin install` / `codex plugin install`); firm data state with file count, generated-vs-hand-edited split, and oldest-generated-file timestamp; and a Summary section listing actionable issues. `wbox skills doctor` keeps working as an alias of the new top-level command — both call the same function so output never drifts.
|
|
14
|
+
- `wbox skills list` and `wbox skills doctor` now detect plugin-installed copies of `wealthbox-crm` under `~/.claude/plugins/cache/.../skills/wealthbox-crm/` and `~/.codex/plugins/cache/.../skills/wealthbox-crm/`. Previously a user who installed via the marketplace plugin path saw "not installed" everywhere even though the plugin was actively serving the skill.
|
|
15
|
+
- Bootstrap installer (`scripts/install.sh` / `scripts/install.ps1`) now prefers the Claude Code plugin marketplace path when `claude` is on PATH — runs `claude plugin marketplace add` + `claude plugin install` directly, then offers a separate Codex install. Falls back to the legacy `wbox skills install` picker when no `claude` CLI is detected.
|
|
16
|
+
- Bootstrap installer pre-flight checks Windows PowerShell `ExecutionPolicy` and offers to set `RemoteSigned` for the current user if needed (no admin required), instead of bombing partway through with Astral's terse error.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Bootstrap `install.sh` no longer hangs at startup. The previous version did `exec </dev/tty` at the top, which cut off bash's source of piped script content (`curl … | bash`) and waited forever for the rest of the script to arrive on the user's keyboard. The whole body is now wrapped in a `main()` function so bash reads it fully before executing.
|
|
21
|
+
- Bootstrap `install.ps1` no longer auto-closes its window on error. Wrapped in try/catch/finally with explicit step headers and a "Press Enter to close" pause so a transient PowerShell host stays open long enough to show the actual error.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
9
25
|
## [1.2.0](https://github.com/massive-value/wealthbox-cli/releases/tag/v1.2.0) — 2026-05-02
|
|
10
26
|
|
|
11
27
|
### Changed
|
{wealthbox_cli-1.2.0 → wealthbox_cli-1.4.0}/plugins/wealthbox-crm/.claude-plugin/plugin.json
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wealthbox-crm",
|
|
3
3
|
"description": "Wealthbox CRM agent skill — drive contacts, tasks, notes, events, and households via natural language. Bundles SKILL.md, references, and firm-bootstrap workflow. Requires the `wbox` CLI (auto-install instructions in SKILL.md).",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kadin Bullock"
|
|
7
7
|
},
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SKILL_NAME = "wealthbox-crm"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Platform:
|
|
13
|
+
id: str
|
|
14
|
+
label: str
|
|
15
|
+
root_dir: Path
|
|
16
|
+
skill_filename: str
|
|
17
|
+
requires_project_cwd: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class PluginInstall:
|
|
22
|
+
"""A copy of `wealthbox-crm` discovered inside an agent host's plugin cache.
|
|
23
|
+
|
|
24
|
+
These are managed by the host CLI (`claude plugin install ...` or the
|
|
25
|
+
Codex equivalent) — `wbox skills install` does not write here, and
|
|
26
|
+
`wbox skills uninstall` does not touch them.
|
|
27
|
+
|
|
28
|
+
`active` is True when the host's `installed_plugins.json` has a record
|
|
29
|
+
pointing at this version's installPath. Old/cached versions kept around
|
|
30
|
+
by the host for rollback report active=False. If the host has no
|
|
31
|
+
`installed_plugins.json` (e.g., older Claude Code, or a Codex setup that
|
|
32
|
+
hasn't shipped one yet), `active` defaults to True since we can't tell.
|
|
33
|
+
"""
|
|
34
|
+
host: str # 'claude-code-user' | 'claude-code-project' | 'codex'
|
|
35
|
+
marketplace: str
|
|
36
|
+
version: str
|
|
37
|
+
skill_dir: Path
|
|
38
|
+
active: bool = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _home() -> Path:
|
|
42
|
+
return Path.home()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_active_install_paths(plugins_root: Path) -> set[Path] | None:
|
|
46
|
+
"""Read <plugins_root>/installed_plugins.json and return the set of
|
|
47
|
+
install-path roots the host considers active across every scope.
|
|
48
|
+
|
|
49
|
+
Returns None when no state file exists — caller should treat every
|
|
50
|
+
discovered plugin as active in that case (we don't have enough info
|
|
51
|
+
to differentiate cached vs active).
|
|
52
|
+
"""
|
|
53
|
+
state_file = plugins_root / "installed_plugins.json"
|
|
54
|
+
if not state_file.is_file():
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(state_file.read_text(encoding="utf-8"))
|
|
58
|
+
except (json.JSONDecodeError, OSError):
|
|
59
|
+
return None
|
|
60
|
+
active: set[Path] = set()
|
|
61
|
+
for entries in (data.get("plugins") or {}).values():
|
|
62
|
+
if not isinstance(entries, list):
|
|
63
|
+
continue
|
|
64
|
+
for entry in entries:
|
|
65
|
+
install_path = entry.get("installPath")
|
|
66
|
+
if not install_path:
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
active.add(Path(install_path).resolve())
|
|
70
|
+
except OSError:
|
|
71
|
+
active.add(Path(install_path))
|
|
72
|
+
return active
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _scan_plugin_cache(
|
|
76
|
+
cache_root: Path,
|
|
77
|
+
host: str,
|
|
78
|
+
active_paths: set[Path] | None,
|
|
79
|
+
) -> list[PluginInstall]:
|
|
80
|
+
"""Find any `<marketplace>/<plugin-name>/<version>/skills/wealthbox-crm/`
|
|
81
|
+
matches under a Claude Code- or Codex-style plugin cache root."""
|
|
82
|
+
if not cache_root.is_dir():
|
|
83
|
+
return []
|
|
84
|
+
out: list[PluginInstall] = []
|
|
85
|
+
for marketplace_dir in cache_root.iterdir():
|
|
86
|
+
if not marketplace_dir.is_dir():
|
|
87
|
+
continue
|
|
88
|
+
plugin_dir = marketplace_dir / SKILL_NAME
|
|
89
|
+
if not plugin_dir.is_dir():
|
|
90
|
+
continue
|
|
91
|
+
for version_dir in plugin_dir.iterdir():
|
|
92
|
+
if not version_dir.is_dir():
|
|
93
|
+
continue
|
|
94
|
+
skill_dir = version_dir / "skills" / SKILL_NAME
|
|
95
|
+
if not (skill_dir / "SKILL.md").is_file():
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
version_dir_resolved = version_dir.resolve()
|
|
99
|
+
except OSError:
|
|
100
|
+
version_dir_resolved = version_dir
|
|
101
|
+
if active_paths is None:
|
|
102
|
+
is_active = True
|
|
103
|
+
else:
|
|
104
|
+
is_active = version_dir_resolved in active_paths
|
|
105
|
+
out.append(PluginInstall(
|
|
106
|
+
host=host,
|
|
107
|
+
marketplace=marketplace_dir.name,
|
|
108
|
+
version=version_dir.name,
|
|
109
|
+
skill_dir=skill_dir,
|
|
110
|
+
active=is_active,
|
|
111
|
+
))
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detect_plugin_installs() -> list[PluginInstall]:
|
|
116
|
+
"""Discover every host-managed plugin copy of wealthbox-crm on this
|
|
117
|
+
machine. Includes Claude Code user scope, Claude Code project scope
|
|
118
|
+
(cwd-relative), and Codex.
|
|
119
|
+
|
|
120
|
+
Deduped by resolved skill_dir path so a single install isn't reported
|
|
121
|
+
twice when home and cwd point at the same directory (rare in real use,
|
|
122
|
+
common in tests).
|
|
123
|
+
"""
|
|
124
|
+
home = _home()
|
|
125
|
+
cwd = Path.cwd()
|
|
126
|
+
candidates = [
|
|
127
|
+
(home / ".claude" / "plugins", "claude-code-user"),
|
|
128
|
+
(cwd / ".claude" / "plugins", "claude-code-project"),
|
|
129
|
+
(home / ".codex" / "plugins", "codex"),
|
|
130
|
+
]
|
|
131
|
+
installs: list[PluginInstall] = []
|
|
132
|
+
seen: set[Path] = set()
|
|
133
|
+
for plugins_root, host in candidates:
|
|
134
|
+
active_paths = _read_active_install_paths(plugins_root)
|
|
135
|
+
for pi in _scan_plugin_cache(
|
|
136
|
+
plugins_root / "cache", host=host, active_paths=active_paths,
|
|
137
|
+
):
|
|
138
|
+
try:
|
|
139
|
+
key = pi.skill_dir.resolve()
|
|
140
|
+
except OSError:
|
|
141
|
+
key = pi.skill_dir
|
|
142
|
+
if key in seen:
|
|
143
|
+
continue
|
|
144
|
+
seen.add(key)
|
|
145
|
+
installs.append(pi)
|
|
146
|
+
return installs
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def detect_platforms() -> list[Platform]:
|
|
150
|
+
home = _home()
|
|
151
|
+
cwd = Path.cwd()
|
|
152
|
+
return [
|
|
153
|
+
Platform(
|
|
154
|
+
id="claude-code-user",
|
|
155
|
+
label="Claude Code (user)",
|
|
156
|
+
root_dir=home / ".claude" / "skills",
|
|
157
|
+
skill_filename="SKILL.md",
|
|
158
|
+
requires_project_cwd=False,
|
|
159
|
+
),
|
|
160
|
+
Platform(
|
|
161
|
+
id="claude-code-project",
|
|
162
|
+
label="Claude Code (current project)",
|
|
163
|
+
root_dir=cwd / ".claude" / "skills",
|
|
164
|
+
skill_filename="SKILL.md",
|
|
165
|
+
requires_project_cwd=True,
|
|
166
|
+
),
|
|
167
|
+
Platform(
|
|
168
|
+
id="codex",
|
|
169
|
+
label="Codex",
|
|
170
|
+
root_dir=home / ".codex" / "skills",
|
|
171
|
+
skill_filename="SKILL.md",
|
|
172
|
+
requires_project_cwd=False,
|
|
173
|
+
),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def skill_dir(platform: Platform) -> Path:
|
|
178
|
+
return platform.root_dir / "wealthbox-crm"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SkillInstallError(Exception):
|
|
182
|
+
"""Raised when a skill install/uninstall cannot proceed safely."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def is_installed(platform: Platform) -> bool:
|
|
186
|
+
dest = skill_dir(platform)
|
|
187
|
+
return dest.is_dir() and (dest / platform.skill_filename).is_file()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def install_skill(platform: Platform, src: Path, *, force: bool) -> None:
|
|
191
|
+
dest = skill_dir(platform)
|
|
192
|
+
if dest.exists() and not force:
|
|
193
|
+
raise SkillInstallError(
|
|
194
|
+
f"Skill already installed at {dest} (use --force to overwrite)"
|
|
195
|
+
)
|
|
196
|
+
if dest.exists():
|
|
197
|
+
_safe_rmtree(platform, dest)
|
|
198
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
shutil.copytree(src, dest)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def upgrade_skill(platform: Platform, src: Path) -> None:
|
|
203
|
+
"""Re-copy the bundled template over an existing install.
|
|
204
|
+
|
|
205
|
+
Preserves the user's `firm/` directory and root `_meta.json` (the
|
|
206
|
+
bundled template ships neither). Refreshes SKILL.md, references/,
|
|
207
|
+
firm-examples/, and bootstrap.md.
|
|
208
|
+
"""
|
|
209
|
+
dest = skill_dir(platform)
|
|
210
|
+
if not dest.is_dir() or not (dest / platform.skill_filename).is_file():
|
|
211
|
+
raise SkillInstallError(
|
|
212
|
+
f"Cannot upgrade: nothing installed at {dest}"
|
|
213
|
+
)
|
|
214
|
+
shutil.copytree(src, dest, dirs_exist_ok=True)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def uninstall_skill(platform: Platform) -> None:
|
|
218
|
+
dest = skill_dir(platform)
|
|
219
|
+
if not dest.exists():
|
|
220
|
+
return
|
|
221
|
+
_safe_rmtree(platform, dest)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _safe_rmtree(platform: Platform, target: Path) -> None:
|
|
225
|
+
root = platform.root_dir.resolve()
|
|
226
|
+
try:
|
|
227
|
+
resolved = target.resolve(strict=True)
|
|
228
|
+
except FileNotFoundError:
|
|
229
|
+
return
|
|
230
|
+
# Guard: resolved target must sit inside resolved root AND end with "wealthbox-crm".
|
|
231
|
+
if resolved.name != "wealthbox-crm" or root not in resolved.parents:
|
|
232
|
+
raise SkillInstallError(
|
|
233
|
+
f"refusing to remove {resolved}: not a standard skill path under {root}"
|
|
234
|
+
)
|
|
235
|
+
shutil.rmtree(resolved)
|
|
@@ -416,6 +416,32 @@ def build_resource_filter(
|
|
|
416
416
|
return result
|
|
417
417
|
|
|
418
418
|
|
|
419
|
+
async def resolve_category_id(
|
|
420
|
+
client: WealthboxClient, category_type: CategoryType, value: str
|
|
421
|
+
) -> int:
|
|
422
|
+
"""Resolve a category name or numeric string to an integer ID.
|
|
423
|
+
|
|
424
|
+
If `value` is purely digits, returns int(value) without an API call.
|
|
425
|
+
Otherwise fetches all categories of the given type and case-insensitively
|
|
426
|
+
matches `name`. Raises typer.BadParameter on miss with available names.
|
|
427
|
+
"""
|
|
428
|
+
if value.isdigit():
|
|
429
|
+
return int(value)
|
|
430
|
+
data = await client.list_all_categories(category_type)
|
|
431
|
+
items = data.get(category_type.value, [])
|
|
432
|
+
target = value.strip().casefold()
|
|
433
|
+
for item in items:
|
|
434
|
+
if str(item.get("name", "")).casefold() == target:
|
|
435
|
+
return int(item["id"])
|
|
436
|
+
available = sorted(str(i["name"]) for i in items if i.get("name"))
|
|
437
|
+
label = category_type.value.replace("_", " ")
|
|
438
|
+
if available:
|
|
439
|
+
raise typer.BadParameter(
|
|
440
|
+
f"Unknown {label} value '{value}'. Available: {', '.join(available)}"
|
|
441
|
+
)
|
|
442
|
+
raise typer.BadParameter(f"No {label} configured in this workspace.")
|
|
443
|
+
|
|
444
|
+
|
|
419
445
|
def parse_more_fields(more_fields: str, reserved: set[str]) -> dict[str, Any]:
|
|
420
446
|
"""Parse --more-fields JSON and validate against reserved keys.
|
|
421
447
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Comprehensive health check across the wbox install.
|
|
2
|
+
|
|
3
|
+
`wbox doctor` and `wbox skills doctor` both call into `run_doctor()` so
|
|
4
|
+
the legacy `skills doctor` command keeps working as an alias.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import platform as _platform_module
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from importlib.metadata import version as _pkg_version
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from ._config import _config_path, load_config
|
|
18
|
+
from ._skill_bootstrap import migrate_legacy_firm, read_firm_meta, read_meta
|
|
19
|
+
from ._skill_paths import firm_dir, firm_meta_path
|
|
20
|
+
from ._skill_platforms import (
|
|
21
|
+
detect_platforms,
|
|
22
|
+
detect_plugin_installs,
|
|
23
|
+
is_installed,
|
|
24
|
+
skill_dir,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _detect_token_source(token_arg: str | None) -> tuple[str | None, str]:
|
|
29
|
+
"""Return (token, source_label) matching get_client's resolution order:
|
|
30
|
+
--token flag > WEALTHBOX_TOKEN env > config file > .env (loaded as env)."""
|
|
31
|
+
if token_arg:
|
|
32
|
+
return token_arg, "--token flag"
|
|
33
|
+
env_token = os.environ.get("WEALTHBOX_TOKEN")
|
|
34
|
+
if env_token:
|
|
35
|
+
return env_token, "env var (WEALTHBOX_TOKEN or .env)"
|
|
36
|
+
cfg = load_config()
|
|
37
|
+
cfg_token = cfg.get("token")
|
|
38
|
+
if cfg_token:
|
|
39
|
+
return cfg_token, f"config file ({_config_path()})"
|
|
40
|
+
return None, "not set"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_doctor(token: str | None = None) -> int:
|
|
44
|
+
"""Run all health checks. Returns exit code (0 = healthy, 1 = issues found)."""
|
|
45
|
+
issues: list[str] = []
|
|
46
|
+
|
|
47
|
+
migrated_from = _ensure_firm_migrated()
|
|
48
|
+
if migrated_from is not None:
|
|
49
|
+
typer.echo(f"! migrated legacy firm/ data from {migrated_from} to {firm_dir()}")
|
|
50
|
+
typer.echo("")
|
|
51
|
+
|
|
52
|
+
# --- wbox CLI ---------------------------------------------------------
|
|
53
|
+
typer.echo("# wbox CLI")
|
|
54
|
+
cli_version = _pkg_version("wealthbox-cli")
|
|
55
|
+
py_version = ".".join(str(v) for v in sys.version_info[:3])
|
|
56
|
+
binary = shutil.which("wbox") or "(not on PATH -running via python -m)"
|
|
57
|
+
typer.echo(f" version: {cli_version}")
|
|
58
|
+
typer.echo(f" python: {py_version} ({_platform_module.system()})")
|
|
59
|
+
typer.echo(f" binary: {binary}")
|
|
60
|
+
|
|
61
|
+
# --- Authentication ---------------------------------------------------
|
|
62
|
+
typer.echo("\n# Authentication")
|
|
63
|
+
config_path = _config_path()
|
|
64
|
+
if config_path.exists():
|
|
65
|
+
cfg = load_config()
|
|
66
|
+
cfg_token = cfg.get("token", "")
|
|
67
|
+
masked = f"...{cfg_token[-4:]}" if len(cfg_token) > 4 else "****"
|
|
68
|
+
typer.echo(f" config: {config_path} (present, masked: {masked})")
|
|
69
|
+
else:
|
|
70
|
+
typer.echo(f" config: {config_path} (not present)")
|
|
71
|
+
resolved_token, source = _detect_token_source(token)
|
|
72
|
+
typer.echo(f" source: {source}")
|
|
73
|
+
if not resolved_token:
|
|
74
|
+
typer.echo(" smoke: skipped (no token)")
|
|
75
|
+
issues.append("token not configured - run 'wbox config set-token'")
|
|
76
|
+
else:
|
|
77
|
+
from wealthbox_tools.client import WealthboxAPIError
|
|
78
|
+
|
|
79
|
+
from ._util import run_client
|
|
80
|
+
try:
|
|
81
|
+
me = run_client(token, lambda c: c.get_me())
|
|
82
|
+
accounts = me.get("accounts") or []
|
|
83
|
+
firm_name = accounts[0].get("name") if accounts else "(no firm)"
|
|
84
|
+
typer.echo(
|
|
85
|
+
f" smoke: OK /me returned {me.get('name')} "
|
|
86
|
+
f"({firm_name}, user {me.get('id')})"
|
|
87
|
+
)
|
|
88
|
+
except WealthboxAPIError as e:
|
|
89
|
+
typer.echo(f" smoke: FAILED token rejected by /me: {e}")
|
|
90
|
+
issues.append("token rejected by /me - check it's still valid")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
typer.echo(f" smoke: FAILED network/config error: {e}")
|
|
93
|
+
issues.append(f"smoke test failed: {e}")
|
|
94
|
+
|
|
95
|
+
# --- Agent CLIs -------------------------------------------------------
|
|
96
|
+
typer.echo("\n# Agent CLIs on PATH")
|
|
97
|
+
claude_path = shutil.which("claude") or shutil.which("claude.exe")
|
|
98
|
+
codex_path = shutil.which("codex") or shutil.which("codex.exe")
|
|
99
|
+
typer.echo(f" claude: {claude_path or 'not detected'}")
|
|
100
|
+
typer.echo(f" codex: {codex_path or 'not detected'}")
|
|
101
|
+
|
|
102
|
+
# --- Legacy skill installs --------------------------------------------
|
|
103
|
+
typer.echo("\n# Skill installs (legacy template-copy)")
|
|
104
|
+
for p in detect_platforms():
|
|
105
|
+
status = "installed" if is_installed(p) else "not installed"
|
|
106
|
+
meta = read_meta(skill_dir(p)) if is_installed(p) else {}
|
|
107
|
+
template_v = (meta.get("template") or {}).get("cli_version", "-")
|
|
108
|
+
upgrade_hint = ""
|
|
109
|
+
if template_v not in ("-", cli_version):
|
|
110
|
+
upgrade_hint = f" [upgrade: {template_v} -> {cli_version}]"
|
|
111
|
+
issues.append(
|
|
112
|
+
f"{p.id} skill template is {template_v}, latest is {cli_version} "
|
|
113
|
+
f"- run 'wbox skills upgrade'"
|
|
114
|
+
)
|
|
115
|
+
typer.echo(
|
|
116
|
+
f" {p.id:<22} {status:<16} template={template_v:<10} {skill_dir(p)}{upgrade_hint}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# --- Plugin installs --------------------------------------------------
|
|
120
|
+
plugin_installs = detect_plugin_installs()
|
|
121
|
+
typer.echo("\n# Plugin installs (managed by host CLI: claude/codex plugin)")
|
|
122
|
+
if not plugin_installs:
|
|
123
|
+
typer.echo(" (none detected)")
|
|
124
|
+
else:
|
|
125
|
+
# Active first, then cached (descending version) so the live
|
|
126
|
+
# install is the first thing the eye lands on.
|
|
127
|
+
plugin_installs.sort(key=lambda p: (not p.active, p.version), reverse=False)
|
|
128
|
+
for pi in plugin_installs:
|
|
129
|
+
status_label = "active" if pi.active else "cached"
|
|
130
|
+
typer.echo(
|
|
131
|
+
f" {pi.host:<22} plugin@{pi.marketplace:<18} "
|
|
132
|
+
f"version={pi.version:<8} {status_label:<7} {pi.skill_dir}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# --- Firm data --------------------------------------------------------
|
|
136
|
+
typer.echo("\n# Firm data (canonical)")
|
|
137
|
+
typer.echo(f" path: {firm_dir()}")
|
|
138
|
+
typer.echo(f" meta: {firm_meta_path()}")
|
|
139
|
+
firm_meta = read_firm_meta()
|
|
140
|
+
if not firm_meta:
|
|
141
|
+
typer.echo(" state: not bootstrapped")
|
|
142
|
+
issues.append("firm data not bootstrapped - run 'wbox skills bootstrap'")
|
|
143
|
+
else:
|
|
144
|
+
identity = firm_meta.get("identity") or {}
|
|
145
|
+
if firm_meta.get("onboarded_at"):
|
|
146
|
+
firm_state = "onboarded"
|
|
147
|
+
else:
|
|
148
|
+
firm_state = "bootstrapped (qualitative pending)"
|
|
149
|
+
issues.append(
|
|
150
|
+
"firm onboarding incomplete - open the skill in your agent to "
|
|
151
|
+
"walk through bootstrap.md (it'll call 'wbox skills mark-onboarded')"
|
|
152
|
+
)
|
|
153
|
+
typer.echo(f" state: {firm_state}")
|
|
154
|
+
typer.echo(f" firm: {identity.get('name', '-')}")
|
|
155
|
+
typer.echo(f" user: {identity.get('user_name', '-')}")
|
|
156
|
+
if firm_dir().exists():
|
|
157
|
+
md_files = sorted(p.name for p in firm_dir().glob("*.md"))
|
|
158
|
+
generated_set = set((firm_meta.get("files") or {}).keys())
|
|
159
|
+
handed = [f for f in md_files if f not in generated_set and not f.startswith("_")]
|
|
160
|
+
typer.echo(
|
|
161
|
+
f" files: {len(md_files)} markdown ({len(generated_set)} generated, "
|
|
162
|
+
f"{len(handed)} hand-edited)"
|
|
163
|
+
)
|
|
164
|
+
file_ts = list((firm_meta.get("files") or {}).values())
|
|
165
|
+
if file_ts:
|
|
166
|
+
try:
|
|
167
|
+
oldest = min(datetime.fromisoformat(t).date() for t in file_ts)
|
|
168
|
+
typer.echo(f" oldest: {oldest}")
|
|
169
|
+
except (ValueError, TypeError):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# --- Summary ----------------------------------------------------------
|
|
173
|
+
typer.echo("\n# Summary")
|
|
174
|
+
if not issues:
|
|
175
|
+
typer.echo(" All checks passed.")
|
|
176
|
+
return 0
|
|
177
|
+
typer.echo(f" {len(issues)} issue(s) found:")
|
|
178
|
+
for i in issues:
|
|
179
|
+
typer.echo(f" ! {i}")
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _ensure_firm_migrated():
|
|
184
|
+
"""Migrate legacy firm/ data on first read (idempotent)."""
|
|
185
|
+
installed = [skill_dir(p) for p in detect_platforms() if is_installed(p)]
|
|
186
|
+
return migrate_legacy_firm(installed)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def doctor_cmd(
|
|
190
|
+
token: str | None = typer.Option(
|
|
191
|
+
None, "--token",
|
|
192
|
+
help="Override the API token for the smoke test. Overrides env var, config, .env.",
|
|
193
|
+
),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Run the comprehensive doctor.
|
|
196
|
+
|
|
197
|
+
Always exits 0 — issues are surfaced in the Summary section. The doctor
|
|
198
|
+
is informational, not a CI gate. (A future --strict flag could change
|
|
199
|
+
this for scripting.) Token is intentionally NOT bound to
|
|
200
|
+
`envvar=WEALTHBOX_TOKEN` so the doctor can report whether the token came
|
|
201
|
+
from the flag, env var, config file, or .env file.
|
|
202
|
+
"""
|
|
203
|
+
run_doctor(token=token)
|
|
@@ -8,6 +8,7 @@ from .activity import app as activity_app
|
|
|
8
8
|
from .categories import app as categories_app
|
|
9
9
|
from .config import app as config_app
|
|
10
10
|
from .contacts import app as contacts_app
|
|
11
|
+
from .doctor import doctor_cmd as _doctor_cmd
|
|
11
12
|
from .events import app as events_app
|
|
12
13
|
from .households import app as households_app
|
|
13
14
|
from .me import app as me_app
|
|
@@ -35,6 +36,12 @@ def _main(
|
|
|
35
36
|
raise typer.Exit()
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
app.command(
|
|
40
|
+
"doctor",
|
|
41
|
+
help="Comprehensive health check: CLI version, auth, agent CLIs, skills, plugins, firm.",
|
|
42
|
+
)(_doctor_cmd)
|
|
43
|
+
|
|
44
|
+
|
|
38
45
|
app.add_typer(activity_app, name="activity")
|
|
39
46
|
app.add_typer(categories_app, name="categories")
|
|
40
47
|
app.add_typer(config_app, name="config")
|