pisama-claude-code 0.3.7__tar.gz → 0.4.2__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 (39) hide show
  1. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/CHANGELOG.md +14 -1
  2. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/PKG-INFO +50 -3
  3. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/README.md +42 -2
  4. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/pyproject.toml +7 -2
  5. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/__init__.py +1 -1
  6. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/cli.py +95 -20
  7. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/install.py +3 -0
  8. pisama_claude_code-0.4.2/src/pisama_claude_code/otel_export.py +287 -0
  9. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  10. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  11. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  12. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/workflows/ci.yml +0 -0
  14. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/workflows/publish.yml +0 -0
  15. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.gitignore +0 -0
  16. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/CODE_OF_CONDUCT.md +0 -0
  17. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/CONTRIBUTING.md +0 -0
  18. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/LICENSE +0 -0
  19. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/SECURITY.md +0 -0
  20. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/assets/demo.gif +0 -0
  21. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/README.md +0 -0
  22. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/demo-simulated.sh +0 -0
  23. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/demo.cast +0 -0
  24. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/record-demo.sh +0 -0
  25. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/adapter.py +0 -0
  26. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/guardian.py +0 -0
  27. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/__init__.py +0 -0
  28. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/capture_hook.py +0 -0
  29. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/guardian_hook.py +0 -0
  30. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/py.typed +0 -0
  31. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/storage.py +0 -0
  32. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/trace_converter.py +0 -0
  33. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/trace_types.py +0 -0
  34. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/__init__.py +0 -0
  35. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/conftest.py +0 -0
  36. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_adapter.py +0 -0
  37. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_cli.py +0 -0
  38. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_guardian.py +0 -0
  39. {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_storage.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-01-05
11
+
12
+ ### Added
13
+ - **OpenTelemetry export** - Export traces to any OTEL-compatible backend
14
+ - `export-otel` command for direct export to OTEL collectors (Jaeger, Honeycomb, Datadog)
15
+ - `export --format otel` for OTEL JSON file export
16
+ - GenAI semantic conventions for token usage and model attributes
17
+ - Optional `[otel]` dependency: `pip install pisama-claude-code[otel]`
18
+
19
+ ### Changed
20
+ - Updated description to reflect role in broader PISAMA platform
21
+
10
22
  ## [0.3.5] - 2025-01-05
11
23
 
12
24
  ### Fixed
@@ -91,7 +103,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
91
103
  - Support for Bash, Read, Write, Edit, Grep, Glob tools
92
104
  - Local storage in `~/.claude/pisama/traces/`
93
105
 
94
- [Unreleased]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.5...HEAD
106
+ [Unreleased]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.4.0...HEAD
107
+ [0.4.0]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.5...v0.4.0
95
108
  [0.3.5]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.4...v0.3.5
96
109
  [0.3.4]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.3...v0.3.4
97
110
  [0.3.3]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.2...v0.3.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pisama-claude-code
3
- Version: 0.3.7
3
+ Version: 0.4.2
4
4
  Summary: Trace capture for Claude Code sessions - sync to PISAMA platform for analysis
5
5
  Project-URL: Homepage, https://pisama.dev
6
6
  Project-URL: Documentation, https://pisama.dev/docs/claude-code
@@ -36,6 +36,9 @@ Requires-Dist: httpx>=0.25.0
36
36
  Requires-Dist: pydantic>=2.0.0
37
37
  Requires-Dist: rich>=13.0.0
38
38
  Provides-Extra: all
39
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == 'all'
40
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == 'all'
41
+ Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'all'
39
42
  Requires-Dist: pisama-core>=0.1.0; extra == 'all'
40
43
  Provides-Extra: core
41
44
  Requires-Dist: pisama-core>=0.1.0; extra == 'core'
@@ -43,6 +46,10 @@ Provides-Extra: dev
43
46
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
44
47
  Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
45
48
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
49
+ Provides-Extra: otel
50
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == 'otel'
51
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == 'otel'
52
+ Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'otel'
46
53
  Description-Content-Type: text/markdown
47
54
 
48
55
  # pisama-claude-code
@@ -163,10 +170,34 @@ pisama-cc export -o traces.jsonl
163
170
  # Export compressed
164
171
  pisama-cc export -o traces.jsonl.gz --compress
165
172
 
173
+ # Export to OpenTelemetry format
174
+ pisama-cc export --format otel -o traces-otel.json
175
+
166
176
  # Filter by date range
167
177
  pisama-cc traces --since 2025-01-01 --until 2025-01-04
168
178
  ```
169
179
 
180
+ ### OpenTelemetry Integration
181
+
182
+ Export traces to any OTEL-compatible backend (Jaeger, Honeycomb, Datadog, etc.):
183
+
184
+ ```bash
185
+ # Install OTEL support
186
+ pip install pisama-claude-code[otel]
187
+
188
+ # Export to local Jaeger
189
+ pisama-cc export-otel -e http://localhost:4318/v1/traces
190
+
191
+ # Export to Honeycomb
192
+ pisama-cc export-otel -e https://api.honeycomb.io/v1/traces \
193
+ -H "x-honeycomb-team=YOUR_API_KEY"
194
+
195
+ # Export to file in OTEL format
196
+ pisama-cc export --format otel -o traces.json
197
+ ```
198
+
199
+ OTEL export uses [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for token usage, costs, and model attributes.
200
+
170
201
  ## CLI Reference
171
202
 
172
203
  | Command | Description |
@@ -174,12 +205,14 @@ pisama-cc traces --since 2025-01-01 --until 2025-01-04
174
205
  | `pisama-cc install` | Install capture hooks to `~/.claude/hooks/` |
175
206
  | `pisama-cc uninstall` | Remove hooks |
176
207
  | `pisama-cc status` | Show status, token totals, and cost |
177
- | `pisama-cc traces` | View recent traces (`-v` for verbose) |
208
+ | `pisama-cc traces` | View recent traces (`-v` for verbose, `-c` for content) |
178
209
  | `pisama-cc usage` | Token usage breakdown (`--by-model`, `--by-tool`) |
179
- | `pisama-cc export` | Export to JSONL (`-o FILE`, `--compress`) |
210
+ | `pisama-cc export` | Export to JSONL or OTEL (`--format otel`, `--compress`) |
211
+ | `pisama-cc export-otel` | Export to OpenTelemetry collector (`-e ENDPOINT`) |
180
212
  | `pisama-cc connect` | Connect to PISAMA platform (optional) |
181
213
  | `pisama-cc sync` | Upload traces to platform |
182
214
  | `pisama-cc analyze` | Run failure detection (requires platform) |
215
+ | `pisama-cc vault status` | Show PII tokenization vault status |
183
216
 
184
217
  ## Model Pricing
185
218
 
@@ -240,6 +273,20 @@ Platform features:
240
273
  - Self-healing automation
241
274
  - Visual dashboard
242
275
 
276
+ ## Part of the PISAMA Platform
277
+
278
+ `pisama-claude-code` is the Claude Code integration for the broader **PISAMA (Platform for Intelligent Self-healing AI Multi-Agent) Testing Platform**, which supports multiple agent frameworks:
279
+
280
+ | Framework | Package | Status |
281
+ |-----------|---------|--------|
282
+ | Claude Code | `pisama-claude-code` | Stable |
283
+ | LangChain/LangGraph | `mao-testing` SDK | Available |
284
+ | CrewAI | `mao-testing` SDK | Available |
285
+ | AutoGen | `mao-testing` SDK | Available |
286
+ | n8n | `mao-testing` SDK | Available |
287
+
288
+ For other frameworks, see the [mao-testing SDK](https://github.com/tn-pisama/mao-testing).
289
+
243
290
  ## Contributing
244
291
 
245
292
  We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
@@ -116,10 +116,34 @@ pisama-cc export -o traces.jsonl
116
116
  # Export compressed
117
117
  pisama-cc export -o traces.jsonl.gz --compress
118
118
 
119
+ # Export to OpenTelemetry format
120
+ pisama-cc export --format otel -o traces-otel.json
121
+
119
122
  # Filter by date range
120
123
  pisama-cc traces --since 2025-01-01 --until 2025-01-04
121
124
  ```
122
125
 
126
+ ### OpenTelemetry Integration
127
+
128
+ Export traces to any OTEL-compatible backend (Jaeger, Honeycomb, Datadog, etc.):
129
+
130
+ ```bash
131
+ # Install OTEL support
132
+ pip install pisama-claude-code[otel]
133
+
134
+ # Export to local Jaeger
135
+ pisama-cc export-otel -e http://localhost:4318/v1/traces
136
+
137
+ # Export to Honeycomb
138
+ pisama-cc export-otel -e https://api.honeycomb.io/v1/traces \
139
+ -H "x-honeycomb-team=YOUR_API_KEY"
140
+
141
+ # Export to file in OTEL format
142
+ pisama-cc export --format otel -o traces.json
143
+ ```
144
+
145
+ OTEL export uses [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for token usage, costs, and model attributes.
146
+
123
147
  ## CLI Reference
124
148
 
125
149
  | Command | Description |
@@ -127,12 +151,14 @@ pisama-cc traces --since 2025-01-01 --until 2025-01-04
127
151
  | `pisama-cc install` | Install capture hooks to `~/.claude/hooks/` |
128
152
  | `pisama-cc uninstall` | Remove hooks |
129
153
  | `pisama-cc status` | Show status, token totals, and cost |
130
- | `pisama-cc traces` | View recent traces (`-v` for verbose) |
154
+ | `pisama-cc traces` | View recent traces (`-v` for verbose, `-c` for content) |
131
155
  | `pisama-cc usage` | Token usage breakdown (`--by-model`, `--by-tool`) |
132
- | `pisama-cc export` | Export to JSONL (`-o FILE`, `--compress`) |
156
+ | `pisama-cc export` | Export to JSONL or OTEL (`--format otel`, `--compress`) |
157
+ | `pisama-cc export-otel` | Export to OpenTelemetry collector (`-e ENDPOINT`) |
133
158
  | `pisama-cc connect` | Connect to PISAMA platform (optional) |
134
159
  | `pisama-cc sync` | Upload traces to platform |
135
160
  | `pisama-cc analyze` | Run failure detection (requires platform) |
161
+ | `pisama-cc vault status` | Show PII tokenization vault status |
136
162
 
137
163
  ## Model Pricing
138
164
 
@@ -193,6 +219,20 @@ Platform features:
193
219
  - Self-healing automation
194
220
  - Visual dashboard
195
221
 
222
+ ## Part of the PISAMA Platform
223
+
224
+ `pisama-claude-code` is the Claude Code integration for the broader **PISAMA (Platform for Intelligent Self-healing AI Multi-Agent) Testing Platform**, which supports multiple agent frameworks:
225
+
226
+ | Framework | Package | Status |
227
+ |-----------|---------|--------|
228
+ | Claude Code | `pisama-claude-code` | Stable |
229
+ | LangChain/LangGraph | `mao-testing` SDK | Available |
230
+ | CrewAI | `mao-testing` SDK | Available |
231
+ | AutoGen | `mao-testing` SDK | Available |
232
+ | n8n | `mao-testing` SDK | Available |
233
+
234
+ For other frameworks, see the [mao-testing SDK](https://github.com/tn-pisama/mao-testing).
235
+
196
236
  ## Contributing
197
237
 
198
238
  We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pisama-claude-code"
7
- version = "0.3.7"
7
+ version = "0.4.2"
8
8
  description = "Trace capture for Claude Code sessions - sync to PISAMA platform for analysis"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -62,13 +62,18 @@ dependencies = [
62
62
  core = [
63
63
  "pisama-core>=0.1.0",
64
64
  ]
65
+ otel = [
66
+ "opentelemetry-api>=1.20.0",
67
+ "opentelemetry-sdk>=1.20.0",
68
+ "opentelemetry-exporter-otlp-proto-http>=1.20.0",
69
+ ]
65
70
  dev = [
66
71
  "pytest>=7.0.0",
67
72
  "pytest-asyncio>=0.21.0",
68
73
  "pytest-cov>=4.0.0",
69
74
  ]
70
75
  all = [
71
- "pisama-claude-code[core]",
76
+ "pisama-claude-code[core,otel]",
72
77
  ]
73
78
 
74
79
  [project.scripts]
@@ -1,6 +1,6 @@
1
1
  """PISAMA Claude Code Integration - Trace capture, failure detection, and self-healing."""
2
2
 
3
- __version__ = "0.3.5"
3
+ __version__ = "0.4.0"
4
4
 
5
5
  # Lazy imports to avoid loading everything at startup
6
6
  def install(force: bool = False):
@@ -4,15 +4,20 @@ Command-line interface for capturing Claude Code traces and syncing
4
4
  to the PISAMA platform for analysis and self-healing.
5
5
 
6
6
  Usage:
7
- pisama-cc install Install hooks to ~/.claude/
8
- pisama-cc uninstall Remove hooks
9
- pisama-cc status Show current status (incl. token/cost totals)
10
- pisama-cc traces View recent traces (-v for token usage)
11
- pisama-cc usage Show token usage and cost breakdown
12
- pisama-cc export Export traces to file
13
- pisama-cc connect Connect to PISAMA platform
14
- pisama-cc sync Sync traces to platform
15
- pisama-cc analyze Analyze traces (requires platform)
7
+ pisama-cc install Install hooks to ~/.claude/
8
+ pisama-cc uninstall Remove hooks
9
+ pisama-cc status Show current status (incl. token/cost totals)
10
+ pisama-cc traces View recent traces (-v for token usage)
11
+ pisama-cc usage Show token usage and cost breakdown
12
+ pisama-cc export Export traces to file (JSONL or OTEL format)
13
+ pisama-cc export-otel Export traces to OpenTelemetry collector
14
+ pisama-cc connect Connect to PISAMA platform
15
+ pisama-cc sync Sync traces to platform
16
+ pisama-cc analyze Analyze traces (requires platform)
17
+
18
+ OTEL Export:
19
+ pisama-cc export --format otel -o traces.json
20
+ pisama-cc export-otel -e http://localhost:4318/v1/traces
16
21
  """
17
22
 
18
23
  import click
@@ -53,7 +58,7 @@ def save_config(config: dict):
53
58
 
54
59
 
55
60
  @click.group()
56
- @click.version_option(version="0.3.6")
61
+ @click.version_option(version="0.4.0")
57
62
  def main():
58
63
  """PISAMA Claude Code - Trace capture and sync."""
59
64
  pass
@@ -69,13 +74,6 @@ def install(force: bool):
69
74
  from pisama_claude_code.install import install as do_install
70
75
  do_install(force=force)
71
76
 
72
- # Star reminder after install
73
- click.echo("")
74
- click.echo("=" * 50)
75
- click.echo("If pisama-claude-code is useful, please star us!")
76
- click.echo(f" {GITHUB_URL}")
77
- click.echo("=" * 50)
78
-
79
77
 
80
78
  @main.command()
81
79
  def uninstall():
@@ -515,15 +513,29 @@ def usage(last: int, by_model: bool, by_tool: bool):
515
513
  @click.option("--last", default=50, help="Number of traces to export")
516
514
  @click.option("--output", "-o", default="traces-export.jsonl", help="Output file")
517
515
  @click.option("--compress", is_flag=True, help="Gzip compress output")
518
- def export(last: int, output: str, compress: bool):
519
- """Export traces to a file."""
516
+ @click.option("--format", "fmt", type=click.Choice(["jsonl", "otel"]), default="jsonl", help="Export format")
517
+ def export(last: int, output: str, compress: bool, fmt: str):
518
+ """Export traces to a file (JSONL or OTEL format)."""
520
519
  traces_list = load_recent_traces(last)
521
520
 
522
521
  if not traces_list:
523
522
  click.echo("No traces to export")
524
523
  return
525
524
 
526
- # Prepare export (redact sensitive data)
525
+ # Handle OTEL format
526
+ if fmt == "otel":
527
+ from pisama_claude_code.otel_export import export_to_otel_file
528
+
529
+ # Adjust default output for OTEL
530
+ if output == "traces-export.jsonl":
531
+ output = "traces-export-otel.json"
532
+
533
+ result = export_to_otel_file(traces_list, output)
534
+ click.echo(f"✅ Exported {result['spans_created']} spans to {output} (OTEL format)")
535
+ click.echo(f" Sessions: {result['sessions_exported']}")
536
+ return
537
+
538
+ # Prepare JSONL export (redact sensitive data)
527
539
  export_data = []
528
540
  for t in traces_list:
529
541
  clean = {
@@ -572,6 +584,69 @@ def export(last: int, output: str, compress: bool):
572
584
  click.echo(f"✅ Exported {len(export_data)} traces to {output_path} ({size_kb} KB)")
573
585
 
574
586
 
587
+ @main.command("export-otel")
588
+ @click.option("--last", default=50, help="Number of traces to export")
589
+ @click.option("--endpoint", "-e", required=True, help="OTEL collector endpoint (e.g., http://localhost:4318/v1/traces)")
590
+ @click.option("--service-name", "-s", default="claude-code", help="Service name for spans")
591
+ @click.option("--header", "-H", multiple=True, help="Header in 'key=value' format (can be repeated)")
592
+ def export_otel(last: int, endpoint: str, service_name: str, header: tuple):
593
+ """Export traces to an OpenTelemetry collector.
594
+
595
+ Examples:
596
+
597
+ # Local Jaeger
598
+ pisama-cc export-otel -e http://localhost:4318/v1/traces
599
+
600
+ # Honeycomb
601
+ pisama-cc export-otel -e https://api.honeycomb.io/v1/traces \\
602
+ -H "x-honeycomb-team=YOUR_API_KEY"
603
+
604
+ # Datadog (via OTEL collector)
605
+ pisama-cc export-otel -e http://localhost:4318/v1/traces \\
606
+ -s my-claude-agent
607
+ """
608
+ from pisama_claude_code.otel_export import is_otel_available, export_traces_to_otel
609
+
610
+ if not is_otel_available():
611
+ click.echo("❌ OpenTelemetry not installed")
612
+ click.echo(" Run: pip install pisama-claude-code[otel]")
613
+ return
614
+
615
+ traces_list = load_recent_traces(last)
616
+
617
+ if not traces_list:
618
+ click.echo("No traces to export")
619
+ return
620
+
621
+ # Parse headers
622
+ headers = {}
623
+ for h in header:
624
+ if "=" in h:
625
+ key, value = h.split("=", 1)
626
+ headers[key.strip()] = value.strip()
627
+
628
+ click.echo(f"📤 Exporting {len(traces_list)} traces to OTEL collector...")
629
+ click.echo(f" Endpoint: {endpoint}")
630
+ click.echo(f" Service: {service_name}")
631
+
632
+ try:
633
+ result = export_traces_to_otel(
634
+ traces=traces_list,
635
+ endpoint=endpoint,
636
+ service_name=service_name,
637
+ headers=headers if headers else None,
638
+ )
639
+
640
+ if result["success"]:
641
+ click.echo(f"✅ Exported {result['spans_created']} spans")
642
+ click.echo(f" Sessions: {result['sessions_exported']}")
643
+ click.echo(f" Traces: {result['traces_processed']}")
644
+ else:
645
+ click.echo("❌ Export failed")
646
+ except Exception as e:
647
+ click.echo(f"❌ Export failed: {e}")
648
+
649
+
575
650
  # Helper functions
576
651
 
577
652
  def normalize_trace(t: dict) -> dict:
@@ -74,6 +74,9 @@ def install(force: bool = False):
74
74
  print("\nNext steps:")
75
75
  print(" 1. Add hooks to settings.local.json (see above)")
76
76
  print(" 2. Run 'pisama-cc connect --api-key <key>' to enable analysis")
77
+ print("\n" + "─" * 50)
78
+ print("⭐ If this tool saves you time/money, consider starring:")
79
+ print(" https://github.com/tn-pisama/pisama-claude-code")
77
80
 
78
81
 
79
82
  def _install_shell_hooks(hooks_dir: Path, force: bool):
@@ -0,0 +1,287 @@
1
+ """OpenTelemetry export for pisama-claude-code traces.
2
+
3
+ Converts Claude Code traces to OpenTelemetry spans and exports them
4
+ to any OTEL-compatible backend (Jaeger, Honeycomb, Datadog, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import json
9
+ import time
10
+ from datetime import datetime
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ # Lazy imports for OTEL
14
+ _otel_available = None
15
+
16
+
17
+ def is_otel_available() -> bool:
18
+ """Check if OpenTelemetry is installed."""
19
+ global _otel_available
20
+ if _otel_available is None:
21
+ try:
22
+ import opentelemetry # noqa: F401
23
+ _otel_available = True
24
+ except ImportError:
25
+ _otel_available = False
26
+ return _otel_available
27
+
28
+
29
+ def export_traces_to_otel(
30
+ traces: List[Dict[str, Any]],
31
+ endpoint: str,
32
+ service_name: str = "claude-code",
33
+ headers: Optional[Dict[str, str]] = None,
34
+ ) -> Dict[str, Any]:
35
+ """Export traces to an OTEL endpoint.
36
+
37
+ Args:
38
+ traces: List of normalized trace dicts from pisama-claude-code
39
+ endpoint: OTEL collector endpoint (e.g., http://localhost:4318/v1/traces)
40
+ service_name: Service name for the spans
41
+ headers: Optional headers (e.g., for authentication)
42
+
43
+ Returns:
44
+ Dict with export status and statistics
45
+ """
46
+ if not is_otel_available():
47
+ raise ImportError(
48
+ "OpenTelemetry not installed. Run: pip install pisama-claude-code[otel]"
49
+ )
50
+
51
+ from opentelemetry import trace as otel_trace
52
+ from opentelemetry.sdk.trace import TracerProvider
53
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
54
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
55
+ from opentelemetry.sdk.resources import Resource
56
+ from opentelemetry.trace import SpanKind, Status, StatusCode
57
+
58
+ # Set up resource
59
+ resource = Resource.create({
60
+ "service.name": service_name,
61
+ "service.version": "0.3.7",
62
+ "telemetry.sdk.name": "pisama-claude-code",
63
+ })
64
+
65
+ # Create provider and exporter
66
+ provider = TracerProvider(resource=resource)
67
+ exporter = OTLPSpanExporter(
68
+ endpoint=endpoint,
69
+ headers=headers or {},
70
+ )
71
+ provider.add_span_processor(BatchSpanProcessor(exporter))
72
+ otel_trace.set_tracer_provider(provider)
73
+
74
+ tracer = otel_trace.get_tracer("pisama-claude-code")
75
+
76
+ # Group traces by session
77
+ sessions: Dict[str, List[Dict]] = {}
78
+ for t in traces:
79
+ session_id = t.get("session_id", "unknown")
80
+ if session_id not in sessions:
81
+ sessions[session_id] = []
82
+ sessions[session_id].append(t)
83
+
84
+ spans_created = 0
85
+
86
+ # Create spans for each session
87
+ for session_id, session_traces in sessions.items():
88
+ # Sort by timestamp
89
+ session_traces.sort(key=lambda x: x.get("timestamp", ""))
90
+
91
+ # Create parent span for session
92
+ session_start = _parse_timestamp(session_traces[0].get("timestamp"))
93
+ session_end = _parse_timestamp(session_traces[-1].get("timestamp"))
94
+
95
+ with tracer.start_span(
96
+ name=f"claude-code-session:{session_id}",
97
+ kind=SpanKind.INTERNAL,
98
+ start_time=session_start,
99
+ ) as session_span:
100
+ session_span.set_attribute("session.id", session_id)
101
+ session_span.set_attribute("session.trace_count", len(session_traces))
102
+
103
+ # Calculate session totals
104
+ total_input = sum(t.get("input_tokens", 0) for t in session_traces)
105
+ total_output = sum(t.get("output_tokens", 0) for t in session_traces)
106
+ total_cost = sum(t.get("cost_usd", 0) for t in session_traces)
107
+
108
+ session_span.set_attribute("session.input_tokens", total_input)
109
+ session_span.set_attribute("session.output_tokens", total_output)
110
+ session_span.set_attribute("session.cost_usd", total_cost)
111
+
112
+ spans_created += 1
113
+
114
+ # Create child spans for each tool call
115
+ for t in session_traces:
116
+ span_start = _parse_timestamp(t.get("timestamp"))
117
+ tool_name = t.get("tool_name", "unknown")
118
+ hook_type = t.get("hook_type", "")
119
+
120
+ with tracer.start_span(
121
+ name=f"{tool_name}:{hook_type}",
122
+ kind=SpanKind.INTERNAL,
123
+ start_time=span_start,
124
+ ) as span:
125
+ # Standard attributes
126
+ span.set_attribute("tool.name", tool_name)
127
+ span.set_attribute("hook.type", hook_type)
128
+ span.set_attribute("session.id", session_id)
129
+
130
+ # Model and token usage (GenAI semantic conventions)
131
+ if t.get("model"):
132
+ span.set_attribute("gen_ai.system", "anthropic")
133
+ span.set_attribute("gen_ai.request.model", t["model"])
134
+
135
+ if t.get("input_tokens"):
136
+ span.set_attribute("gen_ai.usage.input_tokens", t["input_tokens"])
137
+ if t.get("output_tokens"):
138
+ span.set_attribute("gen_ai.usage.output_tokens", t["output_tokens"])
139
+ if t.get("cache_read_tokens"):
140
+ span.set_attribute("gen_ai.usage.cache_read_tokens", t["cache_read_tokens"])
141
+ if t.get("cost_usd"):
142
+ span.set_attribute("gen_ai.usage.cost_usd", t["cost_usd"])
143
+
144
+ # Content attributes (if captured)
145
+ if t.get("user_input"):
146
+ span.set_attribute("gen_ai.prompt", _truncate(t["user_input"], 4096))
147
+ if t.get("reasoning"):
148
+ span.set_attribute("gen_ai.reasoning", _truncate(t["reasoning"], 4096))
149
+ if t.get("ai_output"):
150
+ span.set_attribute("gen_ai.completion", _truncate(t["ai_output"], 4096))
151
+
152
+ # Tool input (sanitized)
153
+ tool_input = t.get("tool_input")
154
+ if tool_input:
155
+ try:
156
+ span.set_attribute("tool.input", json.dumps(tool_input)[:2048])
157
+ except (TypeError, ValueError):
158
+ pass
159
+
160
+ # Working directory
161
+ if t.get("working_dir"):
162
+ span.set_attribute("tool.working_dir", t["working_dir"])
163
+
164
+ spans_created += 1
165
+
166
+ # Force flush and shutdown
167
+ provider.force_flush()
168
+ provider.shutdown()
169
+
170
+ return {
171
+ "success": True,
172
+ "endpoint": endpoint,
173
+ "service_name": service_name,
174
+ "sessions_exported": len(sessions),
175
+ "spans_created": spans_created,
176
+ "traces_processed": len(traces),
177
+ }
178
+
179
+
180
+ def convert_trace_to_otel_dict(trace: Dict[str, Any]) -> Dict[str, Any]:
181
+ """Convert a single trace to OTEL span format (dict).
182
+
183
+ Useful for exporting to file in OTEL-compatible format.
184
+ """
185
+ timestamp_ns = _parse_timestamp(trace.get("timestamp"))
186
+
187
+ return {
188
+ "name": f"{trace.get('tool_name', 'unknown')}:{trace.get('hook_type', '')}",
189
+ "kind": "SPAN_KIND_INTERNAL",
190
+ "startTimeUnixNano": timestamp_ns,
191
+ "endTimeUnixNano": timestamp_ns + 1_000_000, # +1ms
192
+ "attributes": [
193
+ {"key": "tool.name", "value": {"stringValue": trace.get("tool_name", "")}},
194
+ {"key": "hook.type", "value": {"stringValue": trace.get("hook_type", "")}},
195
+ {"key": "session.id", "value": {"stringValue": trace.get("session_id", "")}},
196
+ {"key": "gen_ai.system", "value": {"stringValue": "anthropic"}},
197
+ {"key": "gen_ai.request.model", "value": {"stringValue": trace.get("model", "")}},
198
+ {"key": "gen_ai.usage.input_tokens", "value": {"intValue": trace.get("input_tokens", 0)}},
199
+ {"key": "gen_ai.usage.output_tokens", "value": {"intValue": trace.get("output_tokens", 0)}},
200
+ ],
201
+ "traceId": _generate_trace_id(trace.get("session_id", "")),
202
+ "spanId": _generate_span_id(trace.get("timestamp", "")),
203
+ }
204
+
205
+
206
+ def export_to_otel_file(
207
+ traces: List[Dict[str, Any]],
208
+ output_path: str,
209
+ service_name: str = "claude-code",
210
+ ) -> Dict[str, Any]:
211
+ """Export traces to an OTEL-compatible JSON file.
212
+
213
+ The output format follows the OTLP JSON schema.
214
+ """
215
+ resource_spans = []
216
+
217
+ # Group by session
218
+ sessions: Dict[str, List[Dict]] = {}
219
+ for t in traces:
220
+ session_id = t.get("session_id", "unknown")
221
+ if session_id not in sessions:
222
+ sessions[session_id] = []
223
+ sessions[session_id].append(t)
224
+
225
+ for session_id, session_traces in sessions.items():
226
+ spans = [convert_trace_to_otel_dict(t) for t in session_traces]
227
+
228
+ resource_spans.append({
229
+ "resource": {
230
+ "attributes": [
231
+ {"key": "service.name", "value": {"stringValue": service_name}},
232
+ {"key": "session.id", "value": {"stringValue": session_id}},
233
+ ]
234
+ },
235
+ "scopeSpans": [
236
+ {
237
+ "scope": {"name": "pisama-claude-code"},
238
+ "spans": spans,
239
+ }
240
+ ]
241
+ })
242
+
243
+ otel_payload = {"resourceSpans": resource_spans}
244
+
245
+ with open(output_path, "w") as f:
246
+ json.dump(otel_payload, f, indent=2)
247
+
248
+ return {
249
+ "success": True,
250
+ "output_path": output_path,
251
+ "sessions_exported": len(sessions),
252
+ "spans_created": sum(len(s) for s in sessions.values()),
253
+ }
254
+
255
+
256
+ def _parse_timestamp(ts: Optional[str]) -> int:
257
+ """Parse timestamp string to nanoseconds since epoch."""
258
+ if not ts:
259
+ return time.time_ns()
260
+ try:
261
+ dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
262
+ return int(dt.timestamp() * 1_000_000_000)
263
+ except (ValueError, TypeError):
264
+ return time.time_ns()
265
+
266
+
267
+ def _truncate(text: str, max_len: int) -> str:
268
+ """Truncate text to max length."""
269
+ if not text:
270
+ return ""
271
+ if len(text) > max_len:
272
+ return text[:max_len - 3] + "..."
273
+ return text
274
+
275
+
276
+ def _generate_trace_id(session_id: str) -> str:
277
+ """Generate a 32-char hex trace ID from session ID."""
278
+ import hashlib
279
+ h = hashlib.sha256(session_id.encode()).hexdigest()
280
+ return h[:32]
281
+
282
+
283
+ def _generate_span_id(identifier: str) -> str:
284
+ """Generate a 16-char hex span ID."""
285
+ import hashlib
286
+ h = hashlib.sha256(identifier.encode()).hexdigest()
287
+ return h[:16]