deepcell-cli 0.6.1__tar.gz → 0.6.3__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 (109) hide show
  1. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/PKG-INFO +9 -7
  2. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/README.md +8 -6
  3. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/pyproject.toml +5 -7
  4. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/__init__.py +1 -1
  5. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/capabilities.py +21 -5
  6. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/capability-contract.json +18 -18
  7. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/client.py +10 -1
  8. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/help_cmd.py +23 -0
  9. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/sync.py +6 -62
  10. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/version.py +73 -2
  11. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/config.py +1 -1
  12. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/main.py +25 -4
  13. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/mcp_server.py +8 -6
  14. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/oauth_provider.py +12 -4
  15. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/stages.py +4 -1
  16. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/surface.py +17 -6
  17. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/upgrade_check.py +1 -1
  18. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli.egg-info/PKG-INFO +9 -7
  19. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_config.py +1 -1
  20. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_mcp_auth.py +11 -11
  21. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_mcp_server.py +32 -3
  22. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_stages.py +25 -6
  23. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_upgrade_check.py +10 -10
  24. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/setup.cfg +0 -0
  25. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/__main__.py +0 -0
  26. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/_findings.py +0 -0
  27. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/__init__.py +0 -0
  28. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/_batch_input.py +0 -0
  29. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/_datatypes.py +0 -0
  30. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/_negative_args.py +0 -0
  31. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/_swapped_args.py +0 -0
  32. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/_version_display.py +0 -0
  33. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/_write_opts.py +0 -0
  34. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/account.py +0 -0
  35. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/auth.py +0 -0
  36. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/changes.py +0 -0
  37. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/deck.py +0 -0
  38. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/defs.py +0 -0
  39. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/describe.py +0 -0
  40. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/doc.py +0 -0
  41. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/doctor.py +0 -0
  42. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/download.py +0 -0
  43. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/edit.py +0 -0
  44. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/example.py +0 -0
  45. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/export.py +0 -0
  46. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/export_docx.py +0 -0
  47. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/export_pdf.py +0 -0
  48. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/export_pptx.py +0 -0
  49. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/files.py +0 -0
  50. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/grep.py +0 -0
  51. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/guide.py +0 -0
  52. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/impact.py +0 -0
  53. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/import_cmd.py +0 -0
  54. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/ingest.py +0 -0
  55. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/merge.py +0 -0
  56. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/query.py +0 -0
  57. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/reasoning.py +0 -0
  58. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/ref.py +0 -0
  59. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/replace.py +0 -0
  60. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/rules.py +0 -0
  61. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/share.py +0 -0
  62. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/upgrade.py +0 -0
  63. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/variant.py +0 -0
  64. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/viewer.py +0 -0
  65. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/commands/workspace.py +0 -0
  66. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/context.py +0 -0
  67. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/errors.py +0 -0
  68. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/logging_setup.py +0 -0
  69. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/output.py +0 -0
  70. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/revision.py +0 -0
  71. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/sync_state.py +0 -0
  72. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli/xml_replace.py +0 -0
  73. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli.egg-info/SOURCES.txt +0 -0
  74. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli.egg-info/dependency_links.txt +0 -0
  75. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli.egg-info/entry_points.txt +0 -0
  76. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli.egg-info/requires.txt +0 -0
  77. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/src/deepcell_cli.egg-info/top_level.txt +0 -0
  78. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_anon.py +0 -0
  79. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_claim_strength.py +0 -0
  80. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_client.py +0 -0
  81. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_client_surface_header.py +0 -0
  82. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_conflict_reason.py +0 -0
  83. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_deck_help_contract.py +0 -0
  84. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_deck_op_shape.py +0 -0
  85. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_docs_catalog.py +0 -0
  86. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_doctor.py +0 -0
  87. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_download.py +0 -0
  88. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_findings_provenance.py +0 -0
  89. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_guide_eval_a4_a7.py +0 -0
  90. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_guide_flag_drift.py +0 -0
  91. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_help_flag_drift.py +0 -0
  92. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_impact_commands.py +0 -0
  93. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_ingest_command.py +0 -0
  94. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_logging_setup.py +0 -0
  95. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_op_reachability.py +0 -0
  96. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_openapi_route_parity.py +0 -0
  97. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_output_export_notes.py +0 -0
  98. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_output_keeps_the_answer.py +0 -0
  99. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_reasoning_commands.py +0 -0
  100. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_reasoning_diff_command.py +0 -0
  101. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_ref_flag_drift.py +0 -0
  102. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_round6_signal_regressions.py +0 -0
  103. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_search_reach.py +0 -0
  104. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_shed_retry.py +0 -0
  105. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_surface_examples.py +0 -0
  106. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_surface_walker.py +0 -0
  107. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_version_parity.py +0 -0
  108. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_write_message_rule.py +0 -0
  109. {deepcell_cli-0.6.1 → deepcell_cli-0.6.3}/tests/test_xml_replace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepcell-cli
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them
5
5
  Author-email: DeepCell <hello@deepcell.net>
