bazis-cli 2.4.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.
@@ -0,0 +1,33 @@
1
+ # bazis-cli — guide for AI agents
2
+
3
+ The command `bazis` creates, extends and audits Bazis projects with Claude: it runs Claude
4
+ Code (Claude Agent SDK) in the project with the MCP server bazis-mcp and the rules of
5
+ Bazis. It is a tool for the developer's machine, not a dependency of a project.
6
+
7
+ ## Commands
8
+
9
+ - `bazis new DIR "what the project does"` writes the skeleton of a project (package with
10
+ settings, root router and ASGI app, `project.env`, `.env` with a generated secret key,
11
+ `requirements*.txt`, `tests/`, `.mcp.json`, `AGENTS.md`), creates `.venv` with Bazis and
12
+ bazis-mcp (`--no-venv` skips it), then the agent chooses the Bazis packages and builds the
13
+ apps, migrations and tests.
14
+ - `bazis add PACKAGE ["what for"]` installs a Bazis package and its Bazis dependencies and
15
+ sets them up as their guides say.
16
+ - `bazis audit [--fix]` reviews the project against the guides and checks of its packages;
17
+ without `--fix` the agent can only read.
18
+
19
+ Options: `--project-dir` (default: the current directory), `--model` (default
20
+ `claude-opus-5-5`), `--effort` (default `high`), `--budget USD`, `--yes` (run shell commands
21
+ without asking).
22
+
23
+ ## Rules
24
+
25
+ - Edits of the files of the project are accepted; every shell command and every other tool
26
+ call (edits or reads outside the project, web) is asked in the terminal (`a` allows the
27
+ rest of the run) unless `--yes`; without a terminal and `--yes` they are denied.
28
+ - `.env` cannot be read; git cannot commit, push, reset or rebase: the user reviews and
29
+ commits the changes.
30
+ - Only the MCP server bazis-mcp and no Claude Code settings of the project or the user are
31
+ loaded. The checks of bazis-mcp run the project code: use the commands in trusted
32
+ projects.
33
+ - Credentials are those of Claude Code: `claude login` or `ANTHROPIC_API_KEY`.
@@ -0,0 +1,19 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ try:
16
+ from importlib.metadata import PackageNotFoundError, version
17
+ __version__ = version('bazis-cli')
18
+ except PackageNotFoundError:
19
+ __version__ = 'dev'
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '2.4.0'
22
+ __version_tuple__ = version_tuple = (2, 4, 0)
23
+
24
+ __commit_id__ = commit_id = 'g2d504c4db'
@@ -0,0 +1,274 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ The agent that does the work of a command: Claude Code (Claude Agent SDK) in the project
17
+ directory, with the MCP server bazis-mcp of the project and the rules of Bazis.
18
+ """
19
+
20
+ import json
21
+ import re
22
+ import sys
23
+ import warnings
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import TextIO
27
+
28
+ from claude_agent_sdk import (
29
+ AssistantMessage,
30
+ ClaudeAgentOptions,
31
+ HookMatcher,
32
+ PermissionResultAllow,
33
+ PermissionResultDeny,
34
+ ResultError,
35
+ ResultMessage,
36
+ TextBlock,
37
+ ToolUseBlock,
38
+ query,
39
+ )
40
+ from claude_agent_sdk.types import CanUseToolShadowedWarning
41
+
42
+
43
+ DEFAULT_MODEL = 'claude-opus-5-5'
44
+ DEFAULT_EFFORT = 'high'
45
+
46
+ #: git commands that change the repository: the user commits, never the agent
47
+ GIT_DENY = ['Bash(git commit:*)', 'Bash(git push:*)', 'Bash(git reset:*)', 'Bash(git rebase:*)']
48
+ #: tools a review cannot use
49
+ WRITE_TOOLS = ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash', 'WebFetch', 'WebSearch']
50
+ GIT_COMMAND_RE = re.compile(r'(^|[;&|(]\s*)git\s+(commit|push|reset|rebase)\b', re.MULTILINE)
51
+
52
+ RULES = """\
53
+ # Bazis
54
+
55
+ You work on a Bazis project in the current directory. Bazis builds JSON:API services on
56
+ Django, FastAPI and Pydantic; the core is the package `bazis`, every other feature is a
57
+ separate package `bazis-<name>`. A project installs only the packages it needs.
58
+
59
+ - The MCP server `bazis` is the source of truth about Bazis, not your memory of it:
60
+ `list_packages` (what each package solves and requires), `package_guide` (how to use a
61
+ package, also `bazis` for the core), `project_info` (the installed packages, settings,
62
+ models and routes of this project) and `run_doctor` (the system checks). Read the guide
63
+ of every package before you use it and follow it exactly.
64
+ - Read `AGENTS.md` (and `CLAUDE.md` if there is one) of the project first.
65
+ - The Python environment is `.venv` (`.venv/bin/python`, on Windows
66
+ `.venv\\Scripts\\python.exe`). Add a package to `requirements.txt`, install it with
67
+ `.venv/bin/python -m pip install -r requirements.txt` and list its app in
68
+ `BS_INSTALLED_APPS` of `project.env` when its guide says so.
69
+ - After changing models run `.venv/bin/python manage.py makemigrations`. After every change
70
+ run `run_doctor` and fix the errors and the warnings of the packages you used. Run the
71
+ tests with `.venv/bin/python -m pytest` when PostgreSQL and Redis are available (`.env`);
72
+ if they are not, say so instead of skipping the tests silently.
73
+ - Write tests for the behavior you add (`tests/`), with `bazis_test_utils` as the guides show.
74
+ - Never read, print or copy the values of `.env`. Never run git commands that change the
75
+ repository (commit, push, reset): the user reviews and commits the changes.
76
+ - End with a short summary: what you built or changed, the Bazis packages used, how to run
77
+ it, and what is left for the user (for example the database).
78
+ """
79
+
80
+
81
+ @dataclass
82
+ class Task:
83
+ """
84
+ What the agent is asked to do in a project.
85
+ """
86
+
87
+ prompt: str
88
+ project_dir: Path
89
+ #: the agent may change files and run commands
90
+ writes: bool = True
91
+ model: str = DEFAULT_MODEL
92
+ effort: str = DEFAULT_EFFORT
93
+ max_budget_usd: float | None = None
94
+ #: run shell commands without asking
95
+ yes: bool = False
96
+ extra_rules: list[str] = field(default_factory=list)
97
+
98
+
99
+ def mcp_server(project_dir: Path) -> dict:
100
+ """
101
+ The stdio MCP server bazis-mcp of the project, run with the Python of the CLI (it uses
102
+ `.venv` of the project for the project tools).
103
+ """
104
+ return {
105
+ 'type': 'stdio',
106
+ 'command': sys.executable,
107
+ 'args': ['-m', 'bazis.contrib.mcp.server', '--project-dir', str(project_dir)],
108
+ }
109
+
110
+
111
+ def _path_rule(tool: str, path: Path) -> str:
112
+ """
113
+ A permission rule for an absolute path (`//` starts an absolute path in the rules).
114
+ """
115
+ return f'{tool}(//{path.resolve().as_posix().lstrip("/")})'
116
+
117
+
118
+ def options(task: Task, asker: 'Asker | None' = None) -> ClaudeAgentOptions:
119
+ """
120
+ The options of the agent for a task. Only the MCP server bazis-mcp of the CLI is used
121
+ (`strict_mcp_config`) and no settings of the project or the user are loaded, so the
122
+ permissions below are the only ones.
123
+
124
+ A read-only task can only read the project and call the MCP tools. A task that writes
125
+ gets the edits of files in the project accepted (`acceptEdits`); everything else is
126
+ asked: shell commands through a PreToolUse hook (the CLI would otherwise allow commands
127
+ such as `rm` or `sed` in `acceptEdits` mode), the other tools through `can_use_tool`.
128
+ In both, `.env` cannot be read and git cannot change the repository.
129
+ """
130
+ rules = '\n'.join([RULES, *task.extra_rules])
131
+ secrets = [_path_rule(tool, task.project_dir / '.env') for tool in ('Read', 'Edit')]
132
+ common = dict(
133
+ cwd=str(task.project_dir),
134
+ model=task.model,
135
+ effort=task.effort,
136
+ max_budget_usd=task.max_budget_usd,
137
+ mcp_servers={'bazis': mcp_server(task.project_dir)},
138
+ strict_mcp_config=True,
139
+ setting_sources=[],
140
+ system_prompt={'type': 'preset', 'preset': 'claude_code', 'append': rules},
141
+ allowed_tools=['mcp__bazis'],
142
+ )
143
+ if not task.writes:
144
+ return ClaudeAgentOptions(
145
+ disallowed_tools=[*WRITE_TOOLS, *secrets],
146
+ permission_mode='dontAsk',
147
+ **common,
148
+ )
149
+ asker = asker or Asker(task.yes)
150
+ return ClaudeAgentOptions(
151
+ disallowed_tools=[*GIT_DENY, *secrets],
152
+ permission_mode='acceptEdits',
153
+ can_use_tool=asker.can_use_tool,
154
+ hooks={'PreToolUse': [HookMatcher(matcher='Bash', hooks=[asker.bash_hook])]},
155
+ **common,
156
+ )
157
+
158
+
159
+ class Asker:
160
+ """
161
+ Asks the user in the terminal before the tools that are not allowed in advance: `y`
162
+ allows the call, `a` allows the rest of the run. With `yes` everything is allowed;
163
+ without a terminal everything is denied. Commands that change the git repository are
164
+ always denied.
165
+ """
166
+
167
+ def __init__(self, yes: bool, stdin: TextIO | None = None, out: TextIO | None = None):
168
+ self.allow_all = yes
169
+ self.stdin = stdin or sys.stdin
170
+ self.out = out or sys.stderr
171
+
172
+ def decide(self, tool_name: str, tool_input: dict) -> str | None:
173
+ """
174
+ None if allowed, else the reason of the denial.
175
+ """
176
+ command = tool_input.get('command', '') if tool_name == 'Bash' else ''
177
+ if GIT_COMMAND_RE.search(command):
178
+ return 'The user commits the changes: git commands that change the repository are not allowed.'
179
+ if self.allow_all:
180
+ return None
181
+ if not self.stdin.isatty():
182
+ return 'Not allowed: no terminal to ask the user (run bazis with --yes to allow).'
183
+ details = command or json.dumps(tool_input, ensure_ascii=False, indent=2)
184
+ why = tool_input.get('description')
185
+ self.out.write(f'\n? {tool_name}' + (f' — {why}' if why else '') + '\n')
186
+ self.out.write(''.join(f' {line}\n' for line in details.splitlines()))
187
+ self.out.write(' allow [y/N/a=all]: ')
188
+ self.out.flush()
189
+ answer = self.stdin.readline().strip().lower()
190
+ if answer == 'a':
191
+ self.allow_all = True
192
+ if answer in ('y', 'yes', 'a'):
193
+ return None
194
+ return 'The user did not allow it.'
195
+
196
+ async def can_use_tool(self, tool_name: str, tool_input: dict, context) -> object:
197
+ reason = self.decide(tool_name, tool_input)
198
+ if reason is None:
199
+ return PermissionResultAllow()
200
+ return PermissionResultDeny(message=reason)
201
+
202
+ async def bash_hook(self, hook_input: dict, tool_use_id, context) -> dict:
203
+ reason = self.decide('Bash', hook_input.get('tool_input', {}))
204
+ return {
205
+ 'hookSpecificOutput': {
206
+ 'hookEventName': 'PreToolUse',
207
+ 'permissionDecision': 'allow' if reason is None else 'deny',
208
+ 'permissionDecisionReason': reason or 'Allowed by the user.',
209
+ }
210
+ }
211
+
212
+
213
+ def describe(tool_name: str, tool_input: dict) -> str:
214
+ """
215
+ One line about a tool call, for the progress output.
216
+ """
217
+ for key in ('command', 'file_path', 'path', 'pattern', 'url', 'name'):
218
+ if value := tool_input.get(key):
219
+ lines = str(value).splitlines()
220
+ more = ' ...' if len(lines) > 1 else ''
221
+ return lines[0][:200] + more
222
+ return ''
223
+
224
+
225
+ def render(message, out: TextIO | None = None) -> ResultMessage | None:
226
+ """
227
+ Prints the text of the agent and one line per tool call; returns the final result.
228
+ """
229
+ out = out or sys.stdout
230
+ if isinstance(message, AssistantMessage):
231
+ for block in message.content:
232
+ if isinstance(block, TextBlock) and block.text.strip():
233
+ out.write(block.text.rstrip() + '\n')
234
+ elif isinstance(block, ToolUseBlock):
235
+ name = block.name.removeprefix('mcp__bazis__')
236
+ out.write(f' > {name} {describe(block.name, block.input)}'.rstrip() + '\n')
237
+ out.flush()
238
+ elif isinstance(message, ResultMessage):
239
+ return message
240
+ return None
241
+
242
+
243
+ async def run(task: Task, out: TextIO | None = None) -> ResultMessage | None:
244
+ """
245
+ Runs the task and prints the progress; returns the final result of the agent.
246
+ """
247
+ result = None
248
+ # the read-only MCP tools of bazis-mcp are allowed in advance on purpose
249
+ warnings.filterwarnings('ignore', category=CanUseToolShadowedWarning)
250
+ try:
251
+ async for message in query(prompt=task.prompt, options=options(task)):
252
+ result = render(message, out) or result
253
+ except ResultError:
254
+ # the CLI exits with an error after a failed result (e.g. the budget is spent):
255
+ # the result says why
256
+ if result is None:
257
+ raise
258
+ return result
259
+
260
+
261
+ def error_text(result: ResultMessage) -> str:
262
+ """
263
+ Why a run failed.
264
+ """
265
+ reasons = {
266
+ 'error_max_budget_usd': 'the budget (--budget) is spent',
267
+ 'error_max_turns': 'the agent reached the maximum number of turns',
268
+ }
269
+ return (
270
+ '; '.join(result.errors or [])
271
+ or reasons.get(result.subtype)
272
+ or result.result
273
+ or result.subtype
274
+ )
@@ -0,0 +1,32 @@
1
+ # What the package is for and how to use it, for tools and AI agents (see AGENTS.md).
2
+ # `import` paths and `check` ids are verified by tests/test_cli.py.
3
+
4
+ [package]
5
+ name = "bazis-cli"
6
+ summary = "the bazis command: creates, extends and audits Bazis projects with Claude"
7
+ solves = [
8
+ "create a Bazis project from a description of what it does",
9
+ "add a Bazis package to a project and set it up",
10
+ "review a project against the guides and checks of its Bazis packages",
11
+ ]
12
+ requires = ["bazis-mcp"]
13
+ pairs_well = []
14
+
15
+ [[extension_points]]
16
+ name = "bazis"
17
+ import = "bazis.contrib.cli.main.main"
18
+ use_when = "the command line entry point"
19
+
20
+ [[extension_points]]
21
+ name = "Task"
22
+ import = "bazis.contrib.cli.agent.Task"
23
+ use_when = "run the agent of a command from Python (bazis.contrib.cli.agent.run)"
24
+
25
+ [[pitfalls]]
26
+ text = "the agent spends API credits of the account of Claude Code; limit a run with --budget"
27
+
28
+ [[pitfalls]]
29
+ text = "the system checks run the project code with its .venv, also in a read-only audit: use bazis in trusted projects"
30
+
31
+ [[pitfalls]]
32
+ text = "the tests of a new project need PostgreSQL with PostGIS and Redis (.env); without them the agent only checks and migrates"
@@ -0,0 +1,180 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ The `bazis` command:
17
+
18
+ - `bazis new DIR "what the project does"` creates a project and builds it;
19
+ - `bazis add PACKAGE [what for]` adds a Bazis package to the project;
20
+ - `bazis audit [--fix]` reviews the project against the guides and checks of its packages.
21
+ """
22
+
23
+ import argparse
24
+ import asyncio
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ from claude_agent_sdk import ClaudeSDKError, CLIConnectionError
29
+
30
+ from . import __version__, agent, scaffold
31
+
32
+
33
+ def new_task(args) -> agent.Task:
34
+ directory = args.directory.resolve()
35
+ name = args.name or scaffold.package_name(directory)
36
+ scaffold.write_files(directory, name)
37
+ print(f'Created the project {name} in {directory}')
38
+ if not args.no_venv:
39
+ print('Installing the dependencies into .venv ...')
40
+ try:
41
+ scaffold.create_venv(directory)
42
+ except scaffold.ScaffoldError as err:
43
+ raise scaffold.ScaffoldError(
44
+ f'{err}\nThe files of the project are written: create .venv and install '
45
+ f'requirements-dev.txt by hand, or delete {directory} and run bazis new again.'
46
+ ) from err
47
+ prompt = f"""\
48
+ Build this Bazis project: {args.description}
49
+
50
+ The skeleton is ready: the project package `{name}` (settings, root router, ASGI app in
51
+ `{name}/main.py`), `project.env`, `.env`, `requirements.txt`, `tests/`, and `.venv` with
52
+ Bazis installed. Read the guide of the core (`package_guide("bazis")`), choose the Bazis
53
+ packages the project needs with `list_packages`, read their guides, then create the apps
54
+ with their models, routes and tests, and register the routers in `{name}/router.py`.
55
+ """
56
+ return _task(args, prompt, directory)
57
+
58
+
59
+ def add_task(args) -> agent.Task:
60
+ goal = f' The project needs it for: {args.goal}' if args.goal else ''
61
+ prompt = f"""\
62
+ Add the Bazis package {args.package} to this project and use it.{goal}
63
+
64
+ Read its guide with `package_guide("{args.package}")` and the guides of the Bazis packages
65
+ it requires that are not installed (`list_packages`), install them, follow the setup of the
66
+ guides, change the models and routes they concern, create the migrations, add tests, and
67
+ make `run_doctor` pass without the warnings of the new packages.
68
+ """
69
+ return _task(args, prompt, args.project_dir.resolve())
70
+
71
+
72
+ def audit_task(args) -> agent.Task:
73
+ if args.fix:
74
+ action = 'Then fix the problems you are sure about and run the checks and the tests again.'
75
+ else:
76
+ action = 'Change nothing: this is a review.'
77
+ prompt = f"""\
78
+ Audit this Bazis project.
79
+
80
+ 1. Run `run_doctor` with `deploy=true` and `project_info`.
81
+ 2. For every installed Bazis package read `package_guide` and compare the project with its
82
+ rules and pitfalls: routes that bypass permissions or the update checks, settings that
83
+ are unsafe in production, packages that are installed but not used, needs that a Bazis
84
+ package solves but the project implements by hand, missing tests and migrations.
85
+ 3. Report the problems ordered by severity, each with the file, the reason and the fix.
86
+
87
+ {action}
88
+ """
89
+ return _task(args, prompt, args.project_dir.resolve(), writes=args.fix)
90
+
91
+
92
+ def _task(args, prompt: str, project_dir: Path, writes: bool = True) -> agent.Task:
93
+ return agent.Task(
94
+ prompt=prompt,
95
+ project_dir=project_dir,
96
+ writes=writes,
97
+ model=args.model,
98
+ effort=args.effort,
99
+ max_budget_usd=args.budget,
100
+ yes=args.yes,
101
+ )
102
+
103
+
104
+ def parser() -> argparse.ArgumentParser:
105
+ common = argparse.ArgumentParser(add_help=False)
106
+ common.add_argument('--model', default=agent.DEFAULT_MODEL, help='Default: %(default)s.')
107
+ common.add_argument(
108
+ '--effort', default=agent.DEFAULT_EFFORT,
109
+ choices=['low', 'medium', 'high', 'xhigh', 'max'], help='Default: %(default)s.',
110
+ )
111
+ common.add_argument('--budget', type=float, help='Stop after spending this many USD.')
112
+ common.add_argument(
113
+ '--yes', '-y', action='store_true',
114
+ help='Allow shell commands and the other tools without asking (edits of the files of '
115
+ 'the project are always allowed; git cannot commit, push or reset).',
116
+ )
117
+
118
+ project = argparse.ArgumentParser(add_help=False)
119
+ project.add_argument(
120
+ '--project-dir', type=Path, default=Path.cwd(),
121
+ help='The directory of manage.py (default: the current directory).',
122
+ )
123
+
124
+ root = argparse.ArgumentParser(
125
+ prog='bazis', description='Builds and audits Bazis projects with Claude.'
126
+ )
127
+ root.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
128
+ commands = root.add_subparsers(dest='command', required=True)
129
+
130
+ new = commands.add_parser('new', parents=[common], help='Create and build a project.')
131
+ new.add_argument('directory', type=Path, help='A new or empty directory.')
132
+ new.add_argument('description', help='What the project does, in your words.')
133
+ new.add_argument('--name', help='The project package (default: from the directory name).')
134
+ new.add_argument('--no-venv', action='store_true', help='Do not create .venv.')
135
+ new.set_defaults(make_task=new_task)
136
+
137
+ add = commands.add_parser('add', parents=[common, project], help='Add a Bazis package.')
138
+ add.add_argument('package', help='The package, e.g. bazis-permit.')
139
+ add.add_argument('goal', nargs='?', help='What the project needs it for.')
140
+ add.set_defaults(make_task=add_task)
141
+
142
+ audit = commands.add_parser('audit', parents=[common, project], help='Review the project.')
143
+ audit.add_argument('--fix', action='store_true', help='Also fix the problems.')
144
+ audit.set_defaults(make_task=audit_task)
145
+ return root
146
+
147
+
148
+ def main(argv: list[str] | None = None) -> int:
149
+ args = parser().parse_args(argv)
150
+ try:
151
+ task = args.make_task(args)
152
+ except scaffold.ScaffoldError as err:
153
+ print(f'bazis: {err}', file=sys.stderr)
154
+ return 1
155
+ try:
156
+ result = asyncio.run(agent.run(task))
157
+ except ClaudeSDKError as err:
158
+ print(f'bazis: {err}', file=sys.stderr)
159
+ if isinstance(err, CLIConnectionError) or 'auth' in str(err).lower():
160
+ print(
161
+ 'The agent runs Claude Code: sign in with `claude login` or set '
162
+ 'ANTHROPIC_API_KEY.',
163
+ file=sys.stderr,
164
+ )
165
+ return 1
166
+ except KeyboardInterrupt:
167
+ return 130
168
+ if result is None:
169
+ print('bazis: the agent ended without a result', file=sys.stderr)
170
+ return 1
171
+ cost = f', ${result.total_cost_usd:.2f}' if result.total_cost_usd is not None else ''
172
+ print(f'\n[{result.num_turns} turns{cost}]', file=sys.stderr)
173
+ if result.is_error:
174
+ print(f'bazis: {agent.error_text(result)}', file=sys.stderr)
175
+ return 1
176
+ return 0
177
+
178
+
179
+ if __name__ == '__main__':
180
+ sys.exit(main())
@@ -0,0 +1,273 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ The skeleton of a new Bazis project: the files every project has, written without the
17
+ model, and its virtual environment. The agent then adds the apps the project needs.
18
+ """
19
+
20
+ import importlib.util
21
+ import json
22
+ import keyword
23
+ import re
24
+ import secrets
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+
31
+ #: the packages of every project; the agent adds the Bazis packages the project needs
32
+ REQUIREMENTS = ['bazis>=2.4.1']
33
+ REQUIREMENTS_DEV = ['bazis-mcp>=2.4.1', 'bazis-test-utils>=2.4.0']
34
+
35
+
36
+ MANAGE_PY = '''#!/usr/bin/env python
37
+ import os
38
+ import sys
39
+
40
+
41
+ def main():
42
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{name}.settings')
43
+ from django.core.management import execute_from_command_line
44
+
45
+ execute_from_command_line(sys.argv)
46
+
47
+
48
+ if __name__ == '__main__':
49
+ main()
50
+ '''
51
+
52
+ SETTINGS_PY = '''# Settings are environment variables with the BS_ prefix (project.env, .env):
53
+ # bazis.core.configure builds the Django settings from them.
54
+ import bazis.core.configure # noqa: F401
55
+ '''
56
+
57
+ MAIN_PY = '''import os
58
+
59
+
60
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{name}.settings')
61
+
62
+ from bazis.core.app import app # noqa: E402, F401
63
+ '''
64
+
65
+ ROUTER_PY = '''from bazis.core.routing import BazisRouter
66
+
67
+
68
+ router = BazisRouter(prefix='/api/v1')
69
+ # register the routers of the apps: router.register('<app>.router')
70
+ '''
71
+
72
+ URLS_PY = '''from django.conf import settings
73
+ from django.contrib import admin
74
+ from django.urls import path, re_path
75
+ from django.views.static import serve
76
+
77
+
78
+ urlpatterns = [
79
+ path('admin/', admin.site.urls),
80
+ ]
81
+
82
+ if settings.DEBUG:
83
+ urlpatterns += [
84
+ re_path(r'^media/(?P<path>.*)$', serve, dict(document_root=settings.MEDIA_ROOT)),
85
+ re_path(r'^static/(?P<path>.*)$', serve, dict(document_root=settings.STATIC_ROOT)),
86
+ ]
87
+ '''
88
+
89
+ WSGI_PY = '''import os
90
+
91
+ from django.core.wsgi import get_wsgi_application
92
+
93
+
94
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{name}.settings')
95
+
96
+ application = get_wsgi_application()
97
+ '''
98
+
99
+ TEMPLATES = [
100
+ {
101
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
102
+ 'DIRS': ['__BASE_DIR__/templates'],
103
+ 'APP_DIRS': True,
104
+ 'OPTIONS': {
105
+ 'context_processors': [
106
+ 'django.template.context_processors.debug',
107
+ 'django.template.context_processors.request',
108
+ 'django.contrib.auth.context_processors.auth',
109
+ 'django.contrib.messages.context_processors.messages',
110
+ ]
111
+ },
112
+ }
113
+ ]
114
+
115
+ PROJECT_ENV = '''# Settings of the project shared by all environments (secrets go to .env)
116
+ BS_INSTALLED_APPS='[]'
117
+ BS_ROOT_URLCONF={name}.urls
118
+ BS_BAZIS_ROUTER_MODULE={name}.router
119
+ BS_WSGI_APPLICATION={name}.wsgi.application
120
+ BS_DEFAULT_AUTO_FIELD=django.db.models.BigAutoField
121
+ BS_LANGUAGE_CODE=en
122
+ BS_TEMPLATES='{templates}'
123
+ '''
124
+
125
+ DOT_ENV = '''# Local settings and secrets of this environment; not committed
126
+ BS_DEBUG=true
127
+ BS_SECRET_KEY={secret_key}
128
+ BS_DATABASES__DEFAULT__HOST=localhost
129
+ BS_DATABASES__DEFAULT__PORT=5432
130
+ BS_DATABASES__DEFAULT__NAME={name}
131
+ BS_DATABASES__DEFAULT__USER=postgres
132
+ BS_DATABASES__DEFAULT__PASSWORD=postgres
133
+ BS_CACHES__DEFAULT__LOCATION=redis://localhost:6379/0
134
+ '''
135
+
136
+ GITIGNORE = '''.env
137
+ .venv/
138
+ __pycache__/
139
+ *.pyc
140
+ .pytest_cache/
141
+ media/
142
+ static/
143
+ '''
144
+
145
+ PYTEST_INI = '''[pytest]
146
+ DJANGO_SETTINGS_MODULE = {name}.settings
147
+ '''
148
+
149
+ CONFTEST_PY = '''import pytest
150
+
151
+
152
+ @pytest.fixture
153
+ def app():
154
+ from {name}.main import app
155
+
156
+ return app
157
+ '''
158
+
159
+ WINDOWS = sys.platform == 'win32'
160
+ VENV_BIN = '.venv/Scripts' if WINDOWS else '.venv/bin'
161
+ EXE = '.exe' if WINDOWS else ''
162
+
163
+ AGENTS_MD = '''# {name}
164
+
165
+ A [Bazis](https://github.com/ecofuture-tech/bazis) project: JSON:API services on Django,
166
+ FastAPI and Pydantic.
167
+
168
+ - The Python environment is `.venv`; the dependencies are in `requirements.txt` and
169
+ `requirements-dev.txt` (install them with `.venv/bin/python -m pip install -r ...`).
170
+ - Settings are `BS_*` variables: shared ones in `project.env`, local ones and secrets in
171
+ `.env` (not committed). Apps, including Bazis packages, are listed in `BS_INSTALLED_APPS`.
172
+ - The MCP server `bazis` (`.mcp.json`) gives the catalog and the guides of the Bazis
173
+ packages and the facts and checks of this project. Read the guide of a package before
174
+ using it, and run `python manage.py bazis_doctor` and the tests after every change.
175
+ - PostgreSQL with PostGIS and Redis are required to run the project and the tests
176
+ (`.env`); the checks and `makemigrations` work without them.
177
+ '''
178
+
179
+
180
+ class ScaffoldError(Exception):
181
+ pass
182
+
183
+
184
+ def package_name(directory: Path) -> str:
185
+ """
186
+ The name of the project package: the directory name as a Python identifier that does
187
+ not hide a module (`django`, `json`) or the tests.
188
+ """
189
+ name = re.sub(r'\W+', '_', directory.name.lower()).strip('_')
190
+ if not name or name[0].isdigit():
191
+ name = f'project_{name}'.rstrip('_')
192
+ if not valid_package_name(name):
193
+ name = f'{name}_project'
194
+ return name
195
+
196
+
197
+ def valid_package_name(name: str) -> bool:
198
+ return (
199
+ name.isidentifier()
200
+ and not keyword.iskeyword(name)
201
+ and name not in ('tests', 'test')
202
+ and importlib.util.find_spec(name) is None
203
+ )
204
+
205
+
206
+ def write_files(directory: Path, name: str) -> list[Path]:
207
+ """
208
+ Writes the skeleton of the project. Fails if the directory has files already.
209
+ """
210
+ if directory.exists() and any(directory.iterdir()):
211
+ raise ScaffoldError(f'{directory} is not empty')
212
+ if not valid_package_name(name):
213
+ raise ScaffoldError(
214
+ f'{name!r} cannot be the project package: it must be an identifier that is not '
215
+ 'a keyword, "tests" or the name of an installed module'
216
+ )
217
+ files = {
218
+ 'manage.py': MANAGE_PY.format(name=name),
219
+ f'{name}/__init__.py': '',
220
+ f'{name}/settings.py': SETTINGS_PY,
221
+ f'{name}/main.py': MAIN_PY.format(name=name),
222
+ f'{name}/router.py': ROUTER_PY,
223
+ f'{name}/urls.py': URLS_PY,
224
+ f'{name}/wsgi.py': WSGI_PY.format(name=name),
225
+ 'project.env': PROJECT_ENV.format(name=name, templates=json.dumps(TEMPLATES)),
226
+ '.env': DOT_ENV.format(name=name, secret_key=secrets.token_urlsafe(48)),
227
+ '.gitignore': GITIGNORE,
228
+ 'requirements.txt': '\n'.join(REQUIREMENTS) + '\n',
229
+ 'requirements-dev.txt': '-r requirements.txt\n' + '\n'.join(REQUIREMENTS_DEV) + '\n',
230
+ 'pytest.ini': PYTEST_INI.format(name=name),
231
+ 'tests/__init__.py': '',
232
+ 'tests/conftest.py': CONFTEST_PY.format(name=name),
233
+ '.mcp.json': json.dumps(
234
+ {'mcpServers': {'bazis': {'command': f'{VENV_BIN}/bazis-mcp{EXE}', 'args': []}}},
235
+ indent=2,
236
+ ) + '\n',
237
+ 'AGENTS.md': AGENTS_MD.format(name=name),
238
+ }
239
+ written = []
240
+ for relative, content in files.items():
241
+ path = directory / relative
242
+ path.parent.mkdir(parents=True, exist_ok=True)
243
+ path.write_text(content, encoding='utf-8')
244
+ written.append(path)
245
+ return written
246
+
247
+
248
+ def create_venv(directory: Path, run=subprocess.run) -> Path:
249
+ """
250
+ Creates `.venv` with the dependencies of the project (with uv if it is installed) and
251
+ returns its Python.
252
+ """
253
+ venv = directory / '.venv'
254
+ python = venv / ('Scripts/python.exe' if sys.platform == 'win32' else 'bin/python')
255
+ uv = shutil.which('uv')
256
+ if uv:
257
+ commands = [
258
+ # --seed installs pip, which the agent uses to add packages
259
+ [uv, 'venv', '--quiet', '--seed', '--python', sys.executable, str(venv)],
260
+ [uv, 'pip', 'install', '--quiet', '--python', str(python),
261
+ '-r', str(directory / 'requirements-dev.txt')],
262
+ ]
263
+ else:
264
+ commands = [
265
+ [sys.executable, '-m', 'venv', str(venv)],
266
+ [str(python), '-m', 'pip', 'install', '--quiet',
267
+ '-r', str(directory / 'requirements-dev.txt')],
268
+ ]
269
+ for command in commands:
270
+ done = run(command, cwd=directory, capture_output=True, text=True)
271
+ if done.returncode:
272
+ raise ScaffoldError(f'{" ".join(command)} failed:\n{done.stderr.strip()[-2000:]}')
273
+ return python
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: bazis-cli
3
+ Version: 2.4.0
4
+ Summary: The bazis command: creates, extends and audits Bazis projects with Claude (Claude Agent SDK).
5
+ Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
+ Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Project-URL: Home, https://github.com/ecofuture-tech/bazis-cli
9
+ Keywords: bazis,django,fastapi,pydantic,framework,jsonapi,cli,ai,agents,claude
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Framework :: Django
18
+ Classifier: Framework :: FastAPI
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: bazis-mcp>=2.4.1
22
+ Requires-Dist: claude-agent-sdk>=0.2.163
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=8; extra == "test"
25
+ Provides-Extra: dev
26
+ Requires-Dist: ruff; extra == "dev"
27
+
28
+ # bazis-cli
29
+
30
+ The `bazis` command creates, extends and audits [Bazis](https://github.com/ecofuture-tech/bazis)
31
+ projects with Claude. It runs Claude Code (the [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk))
32
+ in the project with the MCP server [bazis-mcp](https://github.com/ecofuture-tech/bazis-mcp),
33
+ so the agent chooses the Bazis packages from their catalog, follows their guides and checks
34
+ the result with the system checks of the project instead of relying on its memory.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install bazis-cli # or: uv tool install bazis-cli
40
+ claude login # or set ANTHROPIC_API_KEY
41
+ ```
42
+
43
+ The agent uses the account of Claude Code and spends its credits; limit a run with
44
+ `--budget`.
45
+
46
+ ## Commands
47
+
48
+ ```bash
49
+ # a new project from a description
50
+ bazis new shop "An online shop: products with categories, orders of users, \
51
+ users see only their own orders"
52
+
53
+ # a Bazis package for an existing project (run in the directory of manage.py)
54
+ bazis add bazis-statusy "orders go through new -> paid -> shipped"
55
+
56
+ # a review against the guides and checks of the packages; --fix also fixes
57
+ bazis audit
58
+ bazis audit --fix
59
+ ```
60
+
61
+ - `bazis new DIR DESCRIPTION` writes the skeleton of a project: the project package
62
+ (settings, root router, ASGI app), `project.env`, `.env` with a generated secret key,
63
+ `requirements.txt`, `requirements-dev.txt`, `tests/`, `.mcp.json` and `AGENTS.md`. It
64
+ creates `.venv` with Bazis and bazis-mcp (with uv if it is installed; `--no-venv` skips
65
+ it). Then the agent chooses the Bazis packages, creates the apps, models, routes,
66
+ migrations and tests, and runs the checks.
67
+ - `bazis add PACKAGE [GOAL]` installs a Bazis package with the Bazis packages it requires
68
+ and sets them up as their guides say.
69
+ - `bazis audit [--fix]` reports the problems of the project by severity. Without `--fix`
70
+ the agent can only read.
71
+
72
+ Options of every command: `--project-dir` (`add`, `audit`; default: the current
73
+ directory), `--model` (default `claude-opus-5-5`), `--effort` (`low` ... `max`, default
74
+ `high`), `--budget USD`, `--yes`.
75
+
76
+ ## Permissions
77
+
78
+ - The agent edits the files of the project directory without asking.
79
+ - Everything else is shown in full and asked in the terminal: every shell command
80
+ (installing packages, migrations, tests), edits and reads outside the project, web access.
81
+ `y` allows it, `a` allows the rest of the run. `--yes` allows all of it; without a
82
+ terminal and without `--yes` it is denied.
83
+ - The agent cannot read `.env` and cannot commit, push or reset with git: review the changes
84
+ and commit them yourself.
85
+ - Only the settings of bazis-cli apply: the Claude Code settings, hooks and MCP servers of
86
+ the project and of the user are not loaded.
87
+ - `bazis audit` without `--fix` can only read the project and call bazis-mcp. bazis-mcp
88
+ runs the system checks of the project, that is the project code with its `.venv`: run
89
+ the commands only in projects you trust.
90
+
91
+ The tests of a project need PostgreSQL with PostGIS and Redis (settings in `.env`); without
92
+ them the agent creates the migrations and runs the system checks, and says that the tests
93
+ did not run.
@@ -0,0 +1,12 @@
1
+ bazis/contrib/cli/AGENTS.md,sha256=mgBQ_Sr0GqLMoY3WVqGLYgc7XGl1K1B5_k9Qk5QpsKE,1802
2
+ bazis/contrib/cli/__init__.py,sha256=OYHBOe-gKbJL9WElLNLkYKhNZB6Sa2G19oNVnjBo-T4,777
3
+ bazis/contrib/cli/_version.py,sha256=9x8v3jtROsCpS8s3YKlkPGuPEcXwPyPiZdq4RuGDXMo,528
4
+ bazis/contrib/cli/agent.py,sha256=hUbqyhVOZkF65TJGaVtj9jlRsu5n_S7z1HAIxiydt8M,10851
5
+ bazis/contrib/cli/bazis_manifest.toml,sha256=4iZKcXoy_AQpyafGgHeEERPyeC4gsY9lWxFMgjPTZiw,1184
6
+ bazis/contrib/cli/main.py,sha256=TmT1Lrt2vhdIa2DvAgkDYmK5LAIW5Jiqoz9N2t9SNUs,7391
7
+ bazis/contrib/cli/scaffold.py,sha256=VBGQkH86W5os-67hUB5l_Gn0Xw6DbNCwlT9HTayzuiQ,8708
8
+ bazis_cli-2.4.0.dist-info/METADATA,sha256=gaikRaAC6qdYo3I7q8d1R6-K7BC39igFNJgb1ZajlEc,4259
9
+ bazis_cli-2.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ bazis_cli-2.4.0.dist-info/entry_points.txt,sha256=M5dr9d744jnX9cWly8Vsrzvoxb46kDpiaokM8VmACdU,54
11
+ bazis_cli-2.4.0.dist-info/top_level.txt,sha256=WgdrPZTZBMG8i_EqxA3vU5qI4ETQ_RsqKqSqsfIApHY,6
12
+ bazis_cli-2.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bazis = bazis.contrib.cli.main:main
@@ -0,0 +1 @@
1
+ bazis