axor-claude 0.1.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 (31) hide show
  1. axor_claude-0.1.0/.github/workflows/ci.yml +120 -0
  2. axor_claude-0.1.0/.gitignore +12 -0
  3. axor_claude-0.1.0/CHANGELOG.md +17 -0
  4. axor_claude-0.1.0/CONTRIBUTING.md +56 -0
  5. axor_claude-0.1.0/LICENSE +21 -0
  6. axor_claude-0.1.0/PKG-INFO +400 -0
  7. axor_claude-0.1.0/README.md +375 -0
  8. axor_claude-0.1.0/axor_claude/__init__.py +81 -0
  9. axor_claude-0.1.0/axor_claude/events.py +215 -0
  10. axor_claude-0.1.0/axor_claude/executor.py +349 -0
  11. axor_claude-0.1.0/axor_claude/extensions/__init__.py +0 -0
  12. axor_claude-0.1.0/axor_claude/extensions/plugin_loader.py +155 -0
  13. axor_claude-0.1.0/axor_claude/extensions/skill_loader.py +88 -0
  14. axor_claude-0.1.0/axor_claude/normalizer.py +111 -0
  15. axor_claude-0.1.0/axor_claude/tool_definitions.py +204 -0
  16. axor_claude-0.1.0/axor_claude/tools/__init__.py +0 -0
  17. axor_claude-0.1.0/axor_claude/tools/bash.py +123 -0
  18. axor_claude-0.1.0/axor_claude/tools/glob.py +102 -0
  19. axor_claude-0.1.0/axor_claude/tools/read.py +99 -0
  20. axor_claude-0.1.0/axor_claude/tools/search.py +187 -0
  21. axor_claude-0.1.0/axor_claude/tools/write.py +81 -0
  22. axor_claude-0.1.0/pyproject.toml +51 -0
  23. axor_claude-0.1.0/tests/__init__.py +0 -0
  24. axor_claude-0.1.0/tests/conftest.py +43 -0
  25. axor_claude-0.1.0/tests/integration/__init__.py +0 -0
  26. axor_claude-0.1.0/tests/integration/test_integration.py +103 -0
  27. axor_claude-0.1.0/tests/unit/__init__.py +0 -0
  28. axor_claude-0.1.0/tests/unit/test_events.py +260 -0
  29. axor_claude-0.1.0/tests/unit/test_normalizer_and_defs.py +166 -0
  30. axor_claude-0.1.0/tests/unit/tools/__init__.py +0 -0
  31. axor_claude-0.1.0/tests/unit/tools/test_tools.py +324 -0
@@ -0,0 +1,120 @@
1
+ name: CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*.*.*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ test:
12
+ name: Test (Python ${{ matrix.python-version }})
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.11", "3.12"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Checkout axor-core
22
+ uses: actions/checkout@v4
23
+ with:
24
+ repository: ${{ github.repository_owner }}/axor-core
25
+ path: axor-core
26
+
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ cache: pip
31
+
32
+ - name: Install
33
+ run: |
34
+ pip install -e axor-core/
35
+ pip install -e ".[dev]"
36
+
37
+ - name: Static analysis — no governance in adapter
38
+ run: |
39
+ python - << 'EOF'
40
+ import re, os, sys
41
+ gov_patterns = [r'\bPolicySelector\b', r'\bCapabilityResolver\b', r'\bIntentLoop\b']
42
+ anthropic_allowed = {"axor_claude/executor.py", "axor_claude/events.py"}
43
+ issues = []
44
+ for root, dirs, files in os.walk("axor_claude"):
45
+ dirs[:] = [d for d in dirs if not d.startswith("__")]
46
+ for f in files:
47
+ if not f.endswith(".py"):
48
+ continue
49
+ path = os.path.join(root, f)
50
+ src = open(path).read()
51
+ rel = path.replace("\\", "/")
52
+ for line in src.splitlines():
53
+ stripped = line.strip()
54
+ if stripped.startswith("#"):
55
+ continue
56
+ for pat in gov_patterns:
57
+ if re.search(pat, stripped):
58
+ issues.append(f"GOV_LOGIC: {rel}: {stripped[:60]}")
59
+ if rel not in anthropic_allowed:
60
+ for line in src.splitlines():
61
+ if re.match(r"\s*(import anthropic|from anthropic)", line):
62
+ issues.append(f"ANTHROPIC_OUTSIDE_EXECUTOR: {rel}")
63
+ if issues:
64
+ print("\n".join(issues))
65
+ sys.exit(1)
66
+ print("✓ adapter architecture clean")
67
+ EOF
68
+
69
+ - name: Run unit tests
70
+ run: pytest tests/unit/ -v --tb=short
71
+
72
+ publish:
73
+ name: Publish to PyPI
74
+ needs: test
75
+ runs-on: ubuntu-latest
76
+ if: startsWith(github.ref, 'refs/tags/v')
77
+ environment: pypi
78
+
79
+ permissions:
80
+ id-token: write
81
+
82
+ steps:
83
+ - uses: actions/checkout@v4
84
+
85
+ - uses: actions/setup-python@v5
86
+ with:
87
+ python-version: "3.12"
88
+
89
+ - name: Verify tag matches package version
90
+ run: |
91
+ python - << 'EOF'
92
+ import pathlib
93
+ import re
94
+ import sys
95
+ import tomllib
96
+
97
+ ref = "${{ github.ref_name }}"
98
+ m = re.fullmatch(r"v(\d+\.\d+\.\d+)", ref)
99
+ if not m:
100
+ print(f"Tag {ref!r} must match vX.Y.Z")
101
+ sys.exit(1)
102
+
103
+ tag_version = m.group(1)
104
+ data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
105
+ pkg_version = data["project"]["version"]
106
+
107
+ if tag_version != pkg_version:
108
+ print(f"Version mismatch: tag={tag_version}, pyproject={pkg_version}")
109
+ sys.exit(1)
110
+
111
+ print(f"Version check passed: {pkg_version}")
112
+ EOF
113
+
114
+ - name: Build
115
+ run: |
116
+ pip install hatchling build
117
+ python -m build
118
+
119
+ - name: Publish to PyPI
120
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .env
8
+ *.egg
9
+ .pytest_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .axor/
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2025-04-12
4
+
5
+ Initial release.
6
+
7
+ ### Added
8
+ - Core governance kernel (axor-core)
9
+ - Claude Code adapter (axor-claude)
10
+ - CLI with interactive REPL (axor-cli)
11
+ - Dynamic policy selection (7-policy matrix)
12
+ - ContextManager with 11 waste categories
13
+ - ToolResultBus for async tool loop
14
+ - Federation via spawn_child with child_executor
15
+ - CancelToken cooperative cancellation
16
+ - BudgetPolicyEngine (60/80/90/95% thresholds)
17
+ - TraceCollector with lineage (17 event kinds)
@@ -0,0 +1,56 @@
1
+ # Contributing to axor-claude
2
+
3
+ ## Architecture principles
4
+
5
+ axor-claude is an **adapter** — it translates between axor-core contracts
6
+ and the Anthropic SDK. It must not contain governance logic.
7
+
8
+ Key rules:
9
+ - `axor_claude/` never defines policy semantics
10
+ - `axor_claude/` never imports axor-core internals beyond `contracts/`
11
+ and `capability.executor.ToolHandler`
12
+ - Tool handlers do only I/O — no business logic
13
+ - `StreamNormalizer` is the only place that knows Anthropic SDK event structure
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ git clone https://github.com/your-org/axor-claude
19
+ cd axor-claude
20
+ python -m venv .venv && source .venv/bin/activate
21
+ pip install -e ".[dev]"
22
+ pip install axor-core # or: pip install -e ../axor-core
23
+ ```
24
+
25
+ ## Running tests
26
+
27
+ ```bash
28
+ pytest tests/unit/ # no API key needed
29
+ pytest tests/integration/ -m integration # requires ANTHROPIC_API_KEY
30
+ ```
31
+
32
+ ## Adding a tool
33
+
34
+ 1. Create `axor_claude/tools/mytool.py` extending `ToolHandler`
35
+ 2. Add Anthropic tool definition to `tool_definitions.py`
36
+ 3. Register in `__init__.py` and `make_session()`
37
+ 4. Add unit tests in `tests/unit/tools/test_mytool.py`
38
+
39
+ Tool handlers must:
40
+ - Be async
41
+ - Raise `ValueError` for missing required args
42
+ - Never catch all exceptions — let callers handle
43
+
44
+ ## Adding an extension loader
45
+
46
+ Implement `ExtensionLoader.load() -> ExtensionBundle`.
47
+ Return `ExtensionFragment` objects — never raw file contents
48
+ without going through the loader abstraction.
49
+
50
+ ## Pull request checklist
51
+
52
+ - [ ] No governance logic in adapter code
53
+ - [ ] No Anthropic SDK imports outside `executor.py` and `events.py`
54
+ - [ ] Unit tests don't require `ANTHROPIC_API_KEY`
55
+ - [ ] Tool handlers raise `ValueError` for empty/missing args
56
+ - [ ] `PYTHONPATH=../axor-core python -c "from axor_claude import make_session"` passes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Axor Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,400 @@
1
+ Metadata-Version: 2.4
2
+ Name: axor-claude
3
+ Version: 0.1.0
4
+ Summary: Claude Code adapter for axor-core governance kernel
5
+ Project-URL: Repository, https://github.com/Bucha11/axor-claude
6
+ Project-URL: Bug Tracker, https://github.com/Bucha11/axor-claude/issues
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: agents,anthropic,axor,claude,claude-code,governance
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: anthropic>=0.40.0
19
+ Requires-Dist: axor-core>=0.1.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # axor-claude
27
+
28
+ [![CI](https://github.com/Bucha11/axor-claude/actions/workflows/ci.yml/badge.svg)](https://github.com/Bucha11/axor-claude/actions/workflows/ci.yml)
29
+ [![PyPI](https://img.shields.io/pypi/v/axor-claude)](https://pypi.org/project/axor-claude/)
30
+ [![Python](https://img.shields.io/pypi/pyversions/axor-claude)](https://pypi.org/project/axor-claude/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
32
+
33
+
34
+ **Claude Code adapter for [axor-core](https://github.com/Bucha11/axor-core).**
35
+
36
+ Wraps Claude via the Anthropic SDK and runs it as a governed agent under axor-core's governance kernel — controlled context, explicit tool permissions, token optimization, and full audit trail.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install axor-claude
44
+ ```
45
+
46
+ Requires an [Anthropic API key](https://console.anthropic.com/).
47
+
48
+ ---
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ import asyncio
54
+ import axor_claude
55
+
56
+ async def main():
57
+ session = axor_claude.make_session(
58
+ api_key="sk-ant-...", # or set ANTHROPIC_API_KEY env var
59
+ )
60
+ result = await session.run("refactor the auth module to add rate limiting")
61
+ print(result.output)
62
+ print(f"policy: {result.metadata['policy']}") # e.g. moderate_mutative
63
+ print(f"tokens: {result.token_usage.total}")
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ No API key in code:
69
+
70
+ ```bash
71
+ export ANTHROPIC_API_KEY=sk-ant-...
72
+ python your_script.py
73
+ ```
74
+
75
+ ---
76
+
77
+ ## What axor-claude provides
78
+
79
+ | Component | What it does |
80
+ |-----------|-------------|
81
+ | `ClaudeCodeExecutor` | Streams Claude API responses, drives multi-turn tool loop via `ToolResultBus` |
82
+ | `ReadHandler` | Read files — line ranges, encoding fallback (utf-8 → latin-1), 1MB cap |
83
+ | `WriteHandler` | Atomic writes (tmpfile → rename), append mode, creates parent dirs |
84
+ | `BashHandler` | Async subprocess — timeout, process group SIGTERM, 512KB output cap |
85
+ | `SearchHandler` | ripgrep if available, pure-Python fallback, regex, context lines |
86
+ | `GlobHandler` | File pattern matching with `**` support, smart ignore list |
87
+ | `ClaudeSkillLoader` | Loads `CLAUDE.md` and `.claude/skills/*.md` as context fragments |
88
+ | `ClaudePluginLoader` | Loads `.claude/plugins/*/plugin.json` — tools, commands, hooks |
89
+ | `normalizer` | Extracts token usage, stop reason, tool calls from API responses |
90
+
91
+ ---
92
+
93
+ ## Configuration
94
+
95
+ ### `make_session()` — all options
96
+
97
+ ```python
98
+ session = axor_claude.make_session(
99
+ api_key="sk-ant-...", # None → reads ANTHROPIC_API_KEY
100
+ model="claude-sonnet-4-5", # default model
101
+ system_prompt="You are...", # override default system prompt
102
+ tools=("read", "write", "bash", "search", "glob"), # default: all
103
+ load_skills=True, # load CLAUDE.md + .claude/skills/
104
+ load_plugins=True, # load .claude/plugins/
105
+ soft_token_limit=100_000, # budget optimization signals
106
+ trace_config=TraceConfig(...), # privacy / persistence settings
107
+ )
108
+ ```
109
+
110
+ ### Tools
111
+
112
+ Register only what you need — policy enforces what Claude can actually use:
113
+
114
+ ```python
115
+ from axor_core import GovernedSession, CapabilityExecutor
116
+ from axor_claude import ClaudeCodeExecutor, ReadHandler, WriteHandler
117
+
118
+ cap = CapabilityExecutor()
119
+ cap.register(ReadHandler())
120
+ cap.register(WriteHandler())
121
+
122
+ session = GovernedSession(
123
+ executor=ClaudeCodeExecutor(),
124
+ capability_executor=cap,
125
+ )
126
+ ```
127
+
128
+ ### Extensions
129
+
130
+ `CLAUDE.md` and `.claude/skills/` are loaded automatically by `make_session()`:
131
+
132
+ ```
133
+ your-project/
134
+ ├── CLAUDE.md ← project-level context → ContextView
135
+ └── .claude/
136
+ ├── skills/
137
+ │ ├── testing.md ← "always write pytest tests"
138
+ │ └── style.md ← "use type annotations"
139
+ └── plugins/
140
+ └── my-plugin/
141
+ ├── plugin.json ← tool definitions, commands, hooks
142
+ └── README.md ← context → ContextView
143
+ ```
144
+
145
+ Custom extension loader:
146
+
147
+ ```python
148
+ from axor_core.contracts.extension import ExtensionLoader, ExtensionBundle, ExtensionFragment
149
+
150
+ class MyLoader(ExtensionLoader):
151
+ async def load(self) -> ExtensionBundle:
152
+ return ExtensionBundle(fragments=(
153
+ ExtensionFragment(
154
+ name="domain_context",
155
+ context_fragment="This project uses FastAPI with async SQLAlchemy.",
156
+ required_tools=("read",),
157
+ policy_overrides={},
158
+ source="my_loader",
159
+ ),
160
+ ))
161
+
162
+ session = axor_claude.make_session(
163
+ extension_loaders=[MyLoader()],
164
+ )
165
+ ```
166
+
167
+ ### Budget tracking
168
+
169
+ ```python
170
+ session = axor_claude.make_session(
171
+ soft_token_limit=100_000,
172
+ )
173
+ # At 60% → suggest context compression
174
+ # At 80% → deny new child nodes
175
+ # At 90% → restrict export mode
176
+ # At 95% → hard stop via CancelToken
177
+ ```
178
+
179
+ ### Custom model and system prompt
180
+
181
+ ```python
182
+ executor = ClaudeCodeExecutor(
183
+ api_key="sk-ant-...",
184
+ model="claude-opus-4-5",
185
+ system_prompt="You are an expert in Go and distributed systems.",
186
+ )
187
+ ```
188
+
189
+ ---
190
+
191
+ ## How the tool loop works
192
+
193
+ `ClaudeCodeExecutor` drives a multi-turn conversation with Claude. Tool calls are intercepted by axor-core's `IntentLoop` before they execute:
194
+
195
+ ```
196
+ Claude API stream → tool_use event
197
+ → IntentLoop intercepts → Intent
198
+ → policy check: is tool in capabilities.allowed_tools?
199
+ → approved → CapabilityExecutor.execute() → ToolResultBus.push()
200
+ → denied → denial result → ToolResultBus.push()
201
+ → executor drains bus → continues conversation with tool_result
202
+ ```
203
+
204
+ The `ToolResultBus` is the handoff mechanism:
205
+
206
+ ```python
207
+ # Inside ClaudeCodeExecutor.stream():
208
+ bus.expect(1)
209
+ yield ExecutorEvent(kind=TOOL_USE, ...) # IntentLoop intercepts here
210
+ # executes tool, pushes to bus
211
+ results = await bus.drain() # result already in queue
212
+ # next API round uses the result
213
+ ```
214
+
215
+ ### Streaming to terminal
216
+
217
+ `ClaudeCodeExecutor` supports a text callback for real-time streaming (used by axor-cli):
218
+
219
+ ```python
220
+ executor = ClaudeCodeExecutor()
221
+
222
+ def on_text(chunk: str) -> None:
223
+ print(chunk, end="", flush=True)
224
+
225
+ executor.set_text_callback(on_text)
226
+ # text chunks are printed as they arrive from Claude
227
+ ```
228
+
229
+ ### Error handling
230
+
231
+ Transient errors are distinguished from fatal ones in the ERROR event payload:
232
+
233
+ ```python
234
+ # ERROR event payload for rate limit:
235
+ {
236
+ "type": "RateLimitError",
237
+ "message": "...",
238
+ "transient": True, # retry is appropriate
239
+ }
240
+
241
+ # ERROR event payload for auth failure:
242
+ {
243
+ "type": "AuthenticationError",
244
+ "message": "...",
245
+ "transient": False, # fix the key, don't retry
246
+ }
247
+ ```
248
+
249
+ Transient types: `RateLimitError`, `InternalServerError`, `APIConnectionError`, `APITimeoutError`.
250
+
251
+ ---
252
+
253
+ ## Policy-driven context
254
+
255
+ The fragment limit passed to Claude scales with the policy's context mode:
256
+
257
+ | context_mode | max fragments to Claude |
258
+ |-------------|------------------------|
259
+ | `minimal` | 5 |
260
+ | `moderate` | 15 |
261
+ | `broad` | 40 |
262
+
263
+ This means a "write a test" task (focused_generative → minimal) sends at most 5 context fragments. A "rewrite repo" task (expansive → broad) sends up to 40.
264
+
265
+ ---
266
+
267
+ ## Custom tools via plugins
268
+
269
+ Register a tool handler and its Anthropic schema:
270
+
271
+ ```python
272
+ from axor_core.capability.executor import ToolHandler
273
+ from axor_claude import register_tool_definition
274
+
275
+ class GitBlameHandler(ToolHandler):
276
+ @property
277
+ def name(self) -> str:
278
+ return "git_blame"
279
+
280
+ async def execute(self, args: dict) -> str:
281
+ import asyncio
282
+ proc = await asyncio.create_subprocess_exec(
283
+ "git", "blame", args["file"],
284
+ stdout=asyncio.subprocess.PIPE,
285
+ )
286
+ out, _ = await proc.communicate()
287
+ return out.decode()
288
+
289
+ register_tool_definition("git_blame", {
290
+ "name": "git_blame",
291
+ "description": "Show who last modified each line of a file",
292
+ "input_schema": {
293
+ "type": "object",
294
+ "properties": {"file": {"type": "string"}},
295
+ "required": ["file"],
296
+ },
297
+ })
298
+
299
+ cap.register(GitBlameHandler())
300
+ ```
301
+
302
+ Or via `.claude/plugins/git-tools/plugin.json`:
303
+
304
+ ```json
305
+ {
306
+ "name": "git-tools",
307
+ "version": "1.0.0",
308
+ "tools": [{
309
+ "name": "git_blame",
310
+ "description": "Show who last modified each line of a file",
311
+ "input_schema": {
312
+ "type": "object",
313
+ "properties": { "file": { "type": "string" } },
314
+ "required": ["file"]
315
+ }
316
+ }]
317
+ }
318
+ ```
319
+
320
+ ---
321
+
322
+ ## normalizer module
323
+
324
+ Utilities for working with Anthropic API response objects:
325
+
326
+ ```python
327
+ from axor_claude import normalizer
328
+
329
+ usage = normalizer.extract_usage(message)
330
+ # {"input_tokens": 500, "output_tokens": 120, "tool_tokens": 0}
331
+
332
+ stop = normalizer.extract_stop_reason(message)
333
+ # "end_turn" | "tool_use" | "max_tokens" | "stop_sequence"
334
+
335
+ text = normalizer.extract_text_content(message)
336
+ # text content only, skips tool_use blocks
337
+
338
+ tools = normalizer.extract_tool_uses(message)
339
+ # [{"tool_use_id": "tu_1", "tool": "bash", "args": {"command": "ls"}}]
340
+
341
+ meta = normalizer.build_response_metadata(message, "focused_generative", "node_001", depth=0)
342
+ # {"policy": "focused_generative", "model": "claude-sonnet-4-5", "stop_reason": "end_turn", ...}
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Repository structure
348
+
349
+ ```
350
+ axor-claude/
351
+ ├── axor_claude/
352
+ │ ├── __init__.py Public API + make_session() factory
353
+ │ ├── executor.py ClaudeCodeExecutor — streaming, ToolResultBus, text callback
354
+ │ ├── events.py StreamNormalizer — Anthropic SDK events → ExecutorEvent
355
+ │ ├── normalizer.py Response metadata extraction utilities
356
+ │ ├── tool_definitions.py Anthropic API tool schemas (read/write/bash/search/glob/spawn_child)
357
+ │ ├── tools/
358
+ │ │ ├── read.py ReadHandler — line ranges, encoding fallback, size cap
359
+ │ │ ├── write.py WriteHandler — atomic write (tmpfile → rename), append
360
+ │ │ ├── bash.py BashHandler — async subprocess, process group, timeout
361
+ │ │ ├── search.py SearchHandler — ripgrep + Python fallback, context lines
362
+ │ │ └── glob.py GlobHandler — pattern matching, smart ignores
363
+ │ └── extensions/
364
+ │ ├── skill_loader.py ClaudeSkillLoader — CLAUDE.md + .claude/skills/
365
+ │ └── plugin_loader.py ClaudePluginLoader — .claude/plugins/ JSON manifests
366
+ └── tests/
367
+ ├── unit/
368
+ │ ├── test_events.py StreamNormalizer — 30 tests
369
+ │ ├── test_normalizer_and_defs.py normalizer + tool_definitions
370
+ │ └── tools/test_tools.py all handlers — 35 tests
371
+ └── integration/ requires ANTHROPIC_API_KEY
372
+ └── test_integration.py
373
+ ```
374
+
375
+ ---
376
+
377
+ ## Running tests
378
+
379
+ ```bash
380
+ # unit tests — no API key needed
381
+ pytest tests/unit/
382
+
383
+ # integration tests — requires ANTHROPIC_API_KEY
384
+ ANTHROPIC_API_KEY=sk-ant-... pytest tests/integration/ -m integration
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Requirements
390
+
391
+ - Python 3.11+
392
+ - `axor-core >= 0.1.0`
393
+ - `anthropic >= 0.40.0`
394
+ - `ripgrep` (optional — faster search, falls back to Python grep)
395
+
396
+ ---
397
+
398
+ ## License
399
+
400
+ MIT