deepcell-cli 0.6.1__py3-none-any.whl

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 (67) hide show
  1. deepcell_cli/__init__.py +12 -0
  2. deepcell_cli/__main__.py +5 -0
  3. deepcell_cli/_findings.py +84 -0
  4. deepcell_cli/capabilities.py +560 -0
  5. deepcell_cli/capability-contract.json +15622 -0
  6. deepcell_cli/client.py +503 -0
  7. deepcell_cli/commands/__init__.py +1 -0
  8. deepcell_cli/commands/_batch_input.py +29 -0
  9. deepcell_cli/commands/_datatypes.py +56 -0
  10. deepcell_cli/commands/_negative_args.py +133 -0
  11. deepcell_cli/commands/_swapped_args.py +153 -0
  12. deepcell_cli/commands/_version_display.py +40 -0
  13. deepcell_cli/commands/_write_opts.py +139 -0
  14. deepcell_cli/commands/account.py +123 -0
  15. deepcell_cli/commands/auth.py +610 -0
  16. deepcell_cli/commands/changes.py +307 -0
  17. deepcell_cli/commands/deck.py +594 -0
  18. deepcell_cli/commands/defs.py +3890 -0
  19. deepcell_cli/commands/describe.py +902 -0
  20. deepcell_cli/commands/doc.py +529 -0
  21. deepcell_cli/commands/doctor.py +257 -0
  22. deepcell_cli/commands/download.py +36 -0
  23. deepcell_cli/commands/edit.py +384 -0
  24. deepcell_cli/commands/example.py +161 -0
  25. deepcell_cli/commands/export.py +81 -0
  26. deepcell_cli/commands/export_docx.py +57 -0
  27. deepcell_cli/commands/export_pdf.py +66 -0
  28. deepcell_cli/commands/export_pptx.py +45 -0
  29. deepcell_cli/commands/files.py +386 -0
  30. deepcell_cli/commands/grep.py +90 -0
  31. deepcell_cli/commands/guide.py +431 -0
  32. deepcell_cli/commands/help_cmd.py +348 -0
  33. deepcell_cli/commands/impact.py +382 -0
  34. deepcell_cli/commands/import_cmd.py +208 -0
  35. deepcell_cli/commands/ingest.py +110 -0
  36. deepcell_cli/commands/merge.py +399 -0
  37. deepcell_cli/commands/query.py +718 -0
  38. deepcell_cli/commands/reasoning.py +2981 -0
  39. deepcell_cli/commands/ref.py +279 -0
  40. deepcell_cli/commands/replace.py +326 -0
  41. deepcell_cli/commands/rules.py +206 -0
  42. deepcell_cli/commands/share.py +186 -0
  43. deepcell_cli/commands/sync.py +804 -0
  44. deepcell_cli/commands/upgrade.py +185 -0
  45. deepcell_cli/commands/variant.py +353 -0
  46. deepcell_cli/commands/version.py +445 -0
  47. deepcell_cli/commands/viewer.py +54 -0
  48. deepcell_cli/commands/workspace.py +101 -0
  49. deepcell_cli/config.py +352 -0
  50. deepcell_cli/context.py +187 -0
  51. deepcell_cli/errors.py +141 -0
  52. deepcell_cli/logging_setup.py +161 -0
  53. deepcell_cli/main.py +518 -0
  54. deepcell_cli/mcp_server.py +906 -0
  55. deepcell_cli/oauth_provider.py +580 -0
  56. deepcell_cli/output.py +503 -0
  57. deepcell_cli/revision.py +164 -0
  58. deepcell_cli/stages.py +223 -0
  59. deepcell_cli/surface.py +628 -0
  60. deepcell_cli/sync_state.py +120 -0
  61. deepcell_cli/upgrade_check.py +399 -0
  62. deepcell_cli/xml_replace.py +89 -0
  63. deepcell_cli-0.6.1.dist-info/METADATA +264 -0
  64. deepcell_cli-0.6.1.dist-info/RECORD +67 -0
  65. deepcell_cli-0.6.1.dist-info/WHEEL +5 -0
  66. deepcell_cli-0.6.1.dist-info/entry_points.txt +3 -0
  67. deepcell_cli-0.6.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,431 @@
1
+ """``deepcell guide`` — how DeepCell itself works, served from the topic registry.
2
+
3
+ The topic index is never restated here or in the command help: ``deepcell guide``
4
+ with no argument prints it from the registry. A hand-maintained copy would rot
5
+ the moment a topic is added. See docs/agent-skills-redesign.md.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import click
11
+
12
+ from deepcell_cli.config import get_active_workspace
13
+ from deepcell_cli.context import Ctx, pass_ctx
14
+ from deepcell_cli.errors import APIError
15
+ from deepcell_cli.output import output, print_plain
16
+ from deepcell_cli.stages import STAGE_ORDER
17
+
18
+
19
+ #: Printed above `--all`'s listing, because `--all` is an INDEX and does not
20
+ #: read like one. The 2026-08-27 prompt_stock eval is the case in point: a
21
+ #: worker ran `guide --all`, got every topic name on one screen, and went
22
+ #: straight to writing calcs — never opening `calc-specificity`, the one page
23
+ #: carrying the selector syntax its task needed. The output is ~11k of one-line
24
+ #: descriptions, so an agent that has read it has plausibly "read the guidance"
25
+ #: by every signal except the one that matters.
26
+ #:
27
+ #: One string with real newlines rather than a list of fragments: the list form
28
+ #: needed two source lines per output line and so read as four elements with
29
+ #: missing commas — the exact shape of a real bug, and CodeQL flagged it as
30
+ #: one. Prose belongs in a block that looks like the prose it prints.
31
+ _ALL_TOPICS_BANNER = """\
32
+ {count} topics. These are one-line SUMMARIES, not the guidance itself —
33
+ the syntax, the procedure and the worked examples are in the page, not in
34
+ this index. Open the topics your task touches before you write calcs,
35
+ reasoning or a deck."""
36
+
37
+
38
+ def _resolve_frontend_url(content: str) -> str:
39
+ """Replace ``${FRONTEND_URL}`` / ``${WORKSPACE_SLUG}`` placeholders.
40
+
41
+ The frontend URL comes from the ``FRONTEND_URL`` / ``DEEPCELL_FRONTEND_URL``
42
+ env vars when set, otherwise it is derived from the configured API URL.
43
+ """
44
+ from deepcell_cli.config import frontend_base_url
45
+
46
+ url = frontend_base_url().rstrip("/")
47
+ content = content.replace("${FRONTEND_URL}", url).replace(
48
+ "${DEEPCELL_FRONTEND_URL}", url
49
+ )
50
+ return content.replace("${WORKSPACE_SLUG}", get_active_workspace() or "<slug>")
51
+
52
+
53
+ @click.command()
54
+ @click.argument("topic", required=False)
55
+ @click.option("--stage", default=None, help="Only this stage's topics (e.g. generate).")
56
+ @click.option(
57
+ "--pack",
58
+ default=None,
59
+ help="One domain pack: its vocabulary page, then its topics (e.g. finance).",
60
+ )
61
+ @click.option("--packs", "list_packs_flag", is_flag=True, help="List the domain packs.")
62
+ @click.option(
63
+ "--capabilities",
64
+ "capabilities_flag",
65
+ is_flag=True,
66
+ help="Show the Jingwei/CLI/reference/transport capability contract.",
67
+ )
68
+ @click.option("--all", "all_topics", is_flag=True,
69
+ help="Also show the long-form reference pages and domain packs.")
70
+ @click.option("--search", default=None, metavar="TEXT",
71
+ help="Topics whose name or description matches, across both catalogs.")
72
+ @pass_ctx
73
+ def guide(
74
+ ctx: Ctx,
75
+ topic: str | None,
76
+ stage: str | None,
77
+ pack: str | None,
78
+ list_packs_flag: bool,
79
+ capabilities_flag: bool,
80
+ all_topics: bool,
81
+ search: str | None,
82
+ ) -> None:
83
+ """Explain how DeepCell itself works.
84
+
85
+ Topics cover the .deepcell format, calc semantics, built-in functions,
86
+ layout and decks, and reasoning structure.
87
+
88
+ \b
89
+ List the topics: deepcell guide
90
+ One stage: deepcell guide --stage generate (a bare stage name works too)
91
+ Read one: deepcell guide <topic>
92
+ Find by keyword: deepcell guide --search scenario
93
+ Domain packs: deepcell guide --packs
94
+ Capability map: deepcell guide --capabilities
95
+ One pack: deepcell guide --pack finance
96
+ Everything: deepcell guide --all
97
+
98
+ The listing is generated from the topic registry, so it is always the
99
+ complete and current set — this help text deliberately does not repeat it.
100
+ It shows the staged index by default: the work in the order it happens,
101
+ one screen. `--all` adds two more catalogs — the long-form reference pages
102
+ behind each stage, and the domain packs.
103
+ """
104
+ if capabilities_flag:
105
+ data = ctx.client.get("/guide/capabilities/index")
106
+ if ctx.fmt == "json":
107
+ output(data, ctx.fmt)
108
+ return
109
+ operations = data.get("operations", []) if isinstance(data, dict) else []
110
+ lines = [
111
+ "Jingwei endpoint -> CLI capability contract",
112
+ "",
113
+ ]
114
+ for operation in operations:
115
+ commands = operation.get("commands") or []
116
+ if commands:
117
+ disposition = ", ".join(commands)
118
+ else:
119
+ exception = operation.get("exception") or {}
120
+ disposition = f"EXEMPT ({exception.get('reason', 'unclassified')})"
121
+ lines.append(
122
+ f"{operation.get('method', '?'):<6} "
123
+ f"{operation.get('path', ''):<72} {disposition}"
124
+ )
125
+ lines += [
126
+ "",
127
+ f"{len(operations)} operations. As data: deepcell guide "
128
+ "--capabilities -f json",
129
+ ]
130
+ print_plain("\n".join(lines))
131
+ elif topic and _stage_alias(topic) in STAGE_ORDER and not stage:
132
+ # A bare stage name is the documented entry point half-typed:
133
+ # `guide orient` for `guide --stage orient`. Every transcript that
134
+ # tried it exited 1 with a did-you-mean naming `reit`. The stage
135
+ # names are the CLI's own (`deepcell_cli.stages` mirrors the guide's,
136
+ # and a test pins them equal), so no round trip decides this.
137
+ _list_topics(
138
+ ctx, stage=_stage_alias(topic), pack=pack,
139
+ all_topics=all_topics, search=search,
140
+ )
141
+ elif topic:
142
+ data = ctx.client.get(f"/guide/{topic}")
143
+ # The endpoint returns {"topic": "...", "content": "..."}
144
+ content = data.get("content", "") if isinstance(data, dict) else str(data)
145
+ content = _resolve_frontend_url(content)
146
+ if ctx.fmt == "plain":
147
+ # Human default: the Markdown body without a metadata wrapper.
148
+ print_plain(content)
149
+ else:
150
+ if isinstance(data, dict) and "content" in data:
151
+ data["content"] = content
152
+ output(data, ctx.fmt)
153
+ elif list_packs_flag:
154
+ data = ctx.client.get("/guide/packs/index")
155
+ packs = data.get("packs", []) if isinstance(data, dict) else data
156
+ if ctx.fmt == "json" or not isinstance(packs, list) or not packs:
157
+ output(data, ctx.fmt)
158
+ return
159
+ width = max(len(str(p.get("name", ""))) for p in packs)
160
+ lines = [
161
+ f"{str(p.get('name','')):<{width}} {p.get('summary','')}"
162
+ f" [{_pack_counts(p)}]"
163
+ for p in packs
164
+ ]
165
+ lines += [
166
+ "",
167
+ "One pack: deepcell guide --pack <name>",
168
+ "A pack is a vocabulary over the format, not a different format.",
169
+ ]
170
+ print_plain("\n".join(lines))
171
+ elif pack and not (search or stage or all_topics):
172
+ # Only the bare `--pack <name>`: combined with `--stage`, `--all` or
173
+ # `--search` it is a filter on the listing, as it always was.
174
+ _show_pack(ctx, pack)
175
+ else:
176
+ _list_topics(
177
+ ctx, stage=stage, pack=pack, all_topics=all_topics, search=search
178
+ )
179
+
180
+
181
+ def _stage_alias(topic: str) -> str:
182
+ """`guide orient/` is `guide orient`: the staged topic names are
183
+ `orient/start`, so a reader who has seen them types the slash."""
184
+ return topic.strip().lower().rstrip("/")
185
+
186
+
187
+ def _pack_counts(pack: dict) -> str:
188
+ """`3 topics, 1 example` — so a reader knows a thin pack is thin before
189
+ opening it, rather than reading an empty listing as a broken command."""
190
+ topics = len(pack.get("topics") or [])
191
+ examples = len(pack.get("examples") or [])
192
+ return (
193
+ f"{topics} topic{'s' if topics != 1 else ''}, "
194
+ f"{examples} example{'s' if examples != 1 else ''}"
195
+ )
196
+
197
+
198
+ def _show_pack(ctx: Ctx, pack: str) -> None:
199
+ """`--pack <name>`: the vocabulary page, then the pack's topics.
200
+
201
+ Two reads, on purpose. The listing is what tells an unknown pack apart
202
+ from a thin one (404 naming the known packs, against a real answer), and
203
+ the page is the content: three of the four packs are *only* their
204
+ vocabulary page and one worked example, so a topic listing alone printed
205
+ `(empty)` for them and the reader stopped there. `-f json` keeps the bare
206
+ topic list the endpoint returns — the vocabulary row in it names the page
207
+ to read next.
208
+ """
209
+ data = ctx.client.get("/guide", params={"pack": pack})
210
+ if ctx.fmt == "json":
211
+ output(data, ctx.fmt)
212
+ return
213
+ topics = data if isinstance(data, list) else data.get("topics", [])
214
+ topics = [t for t in topics if isinstance(t, dict)]
215
+ page_name = f"pack:{pack}"
216
+ try:
217
+ page = ctx.client.get(f"/guide/{page_name}")
218
+ except APIError as exc:
219
+ # A server without the `pack:<name>` route (older than the vocabulary
220
+ # page) answers "Unknown topic". The listing above already succeeded,
221
+ # so the pack is real: render what that server has — the topic rows
222
+ # — rather than failing a command that used to work against it.
223
+ if exc.status_code != 404:
224
+ raise
225
+ page = None
226
+ lines: list[str] = []
227
+ if page is not None:
228
+ content = page.get("content", "") if isinstance(page, dict) else str(page)
229
+ lines += [_resolve_frontend_url(content).rstrip("\n"), ""]
230
+
231
+ rows = [t for t in topics if t.get("name") != page_name]
232
+ if rows:
233
+ width = max(len(str(t.get("name", ""))) for t in rows)
234
+ lines.append(f"TOPICS — the {pack} pack's guide pages")
235
+ lines += [
236
+ f" {str(t['name']):<{width}} {_describe(t)}".rstrip() for t in rows
237
+ ]
238
+ lines += ["", "Read a topic: deepcell guide <topic>"]
239
+ else:
240
+ # Not `(empty)`: the page above is the whole pack, and saying so is
241
+ # what stops the reader looking for a listing that does not exist.
242
+ examples = page.get("examples") if isinstance(page, dict) else None
243
+ if page is not None:
244
+ lines.append(
245
+ f"No guide topics of its own — the {pack} pack is this "
246
+ "vocabulary page plus its worked example."
247
+ )
248
+ else:
249
+ lines.append(
250
+ f"No guide topics of its own — the {pack} pack is its worked "
251
+ "example."
252
+ )
253
+ if examples:
254
+ lines.append(
255
+ "Worked example: deepcell example show "
256
+ f"{examples[0]} transcript"
257
+ )
258
+ else:
259
+ lines.append(f"Worked examples: deepcell example --pack {pack}")
260
+ print_plain("\n".join(lines))
261
+
262
+
263
+ def _list_topics(
264
+ ctx: Ctx,
265
+ *,
266
+ stage: str | None,
267
+ pack: str | None,
268
+ all_topics: bool,
269
+ search: str | None,
270
+ ) -> None:
271
+ """The index, narrowed by whatever the caller passed."""
272
+ params: dict = {}
273
+ if pack:
274
+ params["pack"] = pack
275
+ if stage:
276
+ params["stage"] = stage
277
+ if all_topics:
278
+ params["all"] = "true"
279
+ if search:
280
+ params["search"] = search
281
+ data = ctx.client.get("/guide", params=params or None)
282
+
283
+ if ctx.fmt == "json":
284
+ output(data, ctx.fmt)
285
+ return
286
+
287
+ topics = data if isinstance(data, list) else data.get("topics", [])
288
+ if not isinstance(topics, list) or not topics:
289
+ if search:
290
+ # An empty list rendered as `[]` reads as a broken command
291
+ # rather than a real answer, and leaves the reader without the
292
+ # next thing to try — the guide searches names, not bodies.
293
+ print_plain(
294
+ f"No topic name or description matches '{search}'.\n"
295
+ "Browse the full index: deepcell guide --all\n"
296
+ "Legal values and enumerations: deepcell ref search "
297
+ f"{search}"
298
+ )
299
+ return
300
+ if stage:
301
+ # A stage the CLI knows and the server does not list is a
302
+ # version skew, not a typo; name the stages the server has.
303
+ print_plain(
304
+ f"No topics in stage '{stage}'.\n"
305
+ "Stages with topics: deepcell guide"
306
+ )
307
+ return
308
+ output(data, ctx.fmt)
309
+ return
310
+
311
+ # The generic plain formatter collapses {name, description} rows to
312
+ # names only — useless for picking a topic — so format it here.
313
+ width = max(len(str(t.get("name", ""))) for t in topics)
314
+ lines: list[str] = []
315
+ if all_topics:
316
+ # Before the names rather than after them: a reader who needs the
317
+ # warning is about to skim past the footer. No trailing blank —
318
+ # the stage loop below opens each band with one whenever `lines`
319
+ # is already non-empty.
320
+ lines += _ALL_TOPICS_BANNER.format(count=len(topics)).split("\n")
321
+ # Group by stage: the sections *are* the index's structure, and a flat
322
+ # list would throw away the ordering that makes the stages worth
323
+ # having. The server returns topics already in stage order, so first
324
+ # appearance is the right order to render them in.
325
+ current: str | None = "\x00"
326
+ for t in topics:
327
+ band = _band_of(t)
328
+ if band != current:
329
+ current = band
330
+ stage_name = t.get("stage")
331
+ if stage_name:
332
+ if lines:
333
+ lines.append("")
334
+ header = stage_name.upper()
335
+ summary = t.get("stage_summary") or ""
336
+ lines.append(f"{header} — {summary}" if summary else header)
337
+ elif all_topics or search:
338
+ # The appendix used to open with no header at all: 60
339
+ # unexplained names, which is what made `--all` read as a
340
+ # second competing catalog rather than an appendix. Say
341
+ # what it is once, at the top of it — and say it separately
342
+ # for the two catalogs that were sharing one heading, since
343
+ # "LEGACY" over `dcf` and `balance-sheet` advises a reader
344
+ # away from the only page that covers them.
345
+ if lines:
346
+ lines.append("")
347
+ lines.append(_appendix_header(band))
348
+ prefix = " " if t.get("stage") else ""
349
+ lines.append(
350
+ f"{prefix}{str(t['name']):<{width}} {_describe(t)}".rstrip()
351
+ )
352
+ # A snippet means the description above does NOT contain the search
353
+ # term — the match is in the topic's body. Showing the line it
354
+ # matched on is what makes such a row legible; without it the
355
+ # reader sees a description with none of their words in it.
356
+ if t.get("snippet"):
357
+ lines.append(f"{prefix}{'':<{width}} ↳ {t['snippet']}")
358
+
359
+ lines += ["", "Read a topic: deepcell guide <topic>"]
360
+ if all_topics:
361
+ # The other half of the same miss: the worker needed a formula
362
+ # SELECTOR, which is an enumeration and so has never lived in the
363
+ # guide at all. Naming `ref` here costs one line and covers the
364
+ # case where the reader is on the wrong surface entirely.
365
+ lines.append("Legal values and formula syntax: deepcell ref")
366
+ if not all_topics and not stage and not search:
367
+ lines.append(
368
+ "Long-form reference and domain packs: deepcell guide --all"
369
+ )
370
+ print_plain("\n".join(lines))
371
+
372
+
373
+ #: The core reference band. Not `LEGACY`: these pages average 2.6x the bytes of
374
+ #: the staged topic that replaced their procedure (`reasoning` is 34,630
375
+ #: against `revise/reasoning`'s 5,122), and the redirect banner they serve says
376
+ #: so — "this page is the full reference and stays authoritative for the detail
377
+ #: below". A heading that called them legacy contradicted the banner printed
378
+ #: underneath them, and told a reader to prefer the shorter page.
379
+ _REFERENCE_HEADER = (
380
+ "REFERENCE — the long-form detail behind the staged procedure, still "
381
+ "authoritative; `→` names the staged topic that owns the procedure"
382
+ )
383
+
384
+
385
+ def _band_of(topic: dict) -> str:
386
+ """Which heading a row belongs under.
387
+
388
+ Bands, not stages, because the appendix holds two unrelated catalogs and
389
+ used to print one heading over both. A stage name is its own band so the
390
+ staged index still breaks per stage, and a pack is keyed by name so a
391
+ second pack with topics gets its own heading rather than being folded into
392
+ finance's.
393
+ """
394
+ if topic.get("stage"):
395
+ return str(topic["stage"])
396
+ if topic.get("legacy"):
397
+ return "reference"
398
+ return f"pack:{topic.get('pack') or 'unknown'}"
399
+
400
+
401
+ def _appendix_header(band: str) -> str:
402
+ """The heading for one appendix band."""
403
+ if band == "reference":
404
+ return _REFERENCE_HEADER
405
+ name = band.split(":", 1)[1]
406
+ return (
407
+ f"PACK: {name.upper()} — domain templates and vocabulary, not part of "
408
+ f"the core path; list them alone with `deepcell guide --pack {name}`"
409
+ )
410
+
411
+
412
+ def _describe(topic: dict) -> str:
413
+ """A row's description, plus where its procedure went if it moved.
414
+
415
+ A legacy topic and the staged topic that replaced it are the same subject
416
+ under two names, and `--all` listed both with nothing saying so — leaving
417
+ the reader to guess whether `items` or `orient/concepts` was the one to
418
+ open. The server already knows: `replaced_by` is the alias table it has
419
+ always had. Rendering it turns the appendix from a second, competing
420
+ catalog into a set of forwarding addresses.
421
+
422
+ `guide:` pointers print bare because they are the common case and the
423
+ prefix is noise; anything else keeps its namespace, since `ref function`
424
+ and `rules R1` are read with different commands.
425
+ """
426
+ description = topic.get("description", "") or ""
427
+ pointer = topic.get("replaced_by")
428
+ if not pointer:
429
+ return description
430
+ shown = pointer[len("guide:"):] if pointer.startswith("guide:") else pointer
431
+ return f"{description} → {shown}".strip()