6
6
  License-Expression: MIT
@@ -36,12 +36,12 @@ Command-line interface for **DeepCell** — a versionable `.deepcell` document t
36
36
 
37
37
  ```bash
38
38
  # macOS / Linux
39
- curl -LsSf https://beta.deepcell.net/install.sh | sh
39
+ curl -LsSf https://deepcell.net/install.sh | sh
40
40
  ```
41
41
 
42
42
  ```powershell
43
43
  # Windows (PowerShell)
44
- irm https://beta.deepcell.net/install.ps1 | iex
44
+ irm https://deepcell.net/install.ps1 | iex
45
45
  ```
46
46
 
47
47
  ```bash
@@ -166,8 +166,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
166
166
  | Document surface | `doc` — outline, blocks, body, links, lint |
167
167
  | Deck surface | `deck` — slides, ordering, binding, branding |
168
168
  | Change review | `changes list / diff / revert`, `impact` |
169
- | Version control | `log`, `diff`, `restore` |
170
- | Sync | `clone`, `status`, `pull`, `push`, `commit`, `merge` |
169
+ | Version control | `log`, `diff`, `restore`, `commit` |
170
+ | Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
171
171
  | Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
172
172
  | Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
173
173
  | Delivery | `viewer`, `share create / list / revoke` |
@@ -218,7 +218,9 @@ deepcell push -m "Updated assumptions" # upload
218
218
  deepcell pull # fetch cloud changes
219
219
  ```
220
220
 
221
- A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell guide revise/history`.
221
+ A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
222
+
223
+ Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
222
224
 
223
225
  ## Import & export
224
226
 
@@ -249,7 +251,7 @@ Interactive and sync commands (`login`, `clone`, `push`, …) are blocked inside
249
251
 
250
252
  | Variable | Purpose | Default |
251
253
  |----------|---------|---------|
252
- | `DEEPCELL_API_URL` | API endpoint | `https://beta.deepcell.net/api/jingwei` |
254
+ | `DEEPCELL_API_URL` | API endpoint | `https://deepcell.net/api/jingwei` |
253
255
  | `DEEPCELL_PROJECT` | Override active project | _(none)_ |
254
256
  | `DEEPCELL_ACCESS_TOKEN` | Auth token for CI/headless use | _(none)_ |
255
257
  | `DEEPCELL_CONFIG` | Alternate `config.json` path (session-scoped) | `~/.deepcell/config.json` |
@@ -4,12 +4,12 @@ Command-line interface for **DeepCell** — a versionable `.deepcell` document t
4
4
 
5
5
  ```bash
6
6
  # macOS / Linux
7
- curl -LsSf https://beta.deepcell.net/install.sh | sh
7
+ curl -LsSf https://deepcell.net/install.sh | sh
8
8
  ```
9
9
 
10
10
  ```powershell
11
11
  # Windows (PowerShell)
12
- irm https://beta.deepcell.net/install.ps1 | iex
12
+ irm https://deepcell.net/install.ps1 | iex
13
13
  ```
14
14
 
15
15
  ```bash
@@ -134,8 +134,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
134
134
  | Document surface | `doc` — outline, blocks, body, links, lint |
135
135
  | Deck surface | `deck` — slides, ordering, binding, branding |
136
136
  | Change review | `changes list / diff / revert`, `impact` |
137
- | Version control | `log`, `diff`, `restore` |
138
- | Sync | `clone`, `status`, `pull`, `push`, `commit`, `merge` |
137
+ | Version control | `log`, `diff`, `restore`, `commit` |
138
+ | Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
139
139
  | Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
140
140
  | Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
141
141
  | Delivery | `viewer`, `share create / list / revoke` |
@@ -186,7 +186,9 @@ deepcell push -m "Updated assumptions" # upload
186
186
  deepcell pull # fetch cloud changes
187
187
  ```
188
188
 
