chaoscypher-cli 0.1.0__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 (133) hide show
  1. chaoscypher_cli/__init__.py +32 -0
  2. chaoscypher_cli/__main__.py +483 -0
  3. chaoscypher_cli/benchmark/__init__.py +90 -0
  4. chaoscypher_cli/benchmark/chat_dataset.py +242 -0
  5. chaoscypher_cli/benchmark/config.py +302 -0
  6. chaoscypher_cli/benchmark/data/config/extraction.yaml +54 -0
  7. chaoscypher_cli/benchmark/data/config/full.yaml +45 -0
  8. chaoscypher_cli/benchmark/data/config/quick.yaml +26 -0
  9. chaoscypher_cli/benchmark/data/datasets/scientific_methods_tiny/manifest.yaml +8 -0
  10. chaoscypher_cli/benchmark/data/datasets/scientific_methods_tiny/scientific_methods_tiny.txt +106 -0
  11. chaoscypher_cli/benchmark/data/datasets/tech_encyclopedia_tiny/manifest.yaml +9 -0
  12. chaoscypher_cli/benchmark/data/datasets/tech_encyclopedia_tiny/queries.yaml +428 -0
  13. chaoscypher_cli/benchmark/data/datasets/tech_encyclopedia_tiny/tech_encyclopedia_tiny.txt +98 -0
  14. chaoscypher_cli/benchmark/data/datasets/war_and_peace_tiny/manifest.yaml +8 -0
  15. chaoscypher_cli/benchmark/data/datasets/war_and_peace_tiny/war_and_peace_tiny.txt +212 -0
  16. chaoscypher_cli/benchmark/dataset.py +86 -0
  17. chaoscypher_cli/benchmark/discovery.py +269 -0
  18. chaoscypher_cli/benchmark/embedding_dataset.py +197 -0
  19. chaoscypher_cli/benchmark/extraction_dataset.py +349 -0
  20. chaoscypher_cli/benchmark/graph_cache.py +147 -0
  21. chaoscypher_cli/benchmark/graph_provider.py +107 -0
  22. chaoscypher_cli/benchmark/judge_prompts.py +75 -0
  23. chaoscypher_cli/benchmark/leaderboard.py +293 -0
  24. chaoscypher_cli/benchmark/models.py +95 -0
  25. chaoscypher_cli/benchmark/orchestrator.py +407 -0
  26. chaoscypher_cli/benchmark/queries.py +141 -0
  27. chaoscypher_cli/benchmark/results.py +118 -0
  28. chaoscypher_cli/benchmark/runner.py +191 -0
  29. chaoscypher_cli/benchmark/scorers/__init__.py +11 -0
  30. chaoscypher_cli/benchmark/scorers/chat.py +107 -0
  31. chaoscypher_cli/benchmark/scorers/embedding.py +101 -0
  32. chaoscypher_cli/benchmark/scorers/v7.py +70 -0
  33. chaoscypher_cli/commands/__init__.py +36 -0
  34. chaoscypher_cli/commands/benchmark/__init__.py +29 -0
  35. chaoscypher_cli/commands/benchmark/fixture.py +95 -0
  36. chaoscypher_cli/commands/benchmark/init.py +73 -0
  37. chaoscypher_cli/commands/benchmark/list.py +65 -0
  38. chaoscypher_cli/commands/benchmark/run.py +261 -0
  39. chaoscypher_cli/commands/benchmark/show.py +37 -0
  40. chaoscypher_cli/commands/chat.py +805 -0
  41. chaoscypher_cli/commands/completions.py +251 -0
  42. chaoscypher_cli/commands/compose/__init__.py +45 -0
  43. chaoscypher_cli/commands/compose/build.py +123 -0
  44. chaoscypher_cli/commands/compose/down.py +67 -0
  45. chaoscypher_cli/commands/compose/run.py +76 -0
  46. chaoscypher_cli/commands/compose/up.py +142 -0
  47. chaoscypher_cli/commands/config_cmd.py +387 -0
  48. chaoscypher_cli/commands/db/__init__.py +52 -0
  49. chaoscypher_cli/commands/db/create.py +77 -0
  50. chaoscypher_cli/commands/db/current.py +51 -0
  51. chaoscypher_cli/commands/db/delete.py +74 -0
  52. chaoscypher_cli/commands/db/info.py +86 -0
  53. chaoscypher_cli/commands/db/list.py +128 -0
  54. chaoscypher_cli/commands/db/migrate.py +202 -0
  55. chaoscypher_cli/commands/db/switch.py +47 -0
  56. chaoscypher_cli/commands/diagnostics.py +91 -0
  57. chaoscypher_cli/commands/doctor.py +303 -0
  58. chaoscypher_cli/commands/graph/__init__.py +33 -0
  59. chaoscypher_cli/commands/health.py +214 -0
  60. chaoscypher_cli/commands/lexicon/__init__.py +37 -0
  61. chaoscypher_cli/commands/lexicon/info.py +167 -0
  62. chaoscypher_cli/commands/lexicon/list.py +96 -0
  63. chaoscypher_cli/commands/lexicon/login.py +312 -0
  64. chaoscypher_cli/commands/lexicon/pull.py +142 -0
  65. chaoscypher_cli/commands/lexicon/push.py +153 -0
  66. chaoscypher_cli/commands/lexicon/remove.py +121 -0
  67. chaoscypher_cli/commands/lexicon/search.py +137 -0
  68. chaoscypher_cli/commands/link/__init__.py +48 -0
  69. chaoscypher_cli/commands/link/create.py +102 -0
  70. chaoscypher_cli/commands/link/delete.py +108 -0
  71. chaoscypher_cli/commands/link/get.py +117 -0
  72. chaoscypher_cli/commands/link/list.py +144 -0
  73. chaoscypher_cli/commands/link/update.py +113 -0
  74. chaoscypher_cli/commands/node/__init__.py +45 -0
  75. chaoscypher_cli/commands/node/create.py +147 -0
  76. chaoscypher_cli/commands/node/delete.py +130 -0
  77. chaoscypher_cli/commands/node/get.py +153 -0
  78. chaoscypher_cli/commands/node/list.py +146 -0
  79. chaoscypher_cli/commands/node/update.py +106 -0
  80. chaoscypher_cli/commands/package/__init__.py +33 -0
  81. chaoscypher_cli/commands/package/export.py +175 -0
  82. chaoscypher_cli/commands/package/load.py +161 -0
  83. chaoscypher_cli/commands/quality/__init__.py +41 -0
  84. chaoscypher_cli/commands/quality/analyze.py +224 -0
  85. chaoscypher_cli/commands/quality/recalculate.py +159 -0
  86. chaoscypher_cli/commands/quality/report.py +285 -0
  87. chaoscypher_cli/commands/quality/score.py +323 -0
  88. chaoscypher_cli/commands/quality/utils.py +58 -0
  89. chaoscypher_cli/commands/render_orchestration.py +64 -0
  90. chaoscypher_cli/commands/runtime/__init__.py +13 -0
  91. chaoscypher_cli/commands/runtime/serve.py +247 -0
  92. chaoscypher_cli/commands/setup.py +1068 -0
  93. chaoscypher_cli/commands/source/__init__.py +122 -0
  94. chaoscypher_cli/commands/source/add.py +592 -0
  95. chaoscypher_cli/commands/source/confirm.py +238 -0
  96. chaoscypher_cli/commands/source/delete.py +59 -0
  97. chaoscypher_cli/commands/source/extract.py +314 -0
  98. chaoscypher_cli/commands/source/get.py +503 -0
  99. chaoscypher_cli/commands/source/list.py +160 -0
  100. chaoscypher_cli/commands/source/rebuild_search.py +96 -0
  101. chaoscypher_cli/commands/source/search.py +270 -0
  102. chaoscypher_cli/commands/template/__init__.py +48 -0
  103. chaoscypher_cli/commands/template/create.py +154 -0
  104. chaoscypher_cli/commands/template/delete.py +74 -0
  105. chaoscypher_cli/commands/template/get.py +135 -0
  106. chaoscypher_cli/commands/template/list.py +127 -0
  107. chaoscypher_cli/commands/template/update.py +144 -0
  108. chaoscypher_cli/commands/template/utils.py +50 -0
  109. chaoscypher_cli/commands/upgrade.py +58 -0
  110. chaoscypher_cli/commands/workflow/__init__.py +31 -0
  111. chaoscypher_cli/commands/workflow/get.py +148 -0
  112. chaoscypher_cli/commands/workflow/list.py +120 -0
  113. chaoscypher_cli/context.py +678 -0
  114. chaoscypher_cli/engine_config.py +96 -0
  115. chaoscypher_cli/lazy.py +195 -0
  116. chaoscypher_cli/mcp/__init__.py +9 -0
  117. chaoscypher_cli/mcp/command.py +143 -0
  118. chaoscypher_cli/py.typed +0 -0
  119. chaoscypher_cli/sources/__init__.py +36 -0
  120. chaoscypher_cli/sources/domains.py +39 -0
  121. chaoscypher_cli/sources/pipeline.py +1337 -0
  122. chaoscypher_cli/sources/service.py +1734 -0
  123. chaoscypher_cli/utils/__init__.py +92 -0
  124. chaoscypher_cli/utils/console.py +88 -0
  125. chaoscypher_cli/utils/display.py +50 -0
  126. chaoscypher_cli/utils/files.py +123 -0
  127. chaoscypher_cli/utils/llm_check.py +165 -0
  128. chaoscypher_cli/utils/paths.py +120 -0
  129. chaoscypher_cli-0.1.0.dist-info/METADATA +174 -0
  130. chaoscypher_cli-0.1.0.dist-info/RECORD +133 -0
  131. chaoscypher_cli-0.1.0.dist-info/WHEEL +5 -0
  132. chaoscypher_cli-0.1.0.dist-info/entry_points.txt +2 -0
  133. chaoscypher_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2024-2026 Chaos Cypher, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ """Chaos Cypher CLI - Command-line tools for Chaos Cypher knowledge graph library.
5
+
6
+ This package provides a user-friendly command-line interface for:
7
+ - Managing knowledge graphs and databases
8
+ - Importing and exporting data
9
+ - Searching and querying graphs
10
+ - Viewing statistics and analytics
11
+
12
+ Example:
13
+ chaoscypher db create my-graph
14
+ chaoscypher source add documents/
15
+ chaoscypher source search "artificial intelligence"
16
+ """
17
+
18
+ from importlib.metadata import PackageNotFoundError, version
19
+
20
+
21
+ try:
22
+ __version__ = version("chaoscypher-cli")
23
+ except PackageNotFoundError:
24
+ # Source-tree run without an installed dist (e.g. `python -m chaoscypher_cli`
25
+ # from a checkout that wasn't `uv sync`'d). Fall back to a sentinel so the
26
+ # CLI still launches.
27
+ __version__ = "0.0.0+unknown"
28
+
29
+ __author__ = "Denis MacPherson"
30
+ __license__ = "AGPL-3.0-only"
31
+
32
+ __all__ = ["__version__"]
@@ -0,0 +1,483 @@
1
+ # Copyright (C) 2024-2026 Chaos Cypher, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ """Chaos Cypher CLI - Knowledge Graph Platform.
5
+
6
+ Organized by user intent:
7
+
8
+ 1. Package Manager (docker-like):
9
+ - pull, push (root level)
10
+ - lexicon/ (login, logout, whoami, search, list, info, remove)
11
+
12
+ 2. Runtime (like docker run):
13
+ - serve, run, compose
14
+
15
+ 3. Builder:
16
+ - init, source/, graph/
17
+
18
+ 4. Graph building:
19
+ - graph/ (node, link, template, workflow, package)
20
+ """
21
+
22
+ import os
23
+ import sys
24
+ import warnings
25
+
26
+
27
+ def _configure_console_encoding() -> None:
28
+ """Force UTF-8 on Windows stdout/stderr before any Rich Console exists.
29
+
30
+ The default Windows console code page is cp1252, which has no mapping
31
+ for routine Rich glyphs (``…`` ``✓`` ``─``) or content we put in
32
+ migration descriptions (``→`` U+2192). Rich's LegacyWindowsTerm
33
+ writes through ``sys.stdout``, so when ``sys.stdout.encoding`` is
34
+ cp1252 and a non-cp1252 char comes through, Python raises
35
+ ``UnicodeEncodeError`` and the whole command aborts mid-render —
36
+ that's the failure mode that crashed ``chaoscypher db migrate status``
37
+ on Windows before this hook existed.
38
+
39
+ ``errors="replace"`` makes any future unmappable character degrade to
40
+ a ``?`` glyph instead of aborting. POSIX shells default to UTF-8
41
+ already, so this is a Windows-only concern.
42
+
43
+ Best-effort: ``sys.stdout`` / ``sys.stderr`` may have been replaced
44
+ with a non-``TextIOWrapper`` (pytest capture, embedders, redirection
45
+ to a binary pipe). We swallow ``AttributeError`` / ``OSError`` rather
46
+ than refuse to start the CLI over an encoding tweak.
47
+ """
48
+ if sys.platform != "win32":
49
+ return
50
+ for stream in (sys.stdout, sys.stderr): # type: ignore[unreachable]
51
+ try:
52
+ stream.reconfigure(encoding="utf-8", errors="replace")
53
+ except AttributeError, OSError:
54
+ continue
55
+
56
+
57
+ _configure_console_encoding()
58
+
59
+
60
+ # Suppress noisy third-party warnings before any chaoscypher_core /
61
+ # langchain import runs (configure_logging in _preconfigure_logging will
62
+ # transitively import 184 langchain/langgraph modules — one of which fires
63
+ # a PendingDeprecationWarning at import time). The langchain filter has to
64
+ # be applied AFTER langchain_core's __init__ runs because
65
+ # surface_langchain_deprecation_warnings() forces a "default" action on
66
+ # LangChain* categories; we therefore import langchain_core eagerly here
67
+ # so we control the order.
68
+ warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarning)
69
+ try:
70
+ import langchain_core # noqa: F401 — triggers surface_langchain_deprecation_warnings()
71
+ from langchain_core._api.deprecation import (
72
+ LangChainDeprecationWarning,
73
+ LangChainPendingDeprecationWarning,
74
+ )
75
+
76
+ warnings.filterwarnings("ignore", category=LangChainDeprecationWarning)
77
+ warnings.filterwarnings("ignore", category=LangChainPendingDeprecationWarning)
78
+ except Exception:
79
+ # langchain_core not installed (fresh dev env, test harness) — skip.
80
+ pass
81
+
82
+
83
+ def _preconfigure_logging() -> None:
84
+ """Configure structlog with a quiet WARNING default before any submodule import.
85
+
86
+ Why this runs unconditionally:
87
+ Before this hook, only the ``mcp`` subcommand pre-configured
88
+ logging; every other command relied on ``context.py:190`` to
89
+ call ``configure_logging`` lazily during ``connect()``. Any
90
+ log line emitted before that call (``settings_file_not_found``,
91
+ ``settings_loaded``, etc.) went through structlog's default
92
+ config, which dumps INFO/DEBUG to stdout — leaking debug lines
93
+ on every ``chaoscypher --help``, ``chaoscypher source list``, etc.
94
+
95
+ Why it works:
96
+ ``configure_logging`` has a process-wide idempotency guard
97
+ (``logging._chaoscypher_logging_configured``). The very first
98
+ call wins; ``context.py:190`` becomes a no-op. Operators who
99
+ want verbose logs set ``LOG_LEVEL=INFO`` (or ``DEBUG``) in the
100
+ environment — both paths honour the env var, so the level the
101
+ operator picks is the level they get.
102
+
103
+ MCP stdio transport caveat (Bug 15, May 2026):
104
+ The MCP server reserves stdout for JSON-RPC; structlog noise on
105
+ stdout breaks stricter clients. For ``chaoscypher mcp …``, route
106
+ to ``sys.stderr``.
107
+
108
+ Best-effort: if the chaoscypher_core import fails (broken install,
109
+ test harness), swallow and let the CLI start normally.
110
+ """
111
+ try:
112
+ from chaoscypher_core.utils.logging import configure_logging
113
+
114
+ configure_logging(
115
+ log_level=os.getenv("LOG_LEVEL", "WARNING"),
116
+ stream=sys.stderr if "mcp" in sys.argv[1:] else None,
117
+ )
118
+ except Exception:
119
+ pass
120
+
121
+
122
+ _preconfigure_logging()
123
+
124
+ import click
125
+
126
+ from chaoscypher_cli import __version__
127
+ from chaoscypher_cli.lazy import LazyGroup
128
+
129
+
130
+ # =============================================================================
131
+ # Lazy-loaded commands (heavy imports - only load when executed)
132
+ # =============================================================================
133
+ # Format: "cmd": ("import.path:attr", "Short help text")
134
+ # Help text enables fast --help without loading the command module
135
+ LAZY_COMMANDS = {
136
+ # Setup wizard
137
+ "setup": (
138
+ "chaoscypher_cli.commands.setup:setup",
139
+ "Configure LLM provider for extraction and chat",
140
+ ),
141
+ # Package management (docker-like UX)
142
+ "pull": ("chaoscypher_cli.commands.lexicon.pull:pull", "Download a package from Lexicon Hub"),
143
+ "push": ("chaoscypher_cli.commands.lexicon.push:push", "Upload a package to Lexicon Hub"),
144
+ # Runtime commands
145
+ "serve": (
146
+ "chaoscypher_cli.commands.runtime.serve:serve",
147
+ "Start the local API server",
148
+ ),
149
+ "compose": (
150
+ "chaoscypher_cli.commands.compose:compose",
151
+ "Multi-package orchestration and composition",
152
+ ),
153
+ # Builder commands
154
+ "source": (
155
+ "chaoscypher_cli.commands.source:source",
156
+ "Add, list, search, and manage document sources",
157
+ ),
158
+ # Groups
159
+ "graph": ("chaoscypher_cli.commands.graph:graph", "Build and manage knowledge graphs"),
160
+ "lexicon": (
161
+ "chaoscypher_cli.commands.lexicon:lexicon",
162
+ "Lexicon Hub - login, search, manage packages",
163
+ ),
164
+ # AI chat
165
+ "chat": ("chaoscypher_cli.commands.chat:chat", "Chat with AI using your knowledge graph"),
166
+ # Database management
167
+ "db": ("chaoscypher_cli.commands.db:db", "Manage databases (create, list, delete, reset)"),
168
+ # Configuration
169
+ "config": ("chaoscypher_cli.commands.config_cmd:config", "View and manage CLI configuration"),
170
+ "completions": (
171
+ "chaoscypher_cli.commands.completions:completions",
172
+ "Generate shell completion script (bash, zsh, fish)",
173
+ ),
174
+ # MCP server
175
+ "mcp": ("chaoscypher_cli.mcp.command:mcp", "Start MCP server over stdio"),
176
+ # System health
177
+ "health": ("chaoscypher_cli.commands.health:health", "Check system health status"),
178
+ "doctor": (
179
+ "chaoscypher_cli.commands.doctor:doctor",
180
+ "Run a comprehensive system diagnostic sweep",
181
+ ),
182
+ "diagnostics": (
183
+ "chaoscypher_cli.commands.diagnostics:diagnostics",
184
+ "Export diagnostic bundle for bug reports",
185
+ ),
186
+ # Benchmarking
187
+ "benchmark": (
188
+ "chaoscypher_cli.commands.benchmark:benchmark",
189
+ "Run and inspect the extraction benchmark",
190
+ ),
191
+ # Schema migrations
192
+ "upgrade": (
193
+ "chaoscypher_cli.commands.upgrade:upgrade_command",
194
+ "Apply pending Alembic migrations (alembic upgrade head)",
195
+ ),
196
+ # Orchestration template renderer (called by entrypoint.sh)
197
+ "render-orchestration": (
198
+ "chaoscypher_cli.commands.render_orchestration:render_orchestration_command",
199
+ "Render nginx/supervisord/valkey configs from current Pydantic settings",
200
+ ),
201
+ }
202
+
203
+
204
+ # Subcommands that bypass the first-run setup gate. These are either
205
+ # bootstrap commands (setup, completions), DB-free read-only diagnostics
206
+ # (health, doctor, diagnostics, config), or schema-housekeeping that
207
+ # must run before any feature command can ever succeed (db, upgrade,
208
+ # render-orchestration). `--help` / `--version` are handled separately
209
+ # via sys.argv inspection.
210
+ _FIRST_RUN_SAFE_SUBCOMMANDS = frozenset(
211
+ {
212
+ "setup",
213
+ "health",
214
+ "doctor",
215
+ "diagnostics",
216
+ "config",
217
+ "db",
218
+ "upgrade",
219
+ "completions",
220
+ "render-orchestration",
221
+ }
222
+ )
223
+
224
+
225
+ # Subcommands that are allowed to run while the DB is blocked on a
226
+ # tier-2 migration. The migrate subcommand is obviously needed so the
227
+ # user can resolve the block; setup/health/diagnostics are read-only
228
+ # or don't touch the live schema.
229
+ _UPGRADE_SAFE_SUBCOMMANDS = frozenset(
230
+ {
231
+ "db",
232
+ "setup",
233
+ "health",
234
+ "doctor",
235
+ "diagnostics",
236
+ "config",
237
+ "benchmark",
238
+ "upgrade",
239
+ "render-orchestration",
240
+ # `mcp` starts its own degraded maintenance-mode server when the DB is
241
+ # blocked (see chaoscypher_cli/mcp/command.py), so it must NOT be
242
+ # exited(2) here — that drop is exactly the opaque -32000 we're fixing.
243
+ "mcp",
244
+ }
245
+ )
246
+
247
+
248
+ def _extract_database_override(argv: list[str]) -> str | None:
249
+ """Find the ``--database``/``-d`` override in a raw arg list.
250
+
251
+ Returns the database name the user supplied via a subcommand option,
252
+ or ``None`` if no override is present (in which case callers should
253
+ fall back to ``settings.yaml``'s ``current_database``). Accepts both
254
+ space-separated forms (``--database foo``, ``-d foo``) and
255
+ equals-joined (``--database=foo``, ``-d=foo``); the equals form is
256
+ the only one Click actually generates internally but operators type
257
+ both.
258
+
259
+ The guard runs at parent-group time, before Click has parsed the
260
+ subcommand's option schema, so we cannot ask Click "what was
261
+ --database resolved to" — we inspect ``sys.argv`` directly. False
262
+ positives are bounded: a value of literally ``"--database"`` inside
263
+ an unrelated string argument would not match because we look for
264
+ the flag as a token, not a substring.
265
+ """
266
+ for i, tok in enumerate(argv):
267
+ if tok in ("--database", "-d") and i + 1 < len(argv):
268
+ return argv[i + 1]
269
+ if tok.startswith("--database="):
270
+ return tok.split("=", 1)[1]
271
+ if tok.startswith("-d="):
272
+ return tok.split("=", 1)[1]
273
+ return None
274
+
275
+
276
+ def _upgrade_guard(ctx: click.Context) -> None:
277
+ """Refuse to run DB-touching commands while the upgrade state is blocked.
278
+
279
+ Surfaces a clear, actionable message pointing at
280
+ ``chaoscypher db migrate`` instead of letting the downstream
281
+ command crash with a schema mismatch. Tolerant of missing
282
+ infrastructure (no DB yet, migration helpers not importable, etc.)
283
+ so fresh installs and limited environments aren't blocked by a
284
+ gate that can't read its own state.
285
+ """
286
+ # `--help` / `-h` are DB-free no-ops — Click will print help and exit
287
+ # before any handler body runs, so gating them just hides command
288
+ # discovery behind a migration the user can't make sense of yet. Same
289
+ # for shell-completion parses, which Click flags via resilient_parsing.
290
+ if ctx.resilient_parsing or any(arg in ("--help", "-h") for arg in sys.argv[1:]):
291
+ return
292
+
293
+ invoked = ctx.invoked_subcommand
294
+ if invoked is None or invoked in _UPGRADE_SAFE_SUBCOMMANDS:
295
+ return
296
+ try:
297
+ from chaoscypher_cli.engine_config import read_current_database
298
+ from chaoscypher_core.database.engine import get_db_path
299
+ from chaoscypher_core.database.migrations.state import get_upgrade_state
300
+ except Exception:
301
+ return # Can't import — fresh install or dev context. Let the command run.
302
+
303
+ try:
304
+ # If the invocation overrides the database via `--database <name>` /
305
+ # `-d <name>`, gate against THAT database — not whatever
306
+ # settings.yaml calls "current". Otherwise `chaoscypher source list
307
+ # --database fresh_db` would refuse to run whenever the default DB
308
+ # is mid-migration, which is exactly the situation a fresh
309
+ # workspace is meant to escape.
310
+ db_name = (
311
+ _extract_database_override(sys.argv[1:])
312
+ or os.environ.get("CHAOSCYPHER_DATABASE")
313
+ or read_current_database()
314
+ or "default"
315
+ )
316
+ db_path = get_db_path(db_name)
317
+ state = get_upgrade_state(db_path)
318
+ except Exception:
319
+ return # No DB yet or unreadable state; don't block the command.
320
+
321
+ if state.ready:
322
+ return
323
+
324
+ click.echo(
325
+ click.style(
326
+ "This database is waiting on a schema upgrade before it can be used.",
327
+ fg="yellow",
328
+ bold=True,
329
+ ),
330
+ err=True,
331
+ )
332
+ if state.message:
333
+ click.echo(f" {state.message}", err=True)
334
+ click.echo(
335
+ "\nRun one of:\n"
336
+ " chaoscypher db migrate status — see what's pending\n"
337
+ " chaoscypher db migrate apply — apply the pending migrations\n"
338
+ " chaoscypher db migrate rollback — restore from pre-upgrade backup",
339
+ err=True,
340
+ )
341
+ ctx.exit(2)
342
+
343
+
344
+ def _stale_cli_yaml_notice(ctx: click.Context) -> None:
345
+ """Warn once (per invocation) about a leftover, no-longer-read cli.yaml.
346
+
347
+ The 2026-06 config unification retired ``cli.yaml`` — all configuration
348
+ now lives in ``settings.yaml``. A cli.yaml left behind by an older install
349
+ is silently ignored; this prints a single dim stderr line so the operator
350
+ knows the file is dead and can delete it. Stateless: the note prints while
351
+ the file exists and disappears once it's removed.
352
+
353
+ Bypassed for ``--help`` / shell-completion parses (mirroring the other
354
+ guards) so command discovery isn't cluttered.
355
+ """
356
+ if ctx.resilient_parsing or any(arg in ("--help", "-h", "--version") for arg in sys.argv[1:]):
357
+ return
358
+ try:
359
+ from chaoscypher_cli.utils.paths import get_config_dir
360
+
361
+ stale = get_config_dir() / "cli.yaml"
362
+ if not stale.exists():
363
+ return
364
+ except Exception:
365
+ return # best-effort UX, never block the CLI
366
+
367
+ click.echo(
368
+ click.style(
369
+ f"chaoscypher: note: {stale} is no longer read and is ignored "
370
+ "(config unification); your settings live in settings.yaml — the "
371
+ "old file can be deleted.",
372
+ dim=True,
373
+ ),
374
+ err=True,
375
+ )
376
+
377
+
378
+ def _first_run_gate(ctx: click.Context) -> None:
379
+ """Auto-route a fresh `pipx install` user into the setup wizard.
380
+
381
+ A user with no engine configuration in ``settings.yaml`` (and no
382
+ ``CHAOSCYPHER_LLM_PROVIDER`` env override) will otherwise
383
+ hit "LLM Required" the moment they run ``chaoscypher source add
384
+ doc.pdf`` — confusing first-run UX. When we detect that signature,
385
+ interactively offer to run ``chaoscypher setup`` first; in
386
+ non-interactive mode, print an actionable message and exit 2.
387
+
388
+ Bypassed for:
389
+ * ``--help`` / ``-h`` / ``--version`` (let Click render those).
390
+ * Shell-completion parses (``ctx.resilient_parsing``).
391
+ * Bootstrap / read-only subcommands (see
392
+ ``_FIRST_RUN_SAFE_SUBCOMMANDS``).
393
+ * Any invocation where setup is already complete (``settings.yaml``
394
+ records ``setup_completed`` / an ``llm.chat_provider``, or the
395
+ ``CHAOSCYPHER_LLM_PROVIDER`` env var is set).
396
+
397
+ Best-effort: if the config helpers can't import (broken install,
398
+ test harness), let the CLI continue — the gate is friendly UX, not
399
+ a correctness invariant.
400
+ """
401
+ if ctx.resilient_parsing or any(arg in ("--help", "-h", "--version") for arg in sys.argv[1:]):
402
+ return
403
+
404
+ invoked = ctx.invoked_subcommand
405
+ if invoked is None or invoked in _FIRST_RUN_SAFE_SUBCOMMANDS:
406
+ return
407
+
408
+ try:
409
+ from chaoscypher_cli.engine_config import is_setup_completed, settings_yaml_path
410
+ except Exception:
411
+ return
412
+
413
+ try:
414
+ if is_setup_completed():
415
+ return
416
+ settings_path = settings_yaml_path()
417
+ except Exception:
418
+ return
419
+
420
+ # First run signature confirmed.
421
+ is_tty = sys.stdin.isatty() and sys.stderr.isatty()
422
+
423
+ click.echo(
424
+ click.style(
425
+ "It looks like this is your first time running Chaos Cypher.",
426
+ fg="cyan",
427
+ bold=True,
428
+ ),
429
+ err=True,
430
+ )
431
+ click.echo(
432
+ "No engine configuration was found at "
433
+ f"{settings_path}, and no LLM provider has been set up yet.",
434
+ err=True,
435
+ )
436
+ click.echo("", err=True)
437
+
438
+ if not is_tty:
439
+ click.echo(
440
+ "Run `chaoscypher setup` to configure an LLM provider before "
441
+ f"using `chaoscypher {invoked}`.",
442
+ err=True,
443
+ )
444
+ ctx.exit(2)
445
+
446
+ if not click.confirm(
447
+ click.style("Run `chaoscypher setup` now?", fg="cyan"),
448
+ default=True,
449
+ err=True,
450
+ ):
451
+ click.echo(
452
+ f"Skipped. Run `chaoscypher setup` when you're ready, then re-run `chaoscypher {invoked}`.",
453
+ err=True,
454
+ )
455
+ ctx.exit(2)
456
+
457
+ # Hand control to the setup wizard. We exit afterwards rather than
458
+ # continue with the original subcommand — the user's original argv
459
+ # may reference a file that doesn't exist yet, a DB that hasn't been
460
+ # created, etc., and the setup wizard's "Next steps" block already
461
+ # shows them what to run next.
462
+ from chaoscypher_cli.commands.setup import setup as setup_cmd
463
+
464
+ ctx.invoke(setup_cmd)
465
+ ctx.exit(0)
466
+
467
+
468
+ @click.group(
469
+ cls=LazyGroup,
470
+ lazy_subcommands=LAZY_COMMANDS,
471
+ context_settings={"max_content_width": 120},
472
+ )
473
+ @click.version_option(version=__version__, prog_name="chaoscypher")
474
+ @click.pass_context
475
+ def main(ctx: click.Context) -> None:
476
+ """Chaos Cypher CLI - Knowledge Graph Platform."""
477
+ _upgrade_guard(ctx)
478
+ _first_run_gate(ctx)
479
+ _stale_cli_yaml_notice(ctx)
480
+
481
+
482
+ if __name__ == "__main__":
483
+ main()
@@ -0,0 +1,90 @@
1
+ # Copyright (C) 2024-2026 Chaos Cypher, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ """ChaosCypher Extraction Benchmark engine.
5
+
6
+ Public API: dataset/scorer protocols, the ExtractionDataset and
7
+ V7ExtractionScorer v1 implementations, model config + price registry,
8
+ the sequential runner, named-config loader, dataset discovery (built-in
9
+ + user overlay), and leaderboard rendering. Result types and JSON I/O
10
+ live in ``results``.
11
+
12
+ Vocabulary:
13
+ dataset - the test unit (corpus + metadata + how to evaluate it).
14
+ corpus - the body of text inside a dataset.
15
+ config - a runnable benchmark recipe (name, params, dataset ids,
16
+ models). Loaded by ``bench run [NAME]``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from chaoscypher_cli.benchmark.config import (
22
+ DEFAULT_CONFIG_NAME,
23
+ BenchmarkConfig,
24
+ list_configs,
25
+ load_config,
26
+ )
27
+ from chaoscypher_cli.benchmark.dataset import (
28
+ BenchmarkDataset,
29
+ DatasetScorer,
30
+ DatasetSource,
31
+ RawOutput,
32
+ )
33
+ from chaoscypher_cli.benchmark.discovery import (
34
+ builtin_dataset_root,
35
+ discover_datasets,
36
+ user_benchmark_root,
37
+ user_dataset_root,
38
+ )
39
+ from chaoscypher_cli.benchmark.extraction_dataset import ExtractionDataset
40
+ from chaoscypher_cli.benchmark.leaderboard import (
41
+ ModelAggregate,
42
+ aggregate_by_model,
43
+ render_leaderboard,
44
+ )
45
+ from chaoscypher_cli.benchmark.models import (
46
+ ModelConfig,
47
+ compute_cost,
48
+ filter_models,
49
+ )
50
+ from chaoscypher_cli.benchmark.results import (
51
+ BenchmarkResult,
52
+ ScoreResult,
53
+ dump_results,
54
+ load_results,
55
+ )
56
+ from chaoscypher_cli.benchmark.runner import BENCHMARK_VERSION, run_benchmark
57
+ from chaoscypher_cli.benchmark.scorers.v7 import V7ExtractionScorer
58
+
59
+
60
+ __version__ = "0.1.0"
61
+
62
+ __all__ = [
63
+ "BENCHMARK_VERSION",
64
+ "DEFAULT_CONFIG_NAME",
65
+ "BenchmarkConfig",
66
+ "BenchmarkDataset",
67
+ "BenchmarkResult",
68
+ "DatasetScorer",
69
+ "DatasetSource",
70
+ "ExtractionDataset",
71
+ "ModelAggregate",
72
+ "ModelConfig",
73
+ "RawOutput",
74
+ "ScoreResult",
75
+ "V7ExtractionScorer",
76
+ "__version__",
77
+ "aggregate_by_model",
78
+ "builtin_dataset_root",
79
+ "compute_cost",
80
+ "discover_datasets",
81
+ "dump_results",
82
+ "filter_models",
83
+ "list_configs",
84
+ "load_config",
85
+ "load_results",
86
+ "render_leaderboard",
87
+ "run_benchmark",
88
+ "user_benchmark_root",
89
+ "user_dataset_root",
90
+ ]