wealthbox-cli 1.1.1__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/PKG-INFO +4 -1
  2. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/README.md +3 -0
  3. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/pyproject.toml +1 -1
  4. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/_skill_bootstrap.py +45 -15
  5. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/_skill_platforms.py +30 -4
  6. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/skills.py +77 -29
  7. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/SKILL.md +24 -4
  8. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/bootstrap.md +10 -15
  9. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/.env.example +0 -0
  10. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/.github/workflows/ci.yml +0 -0
  11. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/.github/workflows/docs.yml +0 -0
  12. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/.gitignore +0 -0
  13. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/CONTRIBUTING.md +0 -0
  14. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/LICENSE +0 -0
  15. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/SECURITY.md +0 -0
  16. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/docs/changelog.md +0 -0
  17. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/docs/cli-reference.md +0 -0
  18. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/docs/getting-started.md +0 -0
  19. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/docs/index.md +0 -0
  20. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/docs/overrides/main.html +0 -0
  21. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/docs/promotion.md +0 -0
  22. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/mkdocs.yml +0 -0
  23. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/__init__.py +0 -0
  24. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/__init__.py +0 -0
  25. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/_config.py +0 -0
  26. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/_util.py +0 -0
  27. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/activity.py +0 -0
  28. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/categories.py +0 -0
  29. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/config.py +0 -0
  30. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/contacts.py +0 -0
  31. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/events.py +0 -0
  32. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/households.py +0 -0
  33. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/main.py +0 -0
  34. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/me.py +0 -0
  35. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/notes.py +0 -0
  36. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/opportunities.py +0 -0
  37. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/projects.py +0 -0
  38. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/tasks.py +0 -0
  39. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/users.py +0 -0
  40. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/cli/workflows.py +0 -0
  41. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/__init__.py +0 -0
  42. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/activity.py +0 -0
  43. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/base.py +0 -0
  44. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/categories.py +0 -0
  45. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/comments.py +0 -0
  46. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/contacts.py +0 -0
  47. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/events.py +0 -0
  48. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/households.py +0 -0
  49. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/me.py +0 -0
  50. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/notes.py +0 -0
  51. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/opportunities.py +0 -0
  52. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/projects.py +0 -0
  53. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/tasks.py +0 -0
  54. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/users.py +0 -0
  55. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/client/workflows.py +0 -0
  56. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/__init__.py +0 -0
  57. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/activity.py +0 -0
  58. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/comments.py +0 -0
  59. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/common.py +0 -0
  60. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/contacts.py +0 -0
  61. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/custom_fields.py +0 -0
  62. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/enums.py +0 -0
  63. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/events.py +0 -0
  64. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/households.py +0 -0
  65. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/notes.py +0 -0
  66. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/opportunities.py +0 -0
  67. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/projects.py +0 -0
  68. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/tasks.py +0 -0
  69. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/models/workflows.py +0 -0
  70. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/py.typed +0 -0
  71. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/contacts.md +0 -0
  72. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/events.md +0 -0
  73. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/notes.md +0 -0
  74. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/opportunities.md +0 -0
  75. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/projects.md +0 -0
  76. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/tasks.md +0 -0
  77. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/firm-examples/workflows.md +0 -0
  78. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/contacts.md +0 -0
  79. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/events.md +0 -0
  80. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/households.md +0 -0
  81. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/lookups.md +0 -0
  82. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/notes.md +0 -0
  83. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/opportunities.md +0 -0
  84. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/projects.md +0 -0
  85. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/tasks.md +0 -0
  86. {wealthbox_cli-1.1.1 → wealthbox_cli-1.1.2}/src/wealthbox_tools/skills/wealthbox-crm/references/workflows.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wealthbox-cli
3
- Version: 1.1.1
3
+ Version: 1.1.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wealthbox-cli"
7
- version = "1.1.1"
7
+ version = "1.1.2"
8
8
  description = "CLI and client library for the Wealthbox CRM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -86,11 +86,11 @@ def render_users_md(users: list[dict[str, Any]]) -> str:
86
86
  FIRM_README = """# firm/ — Firm Customizations
87
87
 
88
88
  This folder holds everything the agent needs to know about your firm's
89
- Wealthbox conventions.
89
+ Wealthbox conventions. Install metadata lives in `../_meta.json` (skill
90
+ root), not here.
90
91
 
91
92
  ## Generated files (overwritten by `wbox skills refresh`)
92
93
 
93
- - `_meta.json` — refresh timestamps, firm identity, CLI version
94
94
  - `categories.md` — every category type + valid values
95
95
  - `custom-fields.md` — custom fields per document type + valid values
96
96
  - `users.md` — user id → name → email
@@ -139,22 +139,52 @@ def write_stubs(firm_dir: Path) -> list[str]:
139
139
  return written
140
140
 
141
141
 
142
- def write_meta_json(
143
- firm_dir: Path,
142
+ def meta_path(skill_dir: Path) -> Path:
143
+ """Location of _meta.json relative to a skill install root."""
144
+ return skill_dir / "_meta.json"
145
+
146
+
147
+ def read_meta(skill_dir: Path) -> dict[str, Any]:
148
+ """Read _meta.json. Returns {} if missing or unreadable."""
149
+ p = meta_path(skill_dir)
150
+ if not p.exists():
151
+ return {}
152
+ try:
153
+ loaded = json.loads(p.read_text(encoding="utf-8"))
154
+ except (json.JSONDecodeError, OSError):
155
+ return {}
156
+ return loaded if isinstance(loaded, dict) else {}
157
+
158
+
159
+ def write_meta(skill_dir: Path, data: dict[str, Any]) -> None:
160
+ meta_path(skill_dir).write_text(
161
+ json.dumps(data, indent=2) + "\n", encoding="utf-8"
162
+ )
163
+
164
+
165
+ def update_template_meta(skill_dir: Path, *, cli_version: str) -> None:
166
+ """Set the template section of _meta.json. Preserves any existing firm section."""
167
+ meta = read_meta(skill_dir)
168
+ meta["template"] = {"cli_version": cli_version}
169
+ write_meta(skill_dir, meta)
170
+
171
+
172
+ def update_firm_meta(
173
+ skill_dir: Path,
144
174
  *,
145
- generated_files: list[str],
146
- firm_identity: dict[str, Any],
175
+ identity: dict[str, Any],
147
176
  cli_version: str,
177
+ generated_files: list[str],
148
178
  ) -> None:
179
+ """Set the firm section of _meta.json. Preserves any existing template section."""
149
180
  now = datetime.now(timezone.utc).isoformat()
150
- meta = {
151
- "firm": firm_identity,
181
+ meta = read_meta(skill_dir)
182
+ meta["firm"] = {
183
+ "identity": identity,
152
184
  "cli_version": cli_version,
153
185
  "files": {name: now for name in generated_files},
154
186
  }
155
- (firm_dir / "_meta.json").write_text(
156
- json.dumps(meta, indent=2) + "\n", encoding="utf-8"
157
- )
187
+ write_meta(skill_dir, meta)
158
188
 
159
189
 
160
190
  # --------------------------------------------------------------------------- #
@@ -230,11 +260,11 @@ def bootstrap_skill_dir(
230
260
  (firm_dir / "users.md").write_text(render_users_md(users), encoding="utf-8")
231
261
 
232
262
  generated = ["categories.md", "custom-fields.md", "users.md"]
233
- write_meta_json(
234
- firm_dir,
235
- generated_files=generated,
236
- firm_identity=firm_identity,
263
+ update_firm_meta(
264
+ skill_dir,
265
+ identity=firm_identity,
237
266
  cli_version=_pkg_version("wealthbox-cli"),
267
+ generated_files=generated,
238
268
  )
239
269
 
240
270
  wrote_stubs = 0
@@ -69,10 +69,36 @@ def install_skill(platform: Platform, src: Path, *, force: bool) -> None:
69
69
  _safe_rmtree(platform, dest)
70
70
  dest.parent.mkdir(parents=True, exist_ok=True)
71
71
  shutil.copytree(src, dest)
72
- if platform.skill_filename != "SKILL.md":
73
- skill_md = dest / "SKILL.md"
74
- if skill_md.exists():
75
- skill_md.rename(dest / platform.skill_filename)
72
+ _rename_skill_md(platform, dest)
73
+
74
+
75
+ def upgrade_skill(platform: Platform, src: Path) -> None:
76
+ """Re-copy the bundled template over an existing install.
77
+
78
+ Preserves the user's `firm/` directory and root `_meta.json` (the
79
+ bundled template ships neither). Refreshes SKILL.md, references/,
80
+ firm-examples/, and bootstrap.md.
81
+ """
82
+ dest = skill_dir(platform)
83
+ if not dest.is_dir() or not (dest / platform.skill_filename).is_file():
84
+ raise SkillInstallError(
85
+ f"Cannot upgrade: nothing installed at {dest}"
86
+ )
87
+ shutil.copytree(src, dest, dirs_exist_ok=True)
88
+ _rename_skill_md(platform, dest)
89
+
90
+
91
+ def _rename_skill_md(platform: Platform, dest: Path) -> None:
92
+ """Rename SKILL.md → platform.skill_filename if needed (e.g. AGENTS.md for codex)."""
93
+ if platform.skill_filename == "SKILL.md":
94
+ return
95
+ skill_md = dest / "SKILL.md"
96
+ target = dest / platform.skill_filename
97
+ if not skill_md.exists():
98
+ return
99
+ if target.exists():
100
+ target.unlink()
101
+ skill_md.rename(target)
76
102
 
77
103
 
78
104
  def uninstall_skill(platform: Platform) -> None:
@@ -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 read_meta, update_template_meta
11
12
  from ._skill_platforms import (
12
13
  Platform,
13
14
  SkillInstallError,
@@ -16,6 +17,7 @@ from ._skill_platforms import (
16
17
  is_installed,
17
18
  skill_dir,
18
19
  uninstall_skill,
20
+ upgrade_skill,
19
21
  )
20
22
 
21
23
  app = typer.Typer(
@@ -27,31 +29,31 @@ app = typer.Typer(
27
29
 
28
30
  @app.command("list", help="Show where the skill is installed per platform.")
29
31
  def list_platforms() -> None:
30
- header = ("platform", "path", "status", "last-bootstrap")
31
- rows: list[tuple[str, str, str, str]] = []
32
+ header = ("platform", "path", "status", "template-version", "last-bootstrap")
33
+ rows: list[tuple[str, str, str, str, str]] = []
32
34
  for p in detect_platforms():
33
35
  dest = skill_dir(p)
34
36
  installed = is_installed(p)
35
- meta_file = dest / "firm" / "_meta.json"
37
+ template_version = "-"
36
38
  last_bootstrap = "-"
37
- if meta_file.exists():
38
- try:
39
- meta = json.loads(meta_file.read_text())
40
- files = meta.get("files", {})
41
- if files:
42
- last_bootstrap = max(files.values())
43
- except (json.JSONDecodeError, OSError):
44
- last_bootstrap = "(unreadable)"
39
+ if installed:
40
+ meta = read_meta(dest)
41
+ template_version = meta.get("template", {}).get("cli_version", "-")
42
+ firm_section = meta.get("firm") or {}
43
+ firm_files = firm_section.get("files") or {}
44
+ if firm_files:
45
+ last_bootstrap = max(firm_files.values())
45
46
  rows.append((
46
47
  p.id,
47
48
  str(dest),
48
49
  "installed" if installed else "not installed",
50
+ template_version,
49
51
  last_bootstrap,
50
52
  ))
51
53
 
52
54
  widths = [
53
55
  max(len(r[i]) for r in ([header] + rows))
54
- for i in range(4)
56
+ for i in range(len(header))
55
57
  ]
56
58
  fmt = " ".join(f"{{:<{w}}}" for w in widths)
57
59
  typer.echo(fmt.format(*header))
@@ -113,6 +115,7 @@ def install_cmd(
113
115
  except SkillInstallError as e:
114
116
  typer.echo(f"Error installing to {target.id}: {e}", err=True)
115
117
  raise typer.Exit(code=1) from e
118
+ update_template_meta(skill_dir(target), cli_version=_pkg_version("wealthbox-cli"))
116
119
  typer.echo(f"OK installed to {skill_dir(target)}")
117
120
 
118
121
  if not no_bootstrap:
@@ -192,26 +195,65 @@ def refresh_cmd(
192
195
  raise typer.Exit(code=2)
193
196
 
194
197
  for t in targets:
195
- meta_path = skill_dir(t) / "firm" / "_meta.json"
196
- if meta_path.exists():
198
+ meta = read_meta(skill_dir(t))
199
+ firm_files = (meta.get("firm") or {}).get("files") or {}
200
+ if firm_files:
197
201
  try:
198
- meta = json.loads(meta_path.read_text())
199
- ts_values = list(meta.get("files", {}).values())
200
- if ts_values:
201
- oldest = min(datetime.fromisoformat(ts) for ts in ts_values)
202
- age_days = (datetime.now(timezone.utc) - oldest).days
203
- if age_days > staleness_days:
204
- typer.echo(
205
- f"! {t.id}: generated files were {age_days} days stale - refreshing now",
206
- err=True,
207
- )
208
- except (json.JSONDecodeError, OSError, ValueError):
202
+ oldest = min(datetime.fromisoformat(ts) for ts in firm_files.values())
203
+ age_days = (datetime.now(timezone.utc) - oldest).days
204
+ if age_days > staleness_days:
205
+ typer.echo(
206
+ f"! {t.id}: generated files were {age_days} days stale - refreshing now",
207
+ err=True,
208
+ )
209
+ except ValueError:
209
210
  pass
210
211
 
211
212
  bootstrap_skill_dir(skill_dir(t), token=token, generated_only=True)
212
213
  typer.echo(f"OK refreshed {t.id}")
213
214
 
214
215
 
216
+ @app.command(
217
+ "upgrade",
218
+ help="Refresh template files (SKILL.md, references/, firm-examples/, bootstrap.md) "
219
+ "in every installed platform. Preserves firm/ and _meta.json firm section.",
220
+ )
221
+ def upgrade_cmd(
222
+ platforms_flag: list[str] = typer.Option(
223
+ [], "--platform", "-p",
224
+ help="Platform id. Default: every installed platform.",
225
+ ),
226
+ ) -> None:
227
+ if platforms_flag:
228
+ targets = _resolve_platforms(platforms_flag)
229
+ for t in targets:
230
+ if not is_installed(t):
231
+ typer.echo(f"Error: {t.id!r} is not installed.", err=True)
232
+ raise typer.Exit(code=2)
233
+ else:
234
+ targets = [p for p in detect_platforms() if is_installed(p)]
235
+
236
+ if not targets:
237
+ typer.echo(
238
+ "No installed platforms found. Run 'wbox skills install' first.",
239
+ err=True,
240
+ )
241
+ raise typer.Exit(code=2)
242
+
243
+ current = _pkg_version("wealthbox-cli")
244
+
245
+ with as_file(files("wealthbox_tools").joinpath("skills/wealthbox-crm")) as src:
246
+ for t in targets:
247
+ existing = (read_meta(skill_dir(t)).get("template") or {}).get("cli_version", "-")
248
+ try:
249
+ upgrade_skill(t, Path(src))
250
+ except SkillInstallError as e:
251
+ typer.echo(f"Error upgrading {t.id}: {e}", err=True)
252
+ raise typer.Exit(code=1) from e
253
+ update_template_meta(skill_dir(t), cli_version=current)
254
+ typer.echo(f"OK upgraded {t.id}: {existing} -> {current}")
255
+
256
+
215
257
  @app.command("doctor", help="Diagnose install state and token.")
216
258
  def doctor_cmd(
217
259
  token: str | None = typer.Option(None, envvar="WEALTHBOX_TOKEN", hidden=True),
@@ -220,13 +262,19 @@ def doctor_cmd(
220
262
 
221
263
  from ._util import run_client
222
264
 
265
+ current = _pkg_version("wealthbox-cli")
223
266
  typer.echo("# Skill install state")
224
267
  for p in detect_platforms():
225
268
  status = "installed" if is_installed(p) else "not installed"
226
- meta_path = skill_dir(p) / "firm" / "_meta.json"
227
- bootstrapped = "bootstrapped" if meta_path.exists() else "not bootstrapped"
269
+ meta = read_meta(skill_dir(p)) if is_installed(p) else {}
270
+ bootstrapped = "bootstrapped" if meta.get("firm") else "not bootstrapped"
271
+ template_v = (meta.get("template") or {}).get("cli_version", "-")
272
+ upgrade_hint = ""
273
+ if template_v not in ("-", current):
274
+ upgrade_hint = f" [upgrade available: {template_v} -> {current}]"
228
275
  typer.echo(
229
- f" {p.id:<22} {status:<16} {bootstrapped:<18} {skill_dir(p)}"
276
+ f" {p.id:<22} {status:<16} {bootstrapped:<18} "
277
+ f"template={template_v:<10} {skill_dir(p)}{upgrade_hint}"
230
278
  )
231
279
 
232
280
  typer.echo("\n# Token")
@@ -8,14 +8,34 @@ description: Use when the user wants to interact with Wealthbox CRM — creating
8
8
  Execute Wealthbox CRM operations via the `wbox` CLI. Supports contacts, tasks, notes, events,
9
9
  opportunities, projects, workflows, households, and read-only lookups (categories, users, activity, me).
10
10
 
11
- ## First Run (self-delete after completion)
11
+ ## First Run
12
12
 
13
- If `firm/_meta.json` does not exist in this skill's directory, this is the
14
- first invocation on this machine. Before handling the user's request:
13
+ If `_meta.json` in this skill's directory does **not** contain a `firm`
14
+ section, the firm has not been onboarded yet on this machine. Before
15
+ handling the user's request:
15
16
 
16
17
  1. Read `bootstrap.md` in this skill's directory.
17
18
  2. Follow it end-to-end.
18
- 3. When done, this section and `bootstrap.md` will have been deleted.
19
+ 3. The bootstrap will populate `_meta.json.firm` so this check passes
20
+ on subsequent runs.
21
+
22
+ ## Staleness Check
23
+
24
+ `_meta.json.template.cli_version` records which `wbox` version installed
25
+ the template files. Compare it to the running CLI version:
26
+
27
+ ```bash
28
+ wbox --version
29
+ ```
30
+
31
+ If the template version is older, suggest the user run:
32
+
33
+ ```bash
34
+ wbox skills upgrade
35
+ ```
36
+
37
+ This refreshes SKILL.md, references/, firm-examples/, and bootstrap.md
38
+ across every installed platform while preserving `firm/`.
19
39
 
20
40
  ## Prerequisites
21
41
 
@@ -1,8 +1,7 @@
1
1
  # First-Run Bootstrap
2
2
 
3
- You were told to read this because `firm/_meta.json` does not exist in the skill
4
- directory. Follow every step below. When you finish, delete this file and trim
5
- the `## First Run` section out of `SKILL.md`.
3
+ You were told to read this because `_meta.json` in the skill root does not
4
+ contain a `firm` section. Follow every step below.
6
5
 
7
6
  ## Step 1 — Fetch API-derived firm data
8
7
 
@@ -17,12 +16,13 @@ It populates these files per installed platform:
17
16
  - `firm/categories.md` — every category type + valid values
18
17
  - `firm/custom-fields.md` — custom fields per document type + valid values
19
18
  - `firm/users.md` — user id → name → email
20
- - `firm/_meta.json` — refresh timestamps + firm identity
19
+ - `_meta.json` (skill root) — adds the `firm` section: identity, refresh
20
+ timestamps, and the CLI version that ran the bootstrap
21
21
  - Stub files: `firm/contacts.md`, `firm/tasks.md`, `firm/notes.md`,
22
22
  `firm/events.md`, `firm/opportunities.md`, `firm/projects.md`,
23
23
  `firm/workflows.md` — one-line TODOs you will fill in next.
24
24
 
25
- Verify `firm/_meta.json` now exists before continuing.
25
+ Verify the `firm` section now exists in `_meta.json` before continuing.
26
26
 
27
27
  ## Step 2 — Gather firm policy (per-resource)
28
28
 
@@ -90,16 +90,11 @@ Ask the advisor: "Is there any firm convention, custom process, or preference
90
90
  I haven't asked about that I should follow?" Append freeform answers to
91
91
  `firm/workflows.md` under a `## Other Conventions` heading.
92
92
 
93
- ## Step 4 — Self-trim
94
-
95
- Now that all firm files are filled in:
96
-
97
- 1. Delete `bootstrap.md` (this file).
98
- 2. Open `SKILL.md`. Find the `## First Run (self-delete after completion)`
99
- section. Delete that heading and everything under it through (but not
100
- including) the next `## ` heading.
101
-
102
- ## Step 5 — Confirm and continue
93
+ ## Step 4 — Confirm and continue
103
94
 
104
95
  Tell the advisor: "First-run setup complete — `firm/` populated for <firm
105
96
  name>." Then return to the user's original request.
97
+
98
+ You don't need to delete this file or trim `SKILL.md`. The first-run check
99
+ keys on `_meta.json.firm`, which now exists, so the bootstrap path will be
100
+ skipped on every subsequent invocation automatically.
File without changes
File without changes
File without changes
File without changes