189
- A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell guide revise/history`.
189
+ A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
190
+
191
+ Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
190
192
 
191
193
  ## Import & export
192
194
 
@@ -217,7 +219,7 @@ Interactive and sync commands (`login`, `clone`, `push`, …) are blocked inside
217
219
 
218
220
  | Variable | Purpose | Default |
219
221
  |----------|---------|---------|
220
- | `DEEPCELL_API_URL` | API endpoint | `https://beta.deepcell.net/api/jingwei` |
222
+ | `DEEPCELL_API_URL` | API endpoint | `https://deepcell.net/api/jingwei` |
221
223
  | `DEEPCELL_PROJECT` | Override active project | _(none)_ |
222
224
  | `DEEPCELL_ACCESS_TOKEN` | Auth token for CI/headless use | _(none)_ |
223
225
  | `DEEPCELL_CONFIG` | Alternate `config.json` path (session-scoped) | `~/.deepcell/config.json` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepcell-cli"
3
- version = "0.6.1"
3
+ version = "0.6.3"
4
4
  description = "Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -42,12 +42,10 @@ deepcell-mcp = "deepcell_cli.mcp_server:main"
42
42
  # deliberately no Repository or Issues entry: the CLI is developed in a private
43
43
  # repository, and a link to one nobody can open is worse than no link.
44
44
  #
45
- # deepcell.net is the canonical host the site is moving to shortly; it does not
46
- # resolve yet, so these two links go live when the domain does. That is a
47
- # deliberate trade for not stamping the outgoing beta host into the metadata of
48
- # a release that cannot be re-uploaded. The installer URLs are NOT part of it —
49
- # they stay on beta.deepcell.net, because a curl that 404s is a broken install
50
- # today rather than a link that is briefly grey.
45
+ # deepcell.net is the canonical origin and serves the site. beta.deepcell.net
46
+ # answers the same deployment and is deliberately left un-redirected, because
47
+ # every CLI already installed has it baked in as its API default — but it is
48
+ # not what anything new should hand out.
51
49
  [project.urls]
52
50
  Homepage = "https://deepcell.net"
53
51
  Documentation = "https://deepcell.net/product/cli"
@@ -9,4 +9,4 @@ same thing, and a third is stamped into ``docs/cli-surface.json`` by
9
9
  reports a version it is not, and skipping the regen lands a red build.
10
10
  """
11
11
 
12
- __version__ = "0.6.1"
12
+ __version__ = "0.6.3"
@@ -33,6 +33,7 @@ MCP_ALLOWED_COMMANDS = frozenset(
33
33
  "cell-meta",
34
34
  "changes",
35
35
  "claim",
36
+ "commit",
36
37
  "deck",
37
38
  "defs",
38
39
  "describe",
@@ -61,13 +62,23 @@ MCP_ALLOWED_COMMANDS = frozenset(
61
62
  "rm",
62
63
  "rules",
63
64
  "share",
64
- "variant",
65
65
  "viewer",
66
66
  "whoami",
67
+ # `workspace` is the pre-rename spelling of `project` — a hidden alias
68
+ # of the same Group. Hidden is not unreachable: `_parse_command`
69
+ # dispatches through Click, which runs a hidden command like any other,
70
+ # so the alias needs a policy of its own or it is reachable with none
71
+ # declared. It carries `project`'s policy because it IS `project`.
72
+ "workspace",
67
73
  "write",
68
74
  }
69
75
  )
70
76
 
77
+ #: Allowed spellings that are aliases of another allowed command. They need an
78
+ #: MCP policy (they run) but must NOT reach the tool catalog: documenting a
79
+ #: second spelling of `project` would put back the row the alias is hidden from.
80
+ MCP_ALIAS_COMMANDS = frozenset({"workspace"})
81
+
71
82
  # Reason codes are stable data rendered into help, guide, MCP errors, and the
72
83
  # for-agent page. The detail is for maintainers and structured clients; user
73
84
  # prose remains translated at its own surface.
@@ -80,10 +91,6 @@ MCP_BLOCKED_COMMANDS: dict[str, dict[str, str]] = {
80
91
  "reason": "local_checkout",
81
92
  "detail": "creates and updates a local Git working copy",
82
93
  },
83
- "commit": {
84
- "reason": "local_checkout",
85
- "detail": "reads local sync state and commits a working copy",
86
- },
87
94
  "login": {
88
95
  "reason": "interactive_auth",
89
96
  "detail": "opens or polls an interactive browser authentication flow",
@@ -132,6 +139,15 @@ MCP_BLOCKED_COMMANDS: dict[str, dict[str, str]] = {
132
139
  "reason": "shared_host_install",
133
140
  "detail": "would inspect or replace the CLI installed on the MCP host",
134
141
  },
142
+ "variant": {
143
+ "reason": "local_checkout",
144
+ "detail": (
145
+ "branches the whole file set, and only a working copy can use one: "
146
+ "`checkout` resets a local tree, and no write command takes a "
147
+ "`--variant`, so a caller with no clone cannot put anything on a "
148
+ "branch for `diff` or `merge` to act on"
149
+ ),
150
+ },
135
151
  "verify-email": {
136
152
  "reason": "interactive_auth",
137
153
  "detail": "prompts for a verification token sent out of band",
@@ -427,9 +427,9 @@
427
427
  "reason": "not_dispatched"
428
428
  },
429
429
  "mcp": {
430
- "available": false,
431
- "detail": "reads local sync state and commits a working copy",
432
- "reason": "local_checkout"
430
+ "available": true,
431
+ "detail": null,
432
+ "reason": null
433
433
  }
434
434
  }
435
435
  },
@@ -5817,9 +5817,9 @@
5817
5817
  "reason": "not_dispatched"
5818
5818
  },
5819
5819
  "mcp": {
5820
- "available": true,
5821
- "detail": null,
5822
- "reason": null
5820
+ "available": false,
5821
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5822
+ "reason": "local_checkout"
5823
5823
  }
5824
5824
  }
5825
5825
  },
@@ -5848,9 +5848,9 @@
5848
5848
  "reason": "not_dispatched"
5849
5849
  },
5850
5850
  "mcp": {
5851
- "available": true,
5852
- "detail": null,
5853
- "reason": null
5851
+ "available": false,
5852
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5853
+ "reason": "local_checkout"
5854
5854
  }
5855
5855
  }
5856
5856
  },
@@ -5879,9 +5879,9 @@
5879
5879
  "reason": "not_dispatched"
5880
5880
  },
5881
5881
  "mcp": {
5882
- "available": true,
5883
- "detail": null,
5884
- "reason": null
5882
+ "available": false,
5883
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5884
+ "reason": "local_checkout"
5885
5885
  }
5886
5886
  }
5887
5887
  },
@@ -5910,9 +5910,9 @@
5910
5910
  "reason": "not_dispatched"
5911
5911
  },
5912
5912
  "mcp": {
5913
- "available": true,
5914
- "detail": null,
5915
- "reason": null
5913
+ "available": false,
5914
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5915
+ "reason": "local_checkout"
5916
5916
  }
5917
5917
  }
5918
5918
  },
@@ -5941,9 +5941,9 @@
5941
5941
  "reason": "not_dispatched"
5942
5942
  },
5943
5943
  "mcp": {
5944
- "available": true,
5945
- "detail": null,
5946
- "reason": null
5944
+ "available": false,
5945
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5946
+ "reason": "local_checkout"
5947
5947
  }
5948
5948
  }
5949
5949
  },
@@ -111,7 +111,16 @@ class DeepCellClient:
111
111
  # which build their own per-request headers and would otherwise be the
112
112
  # two calls a new user makes before anything else. httpx merges these
113
113
  # with per-request headers rather than replacing them.
114
- self._http = httpx.Client(timeout=_TIMEOUT, headers=client_headers())
114
+ # `follow_redirects` because httpx defaults it to False, and an installed
115
+ # CLI outlives the hostname it was built against: every copy already in
116
+ # the wild has `https://beta.deepcell.net/api/jingwei` baked in as its
117
+ # default. The day that host answers a 301 instead of serving, a client
118
+ # that does not follow redirects reports the 3xx as a hard failure and
119
+ # every one of those installs breaks at once — which is why beta is kept
120
+ # serving rather than redirected. This makes the next move survivable.
121
+ self._http = httpx.Client(
122
+ timeout=_TIMEOUT, headers=client_headers(), follow_redirects=True
123
+ )
115
124
 
116
125
  # ── Helpers ─────────────────────────────────────────────
117
126
 
@@ -257,6 +257,19 @@ def help_cmd(
257
257
  if command:
258
258
  path = " ".join(command)
259
259
  entry = commands.get(path)
260
+ if entry is None:
261
+ # A HIDDEN command is absent from the listing on purpose but still
262
+ # runs, so asking for it by name must answer. `clone` is the case:
263
+ # `cli/README.md` points a person at `deepcell help clone`, and the
264
+ # listing it is hidden from is not the same question as the flags it
265
+ # takes. Only reached on an explicit name — the index, the stage
266
+ # filters and every generated surface still see the visible set.
267
+ hidden_entry = build_commands(include_hidden=True).get(path)
268
+ if hidden_entry is not None:
269
+ entry = {
270
+ **hidden_entry,
271
+ "capability": get_command_capability(path),
272
+ }
260
273
  if entry is None:
261
274
  # `build_commands()` keys leaf commands only, so a group path misses
262
275
  # the table. Asking what is under `defs` is a well-formed question
@@ -267,6 +280,16 @@ def help_cmd(
267
280
  children = sorted(
268
281
  name for name in commands if name.startswith(f"{path} ")
269
282
  )
283
+ if not children:
284
+ # A hidden GROUP is keyed nowhere — `build_commands` keys leaves
285
+ # — so `deepcell help variant` has to find its children the same
286
+ # way, in the hidden-inclusive tree.
287
+ all_commands = build_commands(include_hidden=True)
288
+ children = sorted(
289
+ name for name in all_commands if name.startswith(f"{path} ")
290
+ )
291
+ if children:
292
+ commands = {**all_commands, **commands}
270
293
  if children:
271
294
  if ctx.fmt == "json":
272
295
  output({"group": path, "commands": children}, ctx.fmt)
@@ -1,6 +1,10 @@
1
- """Sync commands: clone, pull, push, status, commit.
1
+ """Sync commands: clone, pull, push, status.
2
2
 
3
- Git-like sync between a local folder and a cloud DeepCell workspace.
3
+ Git-like sync between a local folder and a cloud DeepCell workspace. Every
4
+ command here genuinely reads or writes a local working copy — that is the
5
+ membership rule, and `commit` failed it: it posts to
6
+ `/workspaces/{slug}/versions/commit` and touches no local file, so it lives in
7
+ `version.py` with the rest of the server-side history commands.
4
8
  """
5
9
 
6
10
  from __future__ import annotations
@@ -742,63 +746,3 @@ def push(ctx: Ctx, message: str) -> None:
742
746
  "push again"
743
747
  )
744
748
  raise click.exceptions.Exit(1)
745
-
746
-
747
- # -- commit (server-side pending) ---------------------------------------------
748
-
749
-
750
- @click.command()
751
- @click.option("-m", "--message", default=None, help="Commit message.")
752
- @pass_ctx
753
- def commit(ctx: Ctx, message: str | None) -> None:
754
- """Commit changes staged on the server by something other than you.
755
-
756
- You almost certainly do not need this. `defs`, `edit`, `write` and
757
- `reasoning` each commit their own change with their own `-m` — there is no
758
- staging area they queue into. This commits what a *different* actor left
759
- pending on the server, such as files created by an agent session.
760
-
761
- Nothing staged is a no-op, not an error.
762
- """
763
- root, state = _require_sync_root(ctx)
764
- slug = state.workspace_slug
765
-
766
- # `-m` is no longer `required=True`, so this reaches the server even when
767
- # the caller gave no message. The server checks "is anything staged"
768
- # first and answers the no-op; a commit that will actually write still
769
- # 422s without a message, and that is re-raised below as a usage error.
770
- #
771
- # It used to be rejected by Click during parsing, so a worker whose defs
772
- # ops had each already committed and who reasonably tried to finalize got
773
- # "Missing option '-m' / '--message'" — an argument-syntax error for a
774
- # command with nothing to do. That is what the 2026-08-02 CLI eval hit.
775
- data = ctx.client.post(
776
- f"/workspaces/{slug}/versions/commit",
777
- json={"message": message or ""},
778
- )
779
-
780
- # A commit with nothing staged is a benign no-op (the endpoint answers
781
- # 200 with committed=false, sha=null) — report it instead of slicing a
782
- # null sha.
783
- if isinstance(data, dict) and data.get("committed") is False:
784
- echo_info(
785
- "Nothing to commit — no pending changes. `defs`, `edit`, `write` "
786
- "and `reasoning` each commit their own change, so there is "
787
- "normally nothing here to finalize."
788
- )
789
- return
790
-
791
- sha = (data.get("sha") or "")[:8] if isinstance(data, dict) else ""
792
- # Staged content is committed either way (the server persists invalid
793
- # .deepcell on purpose), so report the errors rather than letting a clean
794
- # "Committed" line stand in for them — parity with write/push.
795
- invalid = echo_validation_map(
796
- data.get("validation") if isinstance(data, dict) else None
797
- )
798
- if invalid:
799
- echo_warning(
800
- f"Committed ({sha}), but {len(invalid)} file(s) have validation "
801
- f"error(s): {', '.join(invalid)} — fix the errors above"
802
- )
803
- raise click.exceptions.Exit(1)
804
- echo_success(f"Committed ({sha}): {message}")
@@ -1,4 +1,4 @@
1
- """Version control commands: log, diff, restore."""
1
+ """Version control commands: log, diff, restore, commit."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -7,7 +7,8 @@ import click
7
7
  from deepcell_cli.errors import APIError
8
8
  from deepcell_cli.context import Ctx, pass_ctx
9
9
  from deepcell_cli.output import (
10
- echo_error, echo_info, echo_success, echo_warning, output, output_mutation, print_plain,
10
+ echo_error, echo_info, echo_success, echo_validation_map, echo_warning,
11
+ output, output_mutation, print_plain,
11
12
  )
12
13
 
13
14
 
@@ -443,3 +444,73 @@ def restore(ctx: Ctx, revision: str, filename: str | None, yes: bool) -> None:
443
444
  "Recover them from a newer revision (`deepcell log`, then "
444
445
  "`deepcell download <file> --revision <sha>`)."
445
446
  )
447
+
448
+
449
+ # -- commit (finalize what another actor staged server-side) -------------------
450
+
451
+
452
+ @click.command()
453
+ @click.option("-m", "--message", default=None, help="Commit message.")
454
+ @pass_ctx
455
+ def commit(ctx: Ctx, message: str | None) -> None:
456
+ """Commit changes staged on the server by something other than you.
457
+
458
+ You almost certainly do not need this. `defs`, `edit`, `write` and
459
+ `reasoning` each commit their own change with their own `-m` — there is no
460
+ staging area they queue into. This commits what a *different* actor left
461
+ pending on the server, such as files created by an agent session.
462
+
463
+ Nothing staged is a no-op, not an error.
464
+ """
465
+ # Addressed by workspace, like everything else in this module. It used to
466
+ # live in `sync.py` and call `_require_sync_root`, which reads
467
+ # `.deepcell/sync.json` — for nothing but the slug, because the work is
468
+ # entirely server-side: `POST /workspaces/{slug}/versions/commit` stages
469
+ # nothing locally and reads no local file.
470
+ #
471
+ # That invented clone requirement cost the command the callers it exists
472
+ # for. It raised "Not inside a synced workspace" for anyone standing
473
+ # anywhere else, and it was blocked over MCP as `local_checkout` — a reason
474
+ # untrue of it. An agent, which is precisely who leaves changes staged,
475
+ # could not finalize them from any transport.
476
+ slug = ctx.require_workspace()
477
+
478
+ # `-m` is no longer `required=True`, so this reaches the server even when
479
+ # the caller gave no message. The server checks "is anything staged"
480
+ # first and answers the no-op; a commit that will actually write still
481
+ # 422s without a message, and that is re-raised below as a usage error.
482
+ #
483
+ # It used to be rejected by Click during parsing, so a worker whose defs
484
+ # ops had each already committed and who reasonably tried to finalize got
485
+ # "Missing option '-m' / '--message'" — an argument-syntax error for a
486
+ # command with nothing to do. That is what the 2026-08-02 CLI eval hit.
487
+ data = ctx.client.post(
488
+ f"/workspaces/{slug}/versions/commit",
489
+ json={"message": message or ""},
490
+ )
491
+
492
+ # A commit with nothing staged is a benign no-op (the endpoint answers
493
+ # 200 with committed=false, sha=null) — report it instead of slicing a
494
+ # null sha.
495
+ if isinstance(data, dict) and data.get("committed") is False:
496
+ echo_info(
497
+ "Nothing to commit — no pending changes. `defs`, `edit`, `write` "
498
+ "and `reasoning` each commit their own change, so there is "
499
+ "normally nothing here to finalize."
500
+ )
501
+ return
502
+
503
+ sha = (data.get("sha") or "")[:8] if isinstance(data, dict) else ""
504
+ # Staged content is committed either way (the server persists invalid
505
+ # .deepcell on purpose), so report the errors rather than letting a clean
506
+ # "Committed" line stand in for them — parity with write/push.
507
+ invalid = echo_validation_map(
508
+ data.get("validation") if isinstance(data, dict) else None
509
+ )
510
+ if invalid:
511
+ echo_warning(
512
+ f"Committed ({sha}), but {len(invalid)} file(s) have validation "
513
+ f"error(s): {', '.join(invalid)} — fix the errors above"
514
+ )
515
+ raise click.exceptions.Exit(1)
516
+ echo_success(f"Committed ({sha}): {message}")
@@ -19,7 +19,7 @@ _CREDS_FILE = _DIR / "credentials.json"
19
19
 
20
20
  # ── Defaults ────────────────────────────────────────────────
21
21
 
22
- DEFAULT_API_URL = "https://beta.deepcell.net/api/jingwei"
22
+ DEFAULT_API_URL = "https://deepcell.net/api/jingwei"
23
23
  DEFAULT_FORMAT = "plain"
24
24
 
25
25
  # ── Environment overrides ───────────────────────────────────
@@ -442,11 +442,11 @@ from deepcell_cli.commands.impact import impact # noqa: E402
442
442
  from deepcell_cli.commands.export_docx import to_docx # noqa: E402
443
443
  from deepcell_cli.commands.export_pdf import to_pdf # noqa: E402
444
444
  from deepcell_cli.commands.export_pptx import to_pptx # noqa: E402
445
- from deepcell_cli.commands.version import log, diff, restore # noqa: E402
445
+ from deepcell_cli.commands.version import log, diff, restore, commit # noqa: E402
446
446
  from deepcell_cli.commands.changes import changes # noqa: E402
447
447
  from deepcell_cli.commands.variant import variant # noqa: E402
448
448
  from deepcell_cli.commands.grep import grep # noqa: E402
449
- from deepcell_cli.commands.sync import clone, pull, push, sync_status, commit # noqa: E402
449
+ from deepcell_cli.commands.sync import clone, pull, push, sync_status # noqa: E402
450
450
  from deepcell_cli.commands.merge import merge # noqa: E402
451
451
  from deepcell_cli.commands.import_cmd import import_cmd # noqa: E402
452
452
  from deepcell_cli.commands.ingest import ingest # noqa: E402
@@ -499,13 +499,34 @@ cli.add_command(log)
499
499
  cli.add_command(diff)
500
500
  cli.add_command(restore)
501
501
  cli.add_command(changes)
502
- cli.add_command(variant)
503
502
  cli.add_command(grep)
503
+ cli.add_command(commit)
504
+
505
+ # Local sync and variants: registered, working, and HIDDEN. Both are a person's
506
+ # workflow at a terminal with a clone, and neither is discoverable any more —
507
+ # `surface.py`, `main`'s own help and `docs/cli-commands.md`'s coverage test all
508
+ # skip hidden commands, so they produce no help row and no entry in any
509
+ # generated surface. Muscle memory and existing scripts keep working, and
510
+ # `deepcell help <name>` still answers for each.
511
+ #
512
+ # Sync moves BYTES while every read addresses the project on the server, so a
513
+ # clone is a working copy the read path cannot see. Variants branch the whole
514
+ # file set, and no write command takes a variant, so a branch is only reachable
515
+ # by checking it out onto disk. Both are therefore clone-shaped all the way
516
+ # down, which is precisely what the agent surfaces do not have.
517
+ #
518
+ # The cost is deliberate and was weighed: the `variants` and `merge` domains
519
+ # leave `docs/backend-cli-semantic-contracts.yaml`, because a hidden command is
520
+ # absent from the capability contract the domain resolves through. Those two
521
+ # backend domains now have no taught CLI command.
522
+ for _person_only in (variant, clone, pull, push, sync_status, merge):
523
+ _person_only.hidden = True
524
+
525
+ cli.add_command(variant)
504
526
  cli.add_command(clone)
505
527
  cli.add_command(pull)
506
528
  cli.add_command(push)
507
529
  cli.add_command(sync_status, "status")
508
- cli.add_command(commit)
509
530
  cli.add_command(merge)
510
531
  cli.add_command(import_cmd, "import")
511
532
  cli.add_command(ingest)
@@ -24,6 +24,7 @@ from click.testing import CliRunner
24
24
  from mcp.server.fastmcp import FastMCP
25
25
 
26
26
  from deepcell_cli.capabilities import (
27
+ MCP_ALIAS_COMMANDS,
27
28
  MCP_ALLOWED_COMMANDS,
28
29
  MCP_BLOCKED_COMMANDS as MCP_BLOCK_POLICY,
29
30
  blocked_mcp_commands,
@@ -48,7 +49,7 @@ AUTO_CONFIRM: dict[str, list[str]] = {
48
49
  # Top-level commands documented in TOOL_DESCRIPTION. Must stay equal to the
49
50
  # registered Click commands minus BLOCKED_COMMANDS — tests/test_mcp_server.py
50
51
  # cross-checks both directions so the catalog can't silently drift.
51
- DOCUMENTED_COMMANDS = frozenset(MCP_ALLOWED_COMMANDS)
52
+ DOCUMENTED_COMMANDS = frozenset(MCP_ALLOWED_COMMANDS) - MCP_ALIAS_COMMANDS
52
53
 
53
54
  TOOL_DESCRIPTION = """\
54
55
  Run a DeepCell CLI command. Structured results are JSON; `cat` prints raw XML
@@ -136,9 +137,6 @@ Available commands:
136
137
  replace <file> <old> <new>
137
138
  String-replace in XML (last resort — prefer defs;
138
139
  `edit --replace` is a deprecated alias)
139
- variant list List variants
140
- variant create <name> Create a variant
141
- (Note: variant checkout/diff/merge need a LOCAL clone — blocked here)
142
140
  rm <file> Delete a file (auto-confirmed)
143
141
  log [--file F] [-n N] Show version history
144
142
  diff <rev_a> [rev_b] Diff between revisions
@@ -147,6 +145,9 @@ Available commands:
147
145
  changes diff <base> [head] What one change did, semantically
148
146
  changes revert <base> <head>
149
147
  Undo one change as a new commit (auto-confirmed)
148
+ commit [-m MSG] Finalize changes another actor left staged
149
+ (Rare: every writer commits its own change.
150
+ Nothing staged is a no-op, not an error.)
150
151
 
151
152
  VERIFY — change one thing, see everything it affects
152
153
  relationships <file> Show relationship graph
@@ -182,7 +183,8 @@ Available commands:
182
183
 
183
184
  SYNC — local <-> cloud
184
185
  download <file> -o <path> Download file to local path
185
- (the rest of sync is blocked here — see below)
186
+ (Sync proper is blocked here — it needs a clone.
187
+ Note `commit` is NOT sync: see REVISE.)
186
188
 
187
189
  LEARN — the five reference surfaces
188
190
  help [command] Every command, flag, exit code and example.
@@ -385,7 +387,7 @@ def _config_path_for_token(token: str) -> str:
385
387
  base config's ``api_url`` on creation: ``get_api_url()`` falls back to
386
388
  DEFAULT_API_URL (the public beta) when the config it reads has no
387
389
  ``api_url``, so handing a caller an empty config silently repointed a
388
- self-hosted deployment at beta.deepcell.net. ``active_workspace`` is
390
+ self-hosted deployment at deepcell.net. ``active_workspace`` is
389
391
  deliberately NOT inherited — isolating it is the whole point of this file.
390
392
  """
391
393
  import hashlib
@@ -275,7 +275,9 @@ class DeepCellOAuthProvider(
275
275
  Returns ``None`` when the code is unknown, expired, or still pending.
276
276
  """
277
277
  try:
278
- async with httpx.AsyncClient(timeout=10, headers=client_headers("mcp")) as client:
278
+ async with httpx.AsyncClient(
279
+ timeout=10, headers=client_headers("mcp"), follow_redirects=True
280
+ ) as client:
279
281
  resp = await client.post(
280
282
  f"{self.api_url}/auth/device/token",
281
283
  json={"device_code": device_code},
@@ -436,7 +438,9 @@ class DeepCellOAuthProvider(
436
438
  headers["Authorization"] = f"Bearer {jingwei_api_key}"
437
439
 
438
440
  try:
439
- async with httpx.AsyncClient(timeout=10, headers=client_headers("mcp")) as client:
441
+ async with httpx.AsyncClient(
442
+ timeout=10, headers=client_headers("mcp"), follow_redirects=True
443
+ ) as client:
440
444
  resp = await client.post(
441
445
  f"{self.api_url}/auth/api-keys/verify",
442
446
  json={"key": token},
@@ -514,7 +518,9 @@ class DeepCellOAuthProvider(
514
518
  # so they simply expire (15 min).
515
519
  if isinstance(token, DeepCellRefreshToken):
516
520
  try:
517
- async with httpx.AsyncClient(timeout=10, headers=client_headers("mcp")) as client:
521
+ async with httpx.AsyncClient(
522
+ timeout=10, headers=client_headers("mcp"), follow_redirects=True
523
+ ) as client:
518
524
  await client.post(
519
525
  f"{self.api_url}/auth/revoke-token",
520
526
  json={"refresh_token": token.token},
@@ -547,7 +553,9 @@ class DeepCellOAuthProvider(
547
553
  grant may still be valid, so the client should retry and keep it.
548
554
  """
549
555
  try:
550
- async with httpx.AsyncClient(timeout=10, headers=client_headers("mcp")) as client:
556
+ async with httpx.AsyncClient(
557
+ timeout=10, headers=client_headers("mcp"), follow_redirects=True
558
+ ) as client:
551
559
  resp = await client.post(
552
560
  f"{self.api_url}/auth/refresh",
553
561
  json={"refresh_token": raw_refresh_token},