htmlgraph 0.26.5__py3-none-any.whl → 0.26.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +355 -26
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/subagent_stop.py +71 -12
  38. htmlgraph/hooks/validator.py +192 -79
  39. htmlgraph/operations/__init__.py +18 -0
  40. htmlgraph/operations/initialization.py +596 -0
  41. htmlgraph/operations/initialization.py.backup +228 -0
  42. htmlgraph/orchestration/__init__.py +16 -1
  43. htmlgraph/orchestration/claude_launcher.py +185 -0
  44. htmlgraph/orchestration/command_builder.py +71 -0
  45. htmlgraph/orchestration/headless_spawner.py +72 -1332
  46. htmlgraph/orchestration/plugin_manager.py +136 -0
  47. htmlgraph/orchestration/prompts.py +137 -0
  48. htmlgraph/orchestration/spawners/__init__.py +16 -0
  49. htmlgraph/orchestration/spawners/base.py +194 -0
  50. htmlgraph/orchestration/spawners/claude.py +170 -0
  51. htmlgraph/orchestration/spawners/codex.py +442 -0
  52. htmlgraph/orchestration/spawners/copilot.py +299 -0
  53. htmlgraph/orchestration/spawners/gemini.py +478 -0
  54. htmlgraph/orchestration/subprocess_runner.py +33 -0
  55. htmlgraph/orchestration.md +563 -0
  56. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  57. htmlgraph/orchestrator_config.py +357 -0
  58. htmlgraph/orchestrator_mode.py +45 -12
  59. htmlgraph/transcript.py +16 -4
  60. htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
  61. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
  62. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
  63. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
  64. htmlgraph/cli.py +0 -7256
  65. htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
  66. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
  67. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  68. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  69. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  70. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,136 @@
1
+ """Plugin management for HtmlGraph Claude Code integration.
2
+
3
+ Centralizes plugin installation, directory management, and validation.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ pass
15
+
16
+
17
+ class PluginManager:
18
+ """Manage HtmlGraph Claude plugin installation and directories."""
19
+
20
+ @staticmethod
21
+ def get_plugin_dir() -> Path:
22
+ """Get the plugin directory path.
23
+
24
+ Returns:
25
+ Path to packages/claude-plugin/.claude-plugin
26
+ """
27
+ # Path(__file__) = .../src/python/htmlgraph/orchestration/plugin_manager.py
28
+ # Need to go up 5 levels to reach project root
29
+ return (
30
+ Path(__file__).parent.parent.parent.parent.parent
31
+ / "packages"
32
+ / "claude-plugin"
33
+ / ".claude-plugin"
34
+ )
35
+
36
+ @staticmethod
37
+ def install_or_update(verbose: bool = True) -> None:
38
+ """Install or update HtmlGraph plugin.
39
+
40
+ Args:
41
+ verbose: Whether to show progress messages
42
+ """
43
+ if verbose:
44
+ print("\n📦 Installing/upgrading HtmlGraph plugin...\n")
45
+
46
+ # Step 1: Update marketplace
47
+ try:
48
+ if verbose:
49
+ print(" Updating marketplace...")
50
+ result = subprocess.run(
51
+ ["claude", "plugin", "marketplace", "update", "htmlgraph"],
52
+ capture_output=True,
53
+ text=True,
54
+ check=False,
55
+ )
56
+ if result.returncode == 0:
57
+ if verbose:
58
+ print(" ✓ Marketplace updated")
59
+ else:
60
+ # Non-blocking errors
61
+ if (
62
+ "not found" in result.stderr.lower()
63
+ or "no marketplace" in result.stderr.lower()
64
+ ):
65
+ if verbose:
66
+ print(" ℹ Marketplace not configured (OK, continuing)")
67
+ elif verbose:
68
+ print(f" ⚠ Marketplace update: {result.stderr.strip()}")
69
+ except FileNotFoundError:
70
+ if verbose:
71
+ print(" ⚠ 'claude' command not found")
72
+ except Exception as e:
73
+ if verbose:
74
+ print(f" ⚠ Error updating marketplace: {e}")
75
+
76
+ # Step 2: Try update, fallback to install
77
+ try:
78
+ if verbose:
79
+ print(" Updating plugin to latest version...")
80
+ result = subprocess.run(
81
+ ["claude", "plugin", "update", "htmlgraph"],
82
+ capture_output=True,
83
+ text=True,
84
+ check=False,
85
+ )
86
+ if result.returncode == 0:
87
+ if verbose:
88
+ print(" ✓ Plugin updated successfully")
89
+ else:
90
+ # Fallback to install
91
+ if (
92
+ "not installed" in result.stderr.lower()
93
+ or "not found" in result.stderr.lower()
94
+ ):
95
+ if verbose:
96
+ print(" ℹ Plugin not yet installed, installing...")
97
+ install_result = subprocess.run(
98
+ ["claude", "plugin", "install", "htmlgraph"],
99
+ capture_output=True,
100
+ text=True,
101
+ check=False,
102
+ )
103
+ if install_result.returncode == 0:
104
+ if verbose:
105
+ print(" ✓ Plugin installed successfully")
106
+ elif verbose:
107
+ print(f" ⚠ Plugin install: {install_result.stderr.strip()}")
108
+ elif verbose:
109
+ print(f" ⚠ Plugin update: {result.stderr.strip()}")
110
+ except FileNotFoundError:
111
+ if verbose:
112
+ print(" ⚠ 'claude' command not found")
113
+ except Exception as e:
114
+ if verbose:
115
+ print(f" ⚠ Error updating plugin: {e}")
116
+
117
+ if verbose:
118
+ print("\n✓ Plugin installation complete\n")
119
+
120
+ @staticmethod
121
+ def validate_plugin_dir(plugin_dir: Path) -> None:
122
+ """Validate that plugin directory exists, exit if not.
123
+
124
+ Args:
125
+ plugin_dir: Path to plugin directory
126
+
127
+ Raises:
128
+ SystemExit: If plugin directory doesn't exist
129
+ """
130
+ if not plugin_dir.exists():
131
+ print(f"Error: Plugin directory not found: {plugin_dir}", file=sys.stderr)
132
+ print(
133
+ "Expected location: packages/claude-plugin/.claude-plugin",
134
+ file=sys.stderr,
135
+ )
136
+ sys.exit(1)
@@ -0,0 +1,137 @@
1
+ """
2
+ Orchestrator system prompt loading and management.
3
+
4
+ Centralizes logic for loading and combining orchestrator system prompts,
5
+ keeping CLI code clean and focused on invocation.
6
+ """
7
+
8
+ import textwrap
9
+ from pathlib import Path
10
+
11
+
12
+ def get_orchestrator_prompt(include_dev_mode: bool = False) -> str:
13
+ """
14
+ Load and combine orchestrator system prompts.
15
+
16
+ Args:
17
+ include_dev_mode: If True, append development mode guidance
18
+
19
+ Returns:
20
+ Combined system prompt text ready for --append-system-prompt
21
+ """
22
+ package_dir = Path(__file__).parent.parent
23
+
24
+ # Load base orchestrator prompt
25
+ prompt_file = package_dir / "orchestrator-system-prompt-optimized.txt"
26
+ if prompt_file.exists():
27
+ base_prompt = prompt_file.read_text(encoding="utf-8")
28
+ else:
29
+ # Fallback: minimal orchestrator guidance
30
+ base_prompt = textwrap.dedent(
31
+ """
32
+ You are an AI orchestrator for HtmlGraph project development.
33
+
34
+ CRITICAL DIRECTIVES:
35
+ 1. DELEGATE to spawner skills - do not implement directly
36
+ 2. CREATE work items before delegating (features, bugs, spikes)
37
+ 3. USE SDK for tracking - all work must be tracked in .htmlgraph/
38
+ 4. RESPECT dependencies - check blockers before starting
39
+
40
+ Key Rules:
41
+ - Exploration/Research → Skill(skill=".claude-plugin:gemini")
42
+ - Code implementation → Skill(skill=".claude-plugin:codex")
43
+ - Git/GitHub ops → Skill(skill=".claude-plugin:copilot")
44
+ - Strategic planning → Task() with Claude subagent
45
+
46
+ Always use:
47
+ from htmlgraph import SDK
48
+ sdk = SDK(agent='orchestrator')
49
+
50
+ See CLAUDE.md for complete orchestrator directives.
51
+ """
52
+ )
53
+
54
+ # Load orchestration rules
55
+ rules_file = package_dir / "orchestration.md"
56
+ orchestration_rules = ""
57
+ if rules_file.exists():
58
+ orchestration_rules = rules_file.read_text(encoding="utf-8")
59
+
60
+ # Combine prompts
61
+ combined_prompt = base_prompt
62
+ if orchestration_rules:
63
+ combined_prompt = f"{base_prompt}\n\n---\n\n{orchestration_rules}"
64
+
65
+ # Add dev mode guidance if requested
66
+ if include_dev_mode:
67
+ dev_addendum = textwrap.dedent(
68
+ """
69
+
70
+ ═══════════════════════════════════════════════════════════
71
+ 🔧 DEVELOPMENT MODE - HtmlGraph Project
72
+ ═══════════════════════════════════════════════════════════
73
+
74
+ CRITICAL: Hooks load htmlgraph from PyPI, NOT local source!
75
+
76
+ Development Workflow:
77
+ 1. Make changes to src/python/htmlgraph/
78
+ 2. Run tests: uv run pytest
79
+ 3. Deploy to PyPI: ./scripts/deploy-all.sh X.Y.Z --no-confirm
80
+ 4. Restart Claude Code (hooks auto-load new version from PyPI)
81
+ 5. Verify changes work correctly
82
+
83
+ Why PyPI in Dev Mode?
84
+ - Hooks use: #!/usr/bin/env -S uv run --with htmlgraph
85
+ - Always pulls latest version from PyPI
86
+ - Tests in production-like environment
87
+ - No surprises when distributed to users
88
+ - Single source of truth (PyPI package)
89
+
90
+ Incremental Versioning:
91
+ - Use patch versions: 0.26.2 → 0.26.3 → 0.26.4
92
+ - No need to edit hook shebangs (always get latest)
93
+ - Deploy frequently for rapid iteration
94
+
95
+ Session ID Tracking (v0.26.3+):
96
+ - PostToolUse hooks query for most recent UserQuery session
97
+ - All events should share same session_id
98
+ - Verify: See "Development Mode" section in CLAUDE.md
99
+
100
+ Key References:
101
+ - Development workflow: CLAUDE.md "Development Mode" section
102
+ - Orchestrator patterns: /orchestrator-directives skill
103
+ - Code quality: /code-quality skill
104
+ - Deployment: /deployment-automation skill
105
+
106
+ Remember: You're dogfooding HtmlGraph!
107
+ - Use SDK to track your own work
108
+ - Delegate to spawner agents (Gemini, Codex, Copilot)
109
+ - Follow orchestration patterns
110
+ - Test in production-like environment
111
+
112
+ ═══════════════════════════════════════════════════════════
113
+ """
114
+ )
115
+ combined_prompt += dev_addendum
116
+
117
+ return combined_prompt
118
+
119
+
120
+ def get_prompt_summary() -> dict[str, str]:
121
+ """
122
+ Get summary of available prompt components.
123
+
124
+ Returns:
125
+ Dictionary with component names and their status
126
+ """
127
+ package_dir = Path(__file__).parent.parent
128
+
129
+ prompt_file = package_dir / "orchestrator-system-prompt-optimized.txt"
130
+ rules_file = package_dir / "orchestration.md"
131
+
132
+ return {
133
+ "base_prompt": "✓ Found" if prompt_file.exists() else "✗ Missing",
134
+ "orchestration_rules": "✓ Found" if rules_file.exists() else "✗ Missing",
135
+ "base_prompt_path": str(prompt_file),
136
+ "rules_path": str(rules_file),
137
+ }
@@ -0,0 +1,16 @@
1
+ """AI spawner implementations for multi-AI orchestration."""
2
+
3
+ from .base import AIResult, BaseSpawner
4
+ from .claude import ClaudeSpawner
5
+ from .codex import CodexSpawner
6
+ from .copilot import CopilotSpawner
7
+ from .gemini import GeminiSpawner
8
+
9
+ __all__ = [
10
+ "AIResult",
11
+ "BaseSpawner",
12
+ "ClaudeSpawner",
13
+ "CodexSpawner",
14
+ "CopilotSpawner",
15
+ "GeminiSpawner",
16
+ ]
@@ -0,0 +1,194 @@
1
+ """Base spawner class with common functionality for all AI spawners."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from htmlgraph.orchestration.live_events import LiveEventPublisher
9
+ from htmlgraph.sdk import SDK
10
+
11
+
12
+ @dataclass
13
+ class AIResult:
14
+ """Result from AI CLI execution."""
15
+
16
+ success: bool
17
+ response: str
18
+ tokens_used: int | None
19
+ error: str | None
20
+ raw_output: dict | list | str | None
21
+ tracked_events: list[dict] | None = None # Events tracked in HtmlGraph
22
+
23
+
24
+ class BaseSpawner:
25
+ """
26
+ Base class for AI spawners with common functionality.
27
+
28
+ Provides:
29
+ - Live event publishing for WebSocket streaming
30
+ - SDK initialization with parent session support
31
+ - Common error handling patterns
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize spawner."""
36
+ self._live_publisher: LiveEventPublisher | None = None
37
+
38
+ def _get_live_publisher(self) -> "LiveEventPublisher | None":
39
+ """
40
+ Get LiveEventPublisher instance for real-time WebSocket streaming.
41
+
42
+ Returns None if publisher unavailable (optional dependency).
43
+ """
44
+ if self._live_publisher is None:
45
+ try:
46
+ from htmlgraph.orchestration.live_events import LiveEventPublisher
47
+
48
+ self._live_publisher = LiveEventPublisher()
49
+ except Exception:
50
+ # Live events are optional
51
+ pass
52
+ return self._live_publisher
53
+
54
+ def _publish_live_event(
55
+ self,
56
+ event_type: str,
57
+ spawner_type: str,
58
+ **kwargs: str | int | float | bool | None,
59
+ ) -> None:
60
+ """
61
+ Publish a live event for WebSocket streaming.
62
+
63
+ Silently fails if publisher unavailable (optional feature).
64
+ """
65
+ publisher = self._get_live_publisher()
66
+ if publisher is None:
67
+ return
68
+
69
+ parent_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
70
+
71
+ try:
72
+ if event_type == "spawner_start":
73
+ publisher.spawner_start(
74
+ spawner_type=spawner_type,
75
+ prompt=str(kwargs.get("prompt", "")),
76
+ parent_event_id=parent_event_id,
77
+ model=str(kwargs.get("model", "")) if kwargs.get("model") else None,
78
+ )
79
+ elif event_type == "spawner_phase":
80
+ progress_val = kwargs.get("progress")
81
+ publisher.spawner_phase(
82
+ spawner_type=spawner_type,
83
+ phase=str(kwargs.get("phase", "executing")),
84
+ progress=int(progress_val) if progress_val is not None else None,
85
+ details=str(kwargs.get("details", ""))
86
+ if kwargs.get("details")
87
+ else None,
88
+ parent_event_id=parent_event_id,
89
+ )
90
+ elif event_type == "spawner_complete":
91
+ duration_val = kwargs.get("duration")
92
+ tokens_val = kwargs.get("tokens")
93
+ publisher.spawner_complete(
94
+ spawner_type=spawner_type,
95
+ success=bool(kwargs.get("success", False)),
96
+ duration_seconds=float(duration_val)
97
+ if duration_val is not None
98
+ else None,
99
+ response_preview=str(kwargs.get("response", ""))[:200]
100
+ if kwargs.get("response")
101
+ else None,
102
+ tokens_used=int(tokens_val) if tokens_val is not None else None,
103
+ error=str(kwargs.get("error", "")) if kwargs.get("error") else None,
104
+ parent_event_id=parent_event_id,
105
+ )
106
+ elif event_type == "spawner_tool_use":
107
+ publisher.spawner_tool_use(
108
+ spawner_type=spawner_type,
109
+ tool_name=str(kwargs.get("tool_name", "unknown")),
110
+ parent_event_id=parent_event_id,
111
+ )
112
+ elif event_type == "spawner_message":
113
+ publisher.spawner_message(
114
+ spawner_type=spawner_type,
115
+ message=str(kwargs.get("message", "")),
116
+ role=str(kwargs.get("role", "assistant")),
117
+ parent_event_id=parent_event_id,
118
+ )
119
+ except Exception:
120
+ # Live events should never break spawner execution
121
+ pass
122
+
123
+ def _get_sdk(self) -> "SDK | None":
124
+ """
125
+ Get SDK instance for HtmlGraph tracking with parent session support.
126
+
127
+ Returns None if SDK unavailable.
128
+ """
129
+ try:
130
+ from htmlgraph.sdk import SDK
131
+
132
+ # Read parent session context from environment
133
+ parent_session = os.getenv("HTMLGRAPH_PARENT_SESSION")
134
+ parent_agent = os.getenv("HTMLGRAPH_PARENT_AGENT")
135
+
136
+ # Create SDK with parent session context
137
+ sdk = SDK(
138
+ agent=f"spawner-{parent_agent}" if parent_agent else "spawner",
139
+ parent_session=parent_session, # Pass parent session
140
+ )
141
+
142
+ return sdk
143
+
144
+ except Exception:
145
+ # SDK unavailable or not properly initialized (optional dependency)
146
+ # This happens in test contexts without active sessions
147
+ # Don't log error to avoid noise in tests
148
+ return None
149
+
150
+ def _get_parent_context(self) -> tuple[str | None, int]:
151
+ """
152
+ Get parent activity context for event tracking.
153
+
154
+ Returns:
155
+ Tuple of (parent_activity_id, nesting_depth)
156
+ """
157
+ parent_activity = os.getenv("HTMLGRAPH_PARENT_ACTIVITY")
158
+ nesting_depth_str = os.getenv("HTMLGRAPH_NESTING_DEPTH", "0")
159
+ nesting_depth = int(nesting_depth_str) if nesting_depth_str.isdigit() else 0
160
+ return parent_activity, nesting_depth
161
+
162
+ def _track_activity(
163
+ self,
164
+ sdk: "SDK",
165
+ tool: str,
166
+ summary: str,
167
+ payload: dict[str, Any] | None = None,
168
+ **kwargs: Any,
169
+ ) -> None:
170
+ """
171
+ Track activity in HtmlGraph with parent context.
172
+
173
+ Args:
174
+ sdk: SDK instance
175
+ tool: Tool name
176
+ summary: Activity summary
177
+ payload: Activity payload (will be enriched with parent context)
178
+ **kwargs: Additional arguments for track_activity
179
+ """
180
+ if payload is None:
181
+ payload = {}
182
+
183
+ # Enrich with parent context
184
+ parent_activity, nesting_depth = self._get_parent_context()
185
+ if parent_activity:
186
+ payload["parent_activity"] = parent_activity
187
+ if nesting_depth > 0:
188
+ payload["nesting_depth"] = nesting_depth
189
+
190
+ try:
191
+ sdk.track_activity(tool=tool, summary=summary, payload=payload, **kwargs)
192
+ except Exception:
193
+ # Tracking failure should not break execution
194
+ pass
@@ -0,0 +1,170 @@
1
+ """Claude spawner implementation."""
2
+
3
+ import json
4
+ import subprocess
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .base import AIResult, BaseSpawner
8
+
9
+ if TYPE_CHECKING:
10
+ pass
11
+
12
+
13
+ class ClaudeSpawner(BaseSpawner):
14
+ """
15
+ Spawner for Claude Code CLI.
16
+
17
+ NOTE: Uses same Claude Code authentication as Task() tool, but provides
18
+ isolated execution context. Each call creates a new session without shared
19
+ context. Best for independent tasks or external scripts.
20
+
21
+ For orchestration workflows with shared context, prefer Task() tool which
22
+ leverages prompt caching (5x cheaper for related work).
23
+ """
24
+
25
+ def spawn(
26
+ self,
27
+ prompt: str,
28
+ output_format: str = "json",
29
+ permission_mode: str = "bypassPermissions",
30
+ resume: str | None = None,
31
+ verbose: bool = False,
32
+ timeout: int = 300,
33
+ extra_args: list[str] | None = None,
34
+ ) -> AIResult:
35
+ """
36
+ Spawn Claude in headless mode.
37
+
38
+ NOTE: Uses same Claude Code authentication as Task() tool, but provides
39
+ isolated execution context. Each call creates a new session without shared
40
+ context. Best for independent tasks or external scripts.
41
+
42
+ For orchestration workflows with shared context, prefer Task() tool which
43
+ leverages prompt caching (5x cheaper for related work).
44
+
45
+ Args:
46
+ prompt: Task description for Claude
47
+ output_format: "text" or "json" (stream-json requires --verbose)
48
+ permission_mode: Permission handling mode:
49
+ - "bypassPermissions": Auto-approve all (default)
50
+ - "acceptEdits": Auto-approve edits only
51
+ - "dontAsk": Fail on permission prompts
52
+ - "default": Normal interactive prompts
53
+ - "plan": Plan mode (no execution)
54
+ - "delegate": Delegation mode
55
+ resume: Resume from previous session (--resume). Default: None
56
+ verbose: Enable verbose output (--verbose). Default: False
57
+ timeout: Max seconds (default: 300, Claude can be slow with initialization)
58
+ extra_args: Additional arguments to pass to Claude CLI
59
+
60
+ Returns:
61
+ AIResult with response or error
62
+
63
+ Example:
64
+ >>> spawner = ClaudeSpawner()
65
+ >>> result = spawner.spawn("What is 2+2?")
66
+ >>> if result.success:
67
+ ... print(result.response) # "4"
68
+ ... print(f"Cost: ${result.raw_output['total_cost_usd']}")
69
+ """
70
+ cmd = ["claude", "-p"]
71
+
72
+ if output_format != "text":
73
+ cmd.extend(["--output-format", output_format])
74
+
75
+ if permission_mode:
76
+ cmd.extend(["--permission-mode", permission_mode])
77
+
78
+ # Add resume flag if specified
79
+ if resume:
80
+ cmd.extend(["--resume", resume])
81
+
82
+ # Add verbose flag
83
+ if verbose:
84
+ cmd.append("--verbose")
85
+
86
+ # Add extra args
87
+ if extra_args:
88
+ cmd.extend(extra_args)
89
+
90
+ # Use -- separator to ensure prompt isn't consumed by variadic args
91
+ cmd.append("--")
92
+ cmd.append(prompt)
93
+
94
+ try:
95
+ result = subprocess.run(
96
+ cmd,
97
+ capture_output=True,
98
+ text=True,
99
+ timeout=timeout,
100
+ )
101
+
102
+ if output_format == "json":
103
+ # Parse JSON output
104
+ try:
105
+ output = json.loads(result.stdout)
106
+ except json.JSONDecodeError as e:
107
+ return AIResult(
108
+ success=False,
109
+ response="",
110
+ tokens_used=None,
111
+ error=f"Failed to parse JSON output: {e}",
112
+ raw_output=result.stdout,
113
+ )
114
+
115
+ # Extract result and metadata
116
+ usage = output.get("usage", {})
117
+ tokens = (
118
+ usage.get("input_tokens", 0)
119
+ + usage.get("cache_creation_input_tokens", 0)
120
+ + usage.get("cache_read_input_tokens", 0)
121
+ + usage.get("output_tokens", 0)
122
+ )
123
+
124
+ return AIResult(
125
+ success=output.get("type") == "result"
126
+ and not output.get("is_error"),
127
+ response=output.get("result", ""),
128
+ tokens_used=tokens,
129
+ error=output.get("error") if output.get("is_error") else None,
130
+ raw_output=output,
131
+ )
132
+ else:
133
+ # Plain text output
134
+ return AIResult(
135
+ success=result.returncode == 0,
136
+ response=result.stdout.strip(),
137
+ tokens_used=None,
138
+ error=None if result.returncode == 0 else result.stderr,
139
+ raw_output=result.stdout,
140
+ )
141
+
142
+ except FileNotFoundError:
143
+ return AIResult(
144
+ success=False,
145
+ response="",
146
+ tokens_used=None,
147
+ error="Claude CLI not found. Install Claude Code from: https://claude.com/claude-code",
148
+ raw_output=None,
149
+ )
150
+ except subprocess.TimeoutExpired as e:
151
+ return AIResult(
152
+ success=False,
153
+ response="",
154
+ tokens_used=None,
155
+ error=f"Timed out after {timeout} seconds",
156
+ raw_output={
157
+ "partial_stdout": e.stdout.decode() if e.stdout else None,
158
+ "partial_stderr": e.stderr.decode() if e.stderr else None,
159
+ }
160
+ if e.stdout or e.stderr
161
+ else None,
162
+ )
163
+ except Exception as e:
164
+ return AIResult(
165
+ success=False,
166
+ response="",
167
+ tokens_used=None,
168
+ error=f"Unexpected error: {type(e).__name__}: {e}",
169
+ raw_output=None,
170
+ )