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,478 @@
1
+ """Gemini spawner implementation."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from .base import AIResult, BaseSpawner
10
+
11
+ if TYPE_CHECKING:
12
+ from htmlgraph.sdk import SDK
13
+
14
+
15
+ class GeminiSpawner(BaseSpawner):
16
+ """Spawner for Google Gemini CLI.
17
+
18
+ Model Selection:
19
+ The `model` parameter defaults to None, which is the RECOMMENDED approach.
20
+ When model=None, the Gemini CLI automatically selects the best available model
21
+ based on the task and current availability.
22
+
23
+ As of Gemini CLI v0.22+, the default models include:
24
+ - gemini-2.5-flash-lite: Fast, efficient model for most tasks
25
+ - gemini-3-flash-preview: Preview of Gemini 3 with enhanced capabilities
26
+
27
+ Explicitly specifying a model is DISCOURAGED because:
28
+ 1. Older models (gemini-2.0-flash, gemini-1.5-flash) may fail due to
29
+ "thinking mode" incompatibility in newer CLI versions
30
+ 2. Using None automatically benefits from Google's model updates
31
+ 3. The CLI handles model selection and fallback logic
32
+
33
+ Supported models (if you must specify):
34
+ - None (recommended): CLI chooses best available model
35
+ - "gemini-2.5-flash-lite": Fast, efficient
36
+ - "gemini-3-flash-preview": Gemini 3 preview (enhanced capabilities)
37
+ - "gemini-2.5-pro": More capable, slower
38
+
39
+ DEPRECATED models (may cause errors):
40
+ - "gemini-2.0-flash": Deprecated, use None instead
41
+ - "gemini-1.5-flash": Deprecated, use None instead
42
+ - "gemini-1.5-pro": Deprecated, use None instead
43
+ """
44
+
45
+ def _parse_and_track_events(self, jsonl_output: str, sdk: "SDK") -> list[dict]:
46
+ """
47
+ Parse Gemini stream-json events and track in HtmlGraph.
48
+
49
+ Args:
50
+ jsonl_output: JSONL output from Gemini CLI
51
+ sdk: HtmlGraph SDK instance for tracking
52
+
53
+ Returns:
54
+ Parsed events list
55
+ """
56
+ events = []
57
+
58
+ for line in jsonl_output.splitlines():
59
+ if not line.strip():
60
+ continue
61
+
62
+ try:
63
+ event = json.loads(line)
64
+ events.append(event)
65
+
66
+ # Track based on event type
67
+ event_type = event.get("type")
68
+
69
+ if event_type == "tool_use":
70
+ tool_name = event.get("tool_name", "unknown_tool")
71
+ parameters = event.get("parameters", {})
72
+ self._track_activity(
73
+ sdk,
74
+ tool="gemini_tool_call",
75
+ summary=f"Gemini called {tool_name}",
76
+ payload={
77
+ "tool_name": tool_name,
78
+ "parameters": parameters,
79
+ },
80
+ )
81
+
82
+ elif event_type == "tool_result":
83
+ status = event.get("status", "unknown")
84
+ success = status == "success"
85
+ tool_id = event.get("tool_id", "unknown")
86
+ self._track_activity(
87
+ sdk,
88
+ tool="gemini_tool_result",
89
+ summary=f"Gemini tool result: {status}",
90
+ success=success,
91
+ payload={"tool_id": tool_id, "status": status},
92
+ )
93
+
94
+ elif event_type == "message":
95
+ role = event.get("role")
96
+ if role == "assistant":
97
+ content = event.get("content", "")
98
+ # Truncate for summary
99
+ summary = (
100
+ content[:100] + "..." if len(content) > 100 else content
101
+ )
102
+ self._track_activity(
103
+ sdk,
104
+ tool="gemini_message",
105
+ summary=f"Gemini: {summary}",
106
+ payload={"role": role, "content_length": len(content)},
107
+ )
108
+
109
+ elif event_type == "result":
110
+ stats = event.get("stats", {})
111
+ self._track_activity(
112
+ sdk,
113
+ tool="gemini_completion",
114
+ summary="Gemini task completed",
115
+ payload={"stats": stats},
116
+ )
117
+
118
+ except json.JSONDecodeError:
119
+ # Skip malformed lines
120
+ continue
121
+
122
+ return events
123
+
124
+ def spawn(
125
+ self,
126
+ prompt: str,
127
+ output_format: str = "stream-json",
128
+ model: str | None = None,
129
+ include_directories: list[str] | None = None,
130
+ track_in_htmlgraph: bool = True,
131
+ timeout: int = 120,
132
+ tracker: Any = None,
133
+ parent_event_id: str | None = None,
134
+ ) -> AIResult:
135
+ """
136
+ Spawn Gemini in headless mode.
137
+
138
+ Args:
139
+ prompt: Task description for Gemini
140
+ output_format: "json" or "stream-json" (enables real-time tracking)
141
+ model: Model selection. Default: None (RECOMMENDED).
142
+
143
+ When model=None (default), the Gemini CLI automatically selects
144
+ the best available model, which includes:
145
+ - gemini-2.5-flash-lite: Fast, efficient model
146
+ - gemini-3-flash-preview: Gemini 3 with enhanced capabilities
147
+
148
+ Using None is STRONGLY RECOMMENDED because:
149
+ 1. Automatically benefits from Google's latest models
150
+ 2. Avoids deprecation issues with older model names
151
+ 3. CLI handles optimal model selection and fallback
152
+
153
+ DEPRECATED models (may cause errors with CLI v0.22+):
154
+ - gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
155
+
156
+ include_directories: Directories to include for context. Default: None
157
+ track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
158
+ timeout: Max seconds to wait
159
+ tracker: Optional SpawnerEventTracker for recording subprocess invocation
160
+ parent_event_id: Optional parent event ID for event hierarchy
161
+
162
+ Returns:
163
+ AIResult with response, error, and tracked events if tracking enabled
164
+
165
+ Example:
166
+ >>> spawner = GeminiSpawner()
167
+ >>> result = spawner.spawn(
168
+ ... prompt="Analyze this codebase",
169
+ ... # model=None is the default - uses latest Gemini models
170
+ ... track_in_htmlgraph=True
171
+ ... )
172
+ """
173
+ # Initialize tracking if enabled
174
+ sdk: SDK | None = None
175
+ tracked_events: list[dict] = []
176
+ if track_in_htmlgraph:
177
+ sdk = self._get_sdk()
178
+
179
+ # Publish live event: spawner starting
180
+ self._publish_live_event(
181
+ "spawner_start",
182
+ "gemini",
183
+ prompt=prompt,
184
+ model=model,
185
+ )
186
+ start_time = time.time()
187
+
188
+ try:
189
+ # Build command based on tested pattern from spike spk-4029eef3
190
+ cmd = ["gemini", "-p", prompt, "--output-format", output_format]
191
+
192
+ # Add model option if specified
193
+ if model:
194
+ cmd.extend(["-m", model])
195
+
196
+ # Add include directories if specified
197
+ if include_directories:
198
+ for directory in include_directories:
199
+ cmd.extend(["--include-directories", directory])
200
+
201
+ # CRITICAL: Add --yolo for headless mode (auto-approve all tools)
202
+ cmd.append("--yolo")
203
+
204
+ # Track spawner start if SDK available
205
+ if sdk:
206
+ self._track_activity(
207
+ sdk,
208
+ tool="gemini_spawn_start",
209
+ summary=f"Spawning Gemini: {prompt[:80]}",
210
+ payload={"prompt_length": len(prompt), "model": model},
211
+ )
212
+
213
+ # Publish live event: executing
214
+ self._publish_live_event(
215
+ "spawner_phase",
216
+ "gemini",
217
+ phase="executing",
218
+ details="Running Gemini CLI",
219
+ )
220
+
221
+ # Record subprocess invocation if tracker is available
222
+ subprocess_event_id = None
223
+ print(
224
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
225
+ file=sys.stderr,
226
+ )
227
+ if tracker and parent_event_id:
228
+ print(
229
+ "DEBUG: Recording subprocess invocation for Gemini...",
230
+ file=sys.stderr,
231
+ )
232
+ try:
233
+ subprocess_event = tracker.record_tool_call(
234
+ tool_name="subprocess.gemini",
235
+ tool_input={"cmd": cmd},
236
+ phase_event_id=parent_event_id,
237
+ spawned_agent=model or "gemini-default",
238
+ )
239
+ if subprocess_event:
240
+ subprocess_event_id = subprocess_event.get("event_id")
241
+ print(
242
+ f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}",
243
+ file=sys.stderr,
244
+ )
245
+ else:
246
+ print("DEBUG: subprocess_event was None", file=sys.stderr)
247
+ except Exception as e:
248
+ # Tracking failure should not break execution
249
+ print(
250
+ f"DEBUG: Exception recording Gemini subprocess: {e}",
251
+ file=sys.stderr,
252
+ )
253
+ pass
254
+ else:
255
+ print(
256
+ f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
257
+ file=sys.stderr,
258
+ )
259
+
260
+ # Execute with timeout and stderr redirection
261
+ # Note: Cannot use capture_output with stderr parameter
262
+ result = subprocess.run(
263
+ cmd,
264
+ stdout=subprocess.PIPE,
265
+ stderr=subprocess.DEVNULL, # Redirect stderr to avoid polluting JSON
266
+ text=True,
267
+ timeout=timeout,
268
+ )
269
+
270
+ # Complete subprocess invocation tracking
271
+ if tracker and subprocess_event_id:
272
+ try:
273
+ tracker.complete_tool_call(
274
+ event_id=subprocess_event_id,
275
+ output_summary=result.stdout[:500] if result.stdout else "",
276
+ success=result.returncode == 0,
277
+ )
278
+ except Exception:
279
+ # Tracking failure should not break execution
280
+ pass
281
+
282
+ # Publish live event: processing response
283
+ self._publish_live_event(
284
+ "spawner_phase",
285
+ "gemini",
286
+ phase="processing",
287
+ details="Parsing Gemini response",
288
+ )
289
+
290
+ # Check for command execution errors
291
+ if result.returncode != 0:
292
+ duration = time.time() - start_time
293
+ self._publish_live_event(
294
+ "spawner_complete",
295
+ "gemini",
296
+ success=False,
297
+ duration=duration,
298
+ error=f"CLI failed with exit code {result.returncode}",
299
+ )
300
+ return AIResult(
301
+ success=False,
302
+ response="",
303
+ tokens_used=None,
304
+ error=f"Gemini CLI failed with exit code {result.returncode}",
305
+ raw_output=None,
306
+ tracked_events=tracked_events,
307
+ )
308
+
309
+ # Handle stream-json format with real-time tracking
310
+ if output_format == "stream-json" and sdk:
311
+ try:
312
+ tracked_events = self._parse_and_track_events(result.stdout, sdk)
313
+ # Only use stream-json parsing if we got valid events
314
+ if tracked_events:
315
+ # For stream-json, we need to extract response differently
316
+ # Collect all assistant message content, then check result
317
+ response_text = ""
318
+ for event in tracked_events:
319
+ if event.get("type") == "message":
320
+ # Only collect assistant messages
321
+ if event.get("role") == "assistant":
322
+ content = event.get("content", "")
323
+ if content:
324
+ response_text += content
325
+ elif event.get("type") == "result":
326
+ # Result event may have response field (override if present)
327
+ if "response" in event and event["response"]:
328
+ response_text = event["response"]
329
+ # Don't break - we've already collected messages
330
+
331
+ # Token usage from stats in result event
332
+ tokens = None
333
+ for event in tracked_events:
334
+ if event.get("type") == "result":
335
+ stats = event.get("stats", {})
336
+ if stats and "models" in stats:
337
+ total_tokens = 0
338
+ for model_stats in stats["models"].values():
339
+ model_tokens = model_stats.get(
340
+ "tokens", {}
341
+ ).get("total", 0)
342
+ total_tokens += model_tokens
343
+ tokens = total_tokens if total_tokens > 0 else None
344
+ break
345
+
346
+ # Publish live event: complete
347
+ duration = time.time() - start_time
348
+ self._publish_live_event(
349
+ "spawner_complete",
350
+ "gemini",
351
+ success=True,
352
+ duration=duration,
353
+ response=response_text,
354
+ tokens=tokens,
355
+ )
356
+ return AIResult(
357
+ success=True,
358
+ response=response_text,
359
+ tokens_used=tokens,
360
+ error=None,
361
+ raw_output={"events": tracked_events},
362
+ tracked_events=tracked_events,
363
+ )
364
+
365
+ except Exception:
366
+ # Fall back to regular JSON parsing if tracking fails
367
+ pass
368
+
369
+ # Parse JSON response (for json format or fallback)
370
+ try:
371
+ output = json.loads(result.stdout)
372
+ except json.JSONDecodeError as e:
373
+ duration = time.time() - start_time
374
+ self._publish_live_event(
375
+ "spawner_complete",
376
+ "gemini",
377
+ success=False,
378
+ duration=duration,
379
+ error=f"Failed to parse JSON: {e}",
380
+ )
381
+ return AIResult(
382
+ success=False,
383
+ response="",
384
+ tokens_used=None,
385
+ error=f"Failed to parse JSON output: {e}",
386
+ raw_output={"stdout": result.stdout},
387
+ tracked_events=tracked_events,
388
+ )
389
+
390
+ # Extract response and token usage from parsed output
391
+ # Response is at top level in JSON output
392
+ response_text = output.get("response", "")
393
+
394
+ # Token usage is in stats.models (sum across all models)
395
+ tokens = None
396
+ stats = output.get("stats", {})
397
+ if stats and "models" in stats:
398
+ total_tokens = 0
399
+ for model_stats in stats["models"].values():
400
+ model_tokens = model_stats.get("tokens", {}).get("total", 0)
401
+ total_tokens += model_tokens
402
+ tokens = total_tokens if total_tokens > 0 else None
403
+
404
+ # Publish live event: complete
405
+ duration = time.time() - start_time
406
+ self._publish_live_event(
407
+ "spawner_complete",
408
+ "gemini",
409
+ success=True,
410
+ duration=duration,
411
+ response=response_text,
412
+ tokens=tokens,
413
+ )
414
+ return AIResult(
415
+ success=True,
416
+ response=response_text,
417
+ tokens_used=tokens,
418
+ error=None,
419
+ raw_output=output,
420
+ tracked_events=tracked_events,
421
+ )
422
+
423
+ except subprocess.TimeoutExpired as e:
424
+ duration = time.time() - start_time
425
+ self._publish_live_event(
426
+ "spawner_complete",
427
+ "gemini",
428
+ success=False,
429
+ duration=duration,
430
+ error=f"Timed out after {timeout} seconds",
431
+ )
432
+ return AIResult(
433
+ success=False,
434
+ response="",
435
+ tokens_used=None,
436
+ error=f"Gemini CLI timed out after {timeout} seconds",
437
+ raw_output={
438
+ "partial_stdout": e.stdout.decode() if e.stdout else None,
439
+ "partial_stderr": e.stderr.decode() if e.stderr else None,
440
+ }
441
+ if e.stdout or e.stderr
442
+ else None,
443
+ tracked_events=tracked_events,
444
+ )
445
+ except FileNotFoundError:
446
+ duration = time.time() - start_time
447
+ self._publish_live_event(
448
+ "spawner_complete",
449
+ "gemini",
450
+ success=False,
451
+ duration=duration,
452
+ error="CLI not found",
453
+ )
454
+ return AIResult(
455
+ success=False,
456
+ response="",
457
+ tokens_used=None,
458
+ error="Gemini CLI not found. Ensure 'gemini' is installed and in PATH.",
459
+ raw_output=None,
460
+ tracked_events=tracked_events,
461
+ )
462
+ except Exception as e:
463
+ duration = time.time() - start_time
464
+ self._publish_live_event(
465
+ "spawner_complete",
466
+ "gemini",
467
+ success=False,
468
+ duration=duration,
469
+ error=str(e),
470
+ )
471
+ return AIResult(
472
+ success=False,
473
+ response="",
474
+ tokens_used=None,
475
+ error=f"Unexpected error: {type(e).__name__}: {e}",
476
+ raw_output=None,
477
+ tracked_events=tracked_events,
478
+ )
@@ -0,0 +1,33 @@
1
+ """Subprocess execution with standardized error handling.
2
+
3
+ Provides consistent error handling for Claude Code CLI invocations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ import sys
10
+
11
+
12
+ class SubprocessRunner:
13
+ """Execute subprocess commands with error handling."""
14
+
15
+ @staticmethod
16
+ def run_claude_command(cmd: list[str]) -> None:
17
+ """Execute Claude Code CLI command with error handling.
18
+
19
+ Args:
20
+ cmd: Command list (e.g., ["claude", "--resume"])
21
+
22
+ Raises:
23
+ SystemExit: If 'claude' command not found or other error
24
+ """
25
+ try:
26
+ subprocess.run(cmd, check=False)
27
+ except FileNotFoundError:
28
+ print("Error: 'claude' command not found.", file=sys.stderr)
29
+ print(
30
+ "Please install Claude Code CLI: https://code.claude.com",
31
+ file=sys.stderr,
32
+ )
33
+ sys.exit(1)