core-cli 1.0.2__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {core_cli-1.0.2 → core_cli-2.0.0}/.github/workflows/ci.yml +9 -5
  2. core_cli-2.0.0/PKG-INFO +107 -0
  3. core_cli-2.0.0/README.md +78 -0
  4. {core_cli-1.0.2 → core_cli-2.0.0}/pyproject.toml +22 -3
  5. core_cli-2.0.0/src/core_cli/client.py +319 -0
  6. core_cli-2.0.0/src/core_cli/command.py +77 -0
  7. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/main.py +0 -2
  8. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/__init__.py +0 -1
  9. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/actions.py +3 -17
  10. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/audit_duplicates.py +3 -3
  11. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/bridges.py +4 -6
  12. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/check_imports.py +3 -3
  13. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/docstrings.py +3 -3
  14. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/fix_atomic.py +3 -17
  15. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/format.py +4 -4
  16. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/integrity.py +2 -2
  17. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/lint.py +3 -3
  18. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/logging.py +3 -17
  19. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/test.py +3 -3
  20. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/claim.py +4 -4
  21. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/list.py +4 -4
  22. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/next.py +6 -6
  23. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/propose.py +5 -6
  24. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/project/__init__.py +2 -3
  25. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/project/onboard.py +4 -4
  26. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/project/scout.py +3 -3
  27. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/__init__.py +2 -1
  28. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/create.py +3 -3
  29. core_cli-2.0.0/src/core_cli/resources/proposals/integrate.py +37 -0
  30. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/list.py +3 -3
  31. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/proposals/manage.py +6 -6
  32. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/__init__.py +0 -1
  33. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/audit.py +10 -6
  34. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/fix_ids.py +12 -6
  35. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/resolve_duplicates.py +14 -6
  36. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/vectors/__init__.py +1 -7
  37. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/vectors/hub.py +1 -1
  38. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/vectors/query.py +5 -5
  39. core_cli-2.0.0/tests/test_client.py +54 -0
  40. {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_code.py +0 -47
  41. core_cli-2.0.0/tests/test_contract.py +124 -0
  42. core_cli-1.0.2/PKG-INFO +0 -72
  43. core_cli-1.0.2/README.md +0 -46
  44. core_cli-1.0.2/src/core_cli/resources/code/check_ui.py +0 -61
  45. core_cli-1.0.2/src/core_cli/resources/project/docs.py +0 -41
  46. core_cli-1.0.2/src/core_cli/resources/proposals/integrate.py +0 -57
  47. core_cli-1.0.2/src/core_cli/resources/secrets/__init__.py +0 -10
  48. core_cli-1.0.2/src/core_cli/resources/secrets/hub.py +0 -9
  49. core_cli-1.0.2/src/core_cli/resources/secrets/manage.py +0 -160
  50. core_cli-1.0.2/src/core_cli/resources/symbols/sync.py +0 -39
  51. core_cli-1.0.2/src/core_cli/resources/vectors/rebuild.py +0 -71
  52. core_cli-1.0.2/src/core_cli/resources/vectors/status.py +0 -40
  53. core_cli-1.0.2/src/core_cli/resources/vectors/sync.py +0 -50
  54. core_cli-1.0.2/src/core_cli/resources/vectors/sync_code.py +0 -58
  55. {core_cli-1.0.2 → core_cli-2.0.0}/.gitignore +0 -0
  56. {core_cli-1.0.2 → core_cli-2.0.0}/LICENSE +0 -0
  57. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/__init__.py +0 -0
  58. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/__init__.py +0 -0
  59. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/code/hub.py +0 -0
  60. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/lane/__init__.py +0 -0
  61. {core_cli-1.0.2 → core_cli-2.0.0}/src/core_cli/resources/symbols/hub.py +0 -0
  62. {core_cli-1.0.2 → core_cli-2.0.0}/tests/__init__.py +0 -0
  63. {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_lane.py +0 -0
  64. {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_project.py +0 -0
  65. {core_cli-1.0.2 → core_cli-2.0.0}/tests/test_proposals.py +0 -0
@@ -19,7 +19,9 @@ jobs:
19
19
  python-version: "3.12"
20
20
 
21
21
  - name: Install ruff
22
- run: pip install ruff
22
+ # Pinned: an unpinned ruff picked up new default rules and turned lint
23
+ # red on unchanged code (2026-10). Bump deliberately.
24
+ run: pip install ruff==0.16.10
23
25
 
24
26
  - name: ruff check
25
27
  run: ruff check src/
@@ -37,10 +39,11 @@ jobs:
37
39
  - name: Install mypy + stubs
38
40
  run: pip install mypy types-PyYAML
39
41
 
42
+ - name: Install package
43
+ run: pip install -e .
44
+
40
45
  - name: mypy
41
- # core-runtime ships no type stubs; ignore-missing-imports preserves
42
- # checking of core-cli's own code without requiring stub packages.
43
- run: mypy src/ --ignore-missing-imports --no-error-summary
46
+ run: mypy src/ --no-error-summary
44
47
 
45
48
  test:
46
49
  name: Test
@@ -53,10 +56,11 @@ jobs:
53
56
  python-version: "3.12"
54
57
 
55
58
  - name: Install package + dev deps
56
- # core-runtime is on PyPI (>=2.8.0); installs as a transitive dep.
57
59
  run: pip install -e ".[dev]"
58
60
 
59
61
  - name: Run tests
62
+ # tests/test_contract.py reads CORE's OpenAPI contract from CORE's main
63
+ # branch on GitHub (no CORE checkout here).
60
64
  run: pytest tests/ -v
61
65
 
62
66
  build:
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.5
2
+ Name: core-cli
3
+ Version: 2.0.0
4
+ Summary: Consumer governance CLI for CORE — an HTTP client for governing repositories against CORE constitutional rules.
5
+ Project-URL: Homepage, https://github.com/DariuszNewecki/CORE
6
+ Project-URL: Repository, https://github.com/DariuszNewecki/core-cli
7
+ Project-URL: Bug Tracker, https://github.com/DariuszNewecki/core-cli/issues
8
+ Author-email: Dariusz Newecki <d.newecki@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,code-quality,constitutional-ai,developer-tools,governance
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: rich>=13.7
24
+ Requires-Dist: typer>=0.16
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # core-cli
31
+
32
+ The consumer command-line tool for [CORE](https://github.com/DariuszNewecki/CORE): review
33
+ and approve proposals, run checks and fixes on the repository CORE governs, onboard a
34
+ repository (BYOR), and work the assisted remediation lane. Its binary is called `core`.
35
+
36
+ `core-cli` is an HTTP client and nothing more. Every command talks to a **running CORE
37
+ API**, using only the routes in CORE's published
38
+ [OpenAPI contract](https://github.com/DariuszNewecki/CORE/blob/main/docs/reference/openapi.json);
39
+ it does not install or import CORE. The operator tool, `core-admin`, ships separately in
40
+ `core-runtime` and covers the CORE installation itself: secrets, vector store, database
41
+ sync ([which one do I need?](https://dariusznewecki.github.io/CORE/cli-reference/)).
42
+
43
+ ## Prerequisites
44
+
45
+ - Python 3.12+
46
+ - A reachable CORE API, which itself needs PostgreSQL and Qdrant. See CORE's
47
+ [getting started](https://dariusznewecki.github.io/CORE/getting-started/).
48
+
49
+ > **No authentication.** OSS CORE runs in trusted-localhost mode: the API binds to
50
+ > loopback and accepts every request. Anyone who can reach it can approve and execute
51
+ > proposals. Keep it on `127.0.0.1` and do not expose it on a shared network.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install core-cli
57
+ ```
58
+
59
+ Its only dependencies are `typer`, `rich`, `httpx` and `PyYAML`.
60
+
61
+ ## Configure
62
+
63
+ `core` uses `http://127.0.0.1:8000` unless you point it elsewhere:
64
+
65
+ ```bash
66
+ export CORE_API_URL=http://127.0.0.1:8000
67
+ ```
68
+
69
+ ## Use
70
+
71
+ ```bash
72
+ core proposals list # proposals awaiting a decision
73
+ core proposals show <id> # risk assessment and planned changes
74
+ core lane list # delegated findings waiting for assisted remediation
75
+ core project onboard <path> # preview delivering the machinery floor (BYOR)
76
+ core project onboard <path> --write # deliver it
77
+ core <group> <command> --help # options for any command
78
+ ```
79
+
80
+ Commands that change files or data **preview by default** and act only with `--write`.
81
+
82
+ ## Commands
83
+
84
+ | Group | What it covers |
85
+ |---|---|
86
+ | `proposals` | `list`, `show`, `create`, `approve`, `reject`, `execute`, `integrate` |
87
+ | `lane` | `list`, `next`, `claim`, `propose`: assisted remediation of delegated findings |
88
+ | `project` | `onboard`, `scout`, `promote`: bring a repository under governance (BYOR) |
89
+ | `code` | quality and verification: `lint`, `format`, `test`, `check-imports`, `audit-duplicates`, … |
90
+ | `symbols` | `audit`, `fix-ids`, `resolve-duplicates` |
91
+ | `vectors` | `query`: semantic search over the governed repository |
92
+
93
+ Every command, option and default is in the generated
94
+ [`core` command reference](https://dariusznewecki.github.io/CORE/reference/core/).
95
+
96
+ ## Architecture
97
+
98
+ The split between the consumer CLI (`core`) and the operator CLI (`core-admin`) is
99
+ recorded in
100
+ [ADR-146](https://github.com/DariuszNewecki/CORE/blob/main/.specs/decisions/ADR-146-cli-consumer-operator-split.md):
101
+ `core` covers operations on the repository CORE governs; `core-admin` covers the CORE
102
+ installation. `tests/test_contract.py` checks every route this client calls against CORE's
103
+ OpenAPI contract.
104
+
105
+ ## License
106
+
107
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,78 @@
1
+ # core-cli
2
+
3
+ The consumer command-line tool for [CORE](https://github.com/DariuszNewecki/CORE): review
4
+ and approve proposals, run checks and fixes on the repository CORE governs, onboard a
5
+ repository (BYOR), and work the assisted remediation lane. Its binary is called `core`.
6
+
7
+ `core-cli` is an HTTP client and nothing more. Every command talks to a **running CORE
8
+ API**, using only the routes in CORE's published
9
+ [OpenAPI contract](https://github.com/DariuszNewecki/CORE/blob/main/docs/reference/openapi.json);
10
+ it does not install or import CORE. The operator tool, `core-admin`, ships separately in
11
+ `core-runtime` and covers the CORE installation itself: secrets, vector store, database
12
+ sync ([which one do I need?](https://dariusznewecki.github.io/CORE/cli-reference/)).
13
+
14
+ ## Prerequisites
15
+
16
+ - Python 3.12+
17
+ - A reachable CORE API, which itself needs PostgreSQL and Qdrant. See CORE's
18
+ [getting started](https://dariusznewecki.github.io/CORE/getting-started/).
19
+
20
+ > **No authentication.** OSS CORE runs in trusted-localhost mode: the API binds to
21
+ > loopback and accepts every request. Anyone who can reach it can approve and execute
22
+ > proposals. Keep it on `127.0.0.1` and do not expose it on a shared network.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install core-cli
28
+ ```
29
+
30
+ Its only dependencies are `typer`, `rich`, `httpx` and `PyYAML`.
31
+
32
+ ## Configure
33
+
34
+ `core` uses `http://127.0.0.1:8000` unless you point it elsewhere:
35
+
36
+ ```bash
37
+ export CORE_API_URL=http://127.0.0.1:8000
38
+ ```
39
+
40
+ ## Use
41
+
42
+ ```bash
43
+ core proposals list # proposals awaiting a decision
44
+ core proposals show <id> # risk assessment and planned changes
45
+ core lane list # delegated findings waiting for assisted remediation
46
+ core project onboard <path> # preview delivering the machinery floor (BYOR)
47
+ core project onboard <path> --write # deliver it
48
+ core <group> <command> --help # options for any command
49
+ ```
50
+
51
+ Commands that change files or data **preview by default** and act only with `--write`.
52
+
53
+ ## Commands
54
+
55
+ | Group | What it covers |
56
+ |---|---|
57
+ | `proposals` | `list`, `show`, `create`, `approve`, `reject`, `execute`, `integrate` |
58
+ | `lane` | `list`, `next`, `claim`, `propose`: assisted remediation of delegated findings |
59
+ | `project` | `onboard`, `scout`, `promote`: bring a repository under governance (BYOR) |
60
+ | `code` | quality and verification: `lint`, `format`, `test`, `check-imports`, `audit-duplicates`, … |
61
+ | `symbols` | `audit`, `fix-ids`, `resolve-duplicates` |
62
+ | `vectors` | `query`: semantic search over the governed repository |
63
+
64
+ Every command, option and default is in the generated
65
+ [`core` command reference](https://dariusznewecki.github.io/CORE/reference/core/).
66
+
67
+ ## Architecture
68
+
69
+ The split between the consumer CLI (`core`) and the operator CLI (`core-admin`) is
70
+ recorded in
71
+ [ADR-146](https://github.com/DariuszNewecki/CORE/blob/main/.specs/decisions/ADR-146-cli-consumer-operator-split.md):
72
+ `core` covers operations on the repository CORE governs; `core-admin` covers the CORE
73
+ installation. `tests/test_contract.py` checks every route this client calls against CORE's
74
+ OpenAPI contract.
75
+
76
+ ## License
77
+
78
+ MIT. See [LICENSE](LICENSE).
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "core-cli"
7
- version = "1.0.2"
8
- description = "Consumer governance CLI for CORE — pure HTTP client for governing repositories against CORE constitutional rules."
7
+ version = "2.0.0"
8
+ description = "Consumer governance CLI for CORE — an HTTP client for governing repositories against CORE constitutional rules."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
11
11
  requires-python = ">=3.12"
@@ -23,8 +23,12 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  "Development Status :: 4 - Beta",
25
25
  ]
26
+ # HTTP client only: no dependency on core-runtime (ADR-146, amended 2026-10-03).
26
27
  dependencies = [
27
- "core-runtime>=2.9.0",
28
+ "httpx>=0.27",
29
+ "pyyaml>=6.0",
30
+ "rich>=13.7",
31
+ "typer>=0.16",
28
32
  ]
29
33
 
30
34
  [project.optional-dependencies]
@@ -48,3 +52,18 @@ packages = ["src/core_cli"]
48
52
  asyncio_mode = "auto"
49
53
  pythonpath = ["src"]
50
54
  testpaths = ["tests"]
55
+
56
+ [tool.ruff]
57
+ line-length = 88
58
+ target-version = "py312"
59
+ src = ["src"]
60
+
61
+ [tool.ruff.lint]
62
+ # B008: typer.Option / typer.Argument as parameter defaults is Typer's
63
+ # documented idiom, not a bug (CORE ignores it for the same reason).
64
+ ignore = ["B008"]
65
+
66
+ [tool.ruff.lint.isort]
67
+ # Two blank lines after imports, matching CORE's layout.
68
+ known-first-party = ["core_cli"]
69
+ lines-after-imports = 2
@@ -0,0 +1,319 @@
1
+ """HTTP client for the CORE API — core-cli's only link to CORE.
2
+
3
+ core-cli talks to a running CORE over HTTP and nothing else: it has no
4
+ dependency on core-runtime (ADR-146, amended 2026-10-03). Every route called
5
+ here is in CORE's published OpenAPI contract, ``docs/reference/openapi.json``
6
+ in the CORE repository; ``tests/test_contract.py`` checks that.
7
+
8
+ The base URL comes from the ``CORE_API_URL`` environment variable, defaulting
9
+ to the loopback address CORE binds to.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import os
16
+ from typing import Any
17
+
18
+ import httpx
19
+
20
+
21
+ DEFAULT_BASE_URL = "http://127.0.0.1:8000"
22
+ _TIMEOUT_SECONDS = 30.0
23
+ _LONG_TIMEOUT_SECONDS = 300.0
24
+ _POLL_INTERVAL_SECONDS = 1.0
25
+ _POLL_TERMINAL_STATES = frozenset({"completed", "failed"})
26
+
27
+
28
+ # ID: e5aeda69-d26e-4353-81eb-83e998e968e9
29
+ class CoreApiError(httpx.HTTPStatusError):
30
+ """An error response (status >= 400) from the CORE API.
31
+
32
+ An ``httpx.HTTPStatusError``, so commands can branch on
33
+ ``exc.response.status_code``; ``status_code`` and ``detail`` (the API's
34
+ ``detail`` field, or the raw body) are also set directly.
35
+ """
36
+
37
+ def __init__(self, response: httpx.Response) -> None:
38
+ try:
39
+ detail = response.json().get("detail", response.text)
40
+ except ValueError:
41
+ detail = response.text
42
+ self.status_code = response.status_code
43
+ self.detail = detail
44
+ super().__init__(
45
+ f"API error {response.status_code}: {detail}",
46
+ request=response.request,
47
+ response=response,
48
+ )
49
+
50
+
51
+ # ID: 49ef4530-5b49-4ebb-a9f6-b59150780e1b
52
+ class CoreApiClient:
53
+ """Async client for the CORE API.
54
+
55
+ Methods mirror the API: flat methods for single routes, plus small
56
+ namespaces (``inspect``, ``lane``, ``project``, ``symbols``, ``vectors``)
57
+ for route families. A response with status >= 400 raises ``CoreApiError``.
58
+ """
59
+
60
+ def __init__(self, base_url: str | None = None) -> None:
61
+ self.base_url = base_url or os.environ.get("CORE_API_URL") or DEFAULT_BASE_URL
62
+ self.inspect = _Inspect(self)
63
+ self.lane = _Lane(self)
64
+ self.project = _Project(self)
65
+ self.symbols = _Symbols(self)
66
+ self.vectors = _Vectors(self)
67
+
68
+ async def _request(
69
+ self,
70
+ method: str,
71
+ path: str,
72
+ *,
73
+ timeout: float = _TIMEOUT_SECONDS,
74
+ **kwargs: Any,
75
+ ) -> dict:
76
+ async with httpx.AsyncClient(timeout=timeout) as http:
77
+ response = await http.request(method, f"{self.base_url}{path}", **kwargs)
78
+ if response.status_code >= 400:
79
+ raise CoreApiError(response)
80
+ return response.json()
81
+
82
+ async def _poll_run(
83
+ self, run_id: str, timeout_seconds: float = _LONG_TIMEOUT_SECONDS
84
+ ) -> dict:
85
+ """Poll GET /v1/fix/runs/{run_id} until the run completes or fails."""
86
+ async with asyncio.timeout(timeout_seconds):
87
+ while True:
88
+ payload = await self._request("GET", f"/v1/fix/runs/{run_id}")
89
+ if payload.get("status") in _POLL_TERMINAL_STATES:
90
+ return payload
91
+ await asyncio.sleep(_POLL_INTERVAL_SECONDS)
92
+
93
+ # -- actions and fixes ----------------------------------------------
94
+
95
+ # ID: d5e64d0c-4a5f-4518-96d2-d15388306f00
96
+ async def list_actions(self) -> dict:
97
+ """GET /v1/actions — list the registered atomic actions."""
98
+ return await self._request("GET", "/v1/actions")
99
+
100
+ # ID: 7a49a17d-66a0-413b-b76a-5044c399f109
101
+ async def run_fix(
102
+ self,
103
+ fix_id: str,
104
+ target_files: list[str] | None = None,
105
+ write: bool = False,
106
+ params: dict[str, Any] | None = None,
107
+ ) -> dict:
108
+ """POST /v1/fix/run/{fix_id} — dispatch a fix action; poll with _poll_run."""
109
+ return await self._request(
110
+ "POST",
111
+ f"/v1/fix/run/{fix_id}",
112
+ json={
113
+ "target_files": target_files or [],
114
+ "write": write,
115
+ "params": params or {},
116
+ },
117
+ )
118
+
119
+ # -- checks on the governed repository ------------------------------
120
+
121
+ # ID: 80078450-7b0e-4a2e-b62c-b265d179aa74
122
+ async def lint(self) -> dict:
123
+ """POST /v1/lint — black --check and ruff check."""
124
+ return await self._request("POST", "/v1/lint", timeout=_LONG_TIMEOUT_SECONDS)
125
+
126
+ # ID: e07d0d0b-c466-4613-b63b-0df395730ba6
127
+ async def quality_imports(self) -> dict:
128
+ """POST /v1/quality/imports — import-resolution check."""
129
+ return await self._request("POST", "/v1/quality/imports")
130
+
131
+ # ID: 58ba647c-a7ce-4d58-966b-c21f55e7d3fe
132
+ async def quality_tests(self, path: str | None = None) -> dict:
133
+ """POST /v1/quality/tests — pytest, optionally scoped; poll with _poll_run."""
134
+ return await self._request("POST", "/v1/quality/tests", json={"path": path})
135
+
136
+ # ID: 2887d4f0-91a0-4db7-8df9-d33857113212
137
+ async def baseline(self, label: str = "default") -> dict:
138
+ """POST /v1/integrity/baseline — fingerprint src/."""
139
+ return await self._request(
140
+ "POST", "/v1/integrity/baseline", json={"label": label}
141
+ )
142
+
143
+ # ID: 7c860bc0-ec98-4a6f-9589-03c96bbe81b9
144
+ async def verify(self, label: str = "default") -> dict:
145
+ """POST /v1/integrity/verify — compare src/ against a baseline."""
146
+ return await self._request(
147
+ "POST", "/v1/integrity/verify", json={"label": label}
148
+ )
149
+
150
+ # ID: 2798cdc1-6743-4451-9c96-e6ecc2cb1d4e
151
+ async def integrate(self, commit_message: str) -> dict:
152
+ """POST /v1/integrate — stage, format/lint and commit the working tree."""
153
+ return await self._request(
154
+ "POST",
155
+ "/v1/integrate",
156
+ json={"commit_message": commit_message},
157
+ timeout=_LONG_TIMEOUT_SECONDS,
158
+ )
159
+
160
+ # -- proposals --------------------------------------------------------
161
+
162
+ # ID: 8bafbb59-8d23-45a5-8fc5-271b694bacbc
163
+ async def list_proposals(self, status: str | None = None, limit: int = 50) -> dict:
164
+ """GET /v1/proposals."""
165
+ params: dict[str, Any] = {"limit": limit}
166
+ if status is not None:
167
+ params["status"] = status
168
+ return await self._request("GET", "/v1/proposals", params=params)
169
+
170
+ # ID: 957be7d4-98b7-4e47-99d8-115921b3b12f
171
+ async def create_proposal(
172
+ self,
173
+ goal: str,
174
+ actions: list[dict] | None = None,
175
+ files: list[str] | None = None,
176
+ created_by: str = "cli_operator",
177
+ write: bool = True,
178
+ ) -> dict:
179
+ """POST /v1/proposals."""
180
+ return await self._request(
181
+ "POST",
182
+ "/v1/proposals",
183
+ json={
184
+ "goal": goal,
185
+ "actions": actions or [],
186
+ "files": files or [],
187
+ "created_by": created_by,
188
+ "write": write,
189
+ },
190
+ )
191
+
192
+ # ID: 4c806d5c-fe6e-4ac9-a0cc-dae70a01af93
193
+ async def get_proposal(self, proposal_id: str) -> dict:
194
+ """GET /v1/proposals/{proposal_id}."""
195
+ return await self._request("GET", f"/v1/proposals/{proposal_id}")
196
+
197
+ # ID: b01d56fa-f71a-472d-aa2d-c68e420689f0
198
+ async def approve_proposal(
199
+ self, proposal_id: str, approved_by: str, approval_authority: str
200
+ ) -> dict:
201
+ """POST /v1/proposals/{proposal_id}/approve."""
202
+ return await self._request(
203
+ "POST",
204
+ f"/v1/proposals/{proposal_id}/approve",
205
+ json={"approved_by": approved_by, "approval_authority": approval_authority},
206
+ )
207
+
208
+ # ID: 0e5c0e42-34bd-44c0-848f-62a1c0f78037
209
+ async def reject_proposal(self, proposal_id: str, reason: str) -> dict:
210
+ """POST /v1/proposals/{proposal_id}/reject."""
211
+ return await self._request(
212
+ "POST", f"/v1/proposals/{proposal_id}/reject", json={"reason": reason}
213
+ )
214
+
215
+ # ID: bef4a752-8d94-4d20-89e3-fc1ba79899d9
216
+ async def execute_proposal(self, proposal_id: str, write: bool = False) -> dict:
217
+ """POST /v1/proposals/{proposal_id}/execute."""
218
+ return await self._request(
219
+ "POST", f"/v1/proposals/{proposal_id}/execute", json={"write": write}
220
+ )
221
+
222
+
223
+ class _Namespace:
224
+ def __init__(self, client: CoreApiClient) -> None:
225
+ self._client = client
226
+
227
+
228
+ class _Inspect(_Namespace):
229
+ async def analysis_duplicates(self, threshold: float = 0.85) -> dict:
230
+ """GET /v1/analysis/duplicates."""
231
+ return await self._client._request(
232
+ "GET",
233
+ "/v1/analysis/duplicates",
234
+ params={"threshold": threshold},
235
+ timeout=_LONG_TIMEOUT_SECONDS,
236
+ )
237
+
238
+ async def analysis_bridges(self, consuming: str | None = None) -> dict:
239
+ """GET /v1/analysis/bridges."""
240
+ params: dict[str, Any] = {}
241
+ if consuming is not None:
242
+ params["consuming"] = consuming
243
+ return await self._client._request("GET", "/v1/analysis/bridges", params=params)
244
+
245
+
246
+ class _Lane(_Namespace):
247
+ async def list_delegated(self, limit: int = 50) -> dict:
248
+ """GET /v1/lane."""
249
+ return await self._client._request("GET", "/v1/lane", params={"limit": limit})
250
+
251
+ async def next_delegated(self) -> dict:
252
+ """GET /v1/lane/next."""
253
+ return await self._client._request("GET", "/v1/lane/next")
254
+
255
+ async def get_delegated(self, finding_id: str) -> dict:
256
+ """GET /v1/lane/{finding_id}."""
257
+ return await self._client._request("GET", f"/v1/lane/{finding_id}")
258
+
259
+ async def claim(self, finding_id: str, agent: str) -> dict:
260
+ """POST /v1/lane/{finding_id}/claim."""
261
+ return await self._client._request(
262
+ "POST", f"/v1/lane/{finding_id}/claim", params={"agent": agent}
263
+ )
264
+
265
+ async def propose(
266
+ self, finding_id: str, patch: str, validation_run_id: str
267
+ ) -> dict:
268
+ """POST /v1/lane/{finding_id}/propose."""
269
+ return await self._client._request(
270
+ "POST",
271
+ f"/v1/lane/{finding_id}/propose",
272
+ json={"patch": patch, "validation_run_id": validation_run_id},
273
+ )
274
+
275
+
276
+ class _Project(_Namespace):
277
+ async def scout(self, path: str, reset: bool = False) -> dict:
278
+ """POST /v1/project/scout."""
279
+ return await self._client._request(
280
+ "POST", "/v1/project/scout", json={"path": path, "reset": reset}
281
+ )
282
+
283
+ async def onboard(
284
+ self, path: str, write: bool = False, stage: bool = False
285
+ ) -> dict:
286
+ """POST /v1/project/onboard."""
287
+ return await self._client._request(
288
+ "POST",
289
+ "/v1/project/onboard",
290
+ json={"path": path, "write": write, "stage": stage},
291
+ )
292
+
293
+ async def promote(self, path: str) -> dict:
294
+ """POST /v1/project/onboard/promote."""
295
+ return await self._client._request(
296
+ "POST", "/v1/project/onboard/promote", json={"path": path}
297
+ )
298
+
299
+
300
+ class _Symbols(_Namespace):
301
+ async def get_unassigned(self) -> dict:
302
+ """GET /v1/symbols/unassigned."""
303
+ return await self._client._request("GET", "/v1/symbols/unassigned")
304
+
305
+ async def get_drift(self) -> dict:
306
+ """GET /v1/symbols/drift."""
307
+ return await self._client._request("GET", "/v1/symbols/drift")
308
+
309
+
310
+ class _Vectors(_Namespace):
311
+ async def query(
312
+ self, query: str, collection: str = "policies", limit: int = 5
313
+ ) -> dict:
314
+ """POST /v1/vectors/query — semantic search over the governed repository."""
315
+ return await self._client._request(
316
+ "POST",
317
+ "/v1/vectors/query",
318
+ json={"query": query, "collection": collection, "limit": limit},
319
+ )
@@ -0,0 +1,77 @@
1
+ """``core_command`` — runs an async Typer command and reports failures.
2
+
3
+ core-cli's own replacement for core-runtime's decorator of the same name. It
4
+ does only what an HTTP client needs: run the coroutine, show the dry-run
5
+ banner for dangerous commands, ask for confirmation, and turn an exception
6
+ into a clean error and exit code 1. It never builds a CORE context.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import functools
13
+ import inspect
14
+ import sys
15
+ from collections.abc import Callable
16
+ from typing import Any
17
+
18
+ import typer
19
+ from rich.console import Console
20
+ from rich.markup import escape
21
+
22
+
23
+ console = Console()
24
+
25
+
26
+ def _confirm(message: str) -> bool:
27
+ if not sys.stdin.isatty():
28
+ console.print(
29
+ f"{message}\n[yellow]No interactive terminal — cancelled. Run it from "
30
+ "a terminal to confirm.[/yellow]"
31
+ )
32
+ return False
33
+ return typer.confirm(message, default=False)
34
+
35
+
36
+ # ID: b7b9aa4c-a723-411e-8d37-9eb18903259e
37
+ def core_command(
38
+ *, dangerous: bool = False, confirmation: bool = False
39
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
40
+ """Wrap an async command.
41
+
42
+ ``dangerous`` commands that take a ``write`` flag print a dry-run banner
43
+ when it is off; with ``confirmation`` they ask before running with it on.
44
+ """
45
+
46
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
47
+ has_write = "write" in inspect.signature(func).parameters
48
+
49
+ @functools.wraps(func)
50
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
51
+ write = bool(kwargs.get("write", False))
52
+ if dangerous and has_write and not write:
53
+ console.print(
54
+ "[bold yellow]⚠️ DRY RUN MODE[/bold yellow]\n"
55
+ " No changes will be made. Use [cyan]--write[/cyan] to apply.\n"
56
+ )
57
+ if (
58
+ dangerous
59
+ and confirmation
60
+ and write
61
+ and not _confirm("🚨 Confirm this operation?")
62
+ ):
63
+ raise typer.Exit(0)
64
+ try:
65
+ return asyncio.run(func(*args, **kwargs))
66
+ except typer.Exit:
67
+ raise
68
+ except Exception as exc:
69
+ console.print(
70
+ "\n[bold red]❌ Command failed:[/bold red]\n "
71
+ + escape(f"{type(exc).__name__}: {exc}")
72
+ )
73
+ raise typer.Exit(1) from exc
74
+
75
+ return wrapper
76
+
77
+ return decorator
@@ -8,7 +8,6 @@ from core_cli.resources.code import app as code_app
8
8
  from core_cli.resources.lane import app as lane_app
9
9
  from core_cli.resources.project import app as project_app
10
10
  from core_cli.resources.proposals import app as proposals_app
11
- from core_cli.resources.secrets import app as secrets_app
12
11
  from core_cli.resources.symbols import app as symbols_app
13
12
  from core_cli.resources.vectors import app as vectors_app
14
13
 
@@ -22,7 +21,6 @@ app = typer.Typer(
22
21
 
23
22
  app.add_typer(lane_app, name="lane")
24
23
  app.add_typer(proposals_app, name="proposals")
25
- app.add_typer(secrets_app, name="secrets")
26
24
  app.add_typer(code_app, name="code")
27
25
  app.add_typer(symbols_app, name="symbols")
28
26
  app.add_typer(vectors_app, name="vectors")