htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Todo collection for managing persistent todo items.
|
|
5
|
+
|
|
6
|
+
Unlike other collections, TodoCollection provides:
|
|
7
|
+
- Ephemeral-style API matching TodoWrite format
|
|
8
|
+
- Session-scoped todos that persist across context boundaries
|
|
9
|
+
- Automatic linking to current session and feature
|
|
10
|
+
- Bulk sync from TodoWrite format
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from htmlgraph.ids import generate_id
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from htmlgraph.models import Todo
|
|
21
|
+
from htmlgraph.sdk import SDK
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TodoCollection:
|
|
25
|
+
"""
|
|
26
|
+
Collection interface for persistent todos.
|
|
27
|
+
|
|
28
|
+
Provides an API similar to TodoWrite but with persistence.
|
|
29
|
+
Todos are stored as HTML files in `.htmlgraph/todos/`.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> sdk = SDK(agent="claude")
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Create todos (mirrors TodoWrite API)
|
|
35
|
+
>>> sdk.todos.add("Run tests", "Running tests")
|
|
36
|
+
>>> sdk.todos.add("Fix errors", "Fixing errors")
|
|
37
|
+
>>>
|
|
38
|
+
>>> # Start a todo
|
|
39
|
+
>>> sdk.todos.start("todo-abc123")
|
|
40
|
+
>>>
|
|
41
|
+
>>> # Complete a todo
|
|
42
|
+
>>> sdk.todos.complete("todo-abc123")
|
|
43
|
+
>>>
|
|
44
|
+
>>> # List current todos
|
|
45
|
+
>>> pending = sdk.todos.pending()
|
|
46
|
+
>>> in_progress = sdk.todos.in_progress()
|
|
47
|
+
>>>
|
|
48
|
+
>>> # Sync from TodoWrite format
|
|
49
|
+
>>> sdk.todos.sync_from_todowrite([
|
|
50
|
+
... {"content": "Task 1", "status": "pending", "activeForm": "Doing task 1"},
|
|
51
|
+
... {"content": "Task 2", "status": "completed", "activeForm": "Doing task 2"},
|
|
52
|
+
... ])
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, sdk: SDK):
|
|
56
|
+
"""
|
|
57
|
+
Initialize todo collection.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
sdk: Parent SDK instance
|
|
61
|
+
"""
|
|
62
|
+
self._sdk = sdk
|
|
63
|
+
self._todos_dir = sdk._directory / "todos"
|
|
64
|
+
self._todos_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Cache for loaded todos (lazy-loaded)
|
|
67
|
+
self._todos: dict[str, Todo] | None = None
|
|
68
|
+
self._ref_manager: Any = None # Set by SDK during initialization
|
|
69
|
+
|
|
70
|
+
def set_ref_manager(self, ref_manager: Any) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Set the ref manager for this collection.
|
|
73
|
+
|
|
74
|
+
Called by SDK during initialization to enable short ref support.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ref_manager: RefManager instance from SDK
|
|
78
|
+
"""
|
|
79
|
+
self._ref_manager = ref_manager
|
|
80
|
+
|
|
81
|
+
def _ensure_loaded(self) -> dict[str, Todo]:
|
|
82
|
+
"""Load todos from disk if not cached."""
|
|
83
|
+
if self._todos is None:
|
|
84
|
+
self._todos = {}
|
|
85
|
+
self._load_todos()
|
|
86
|
+
return self._todos
|
|
87
|
+
|
|
88
|
+
def _load_todos(self) -> None:
|
|
89
|
+
"""Load all todos from HTML files."""
|
|
90
|
+
from htmlgraph.models import Todo
|
|
91
|
+
from htmlgraph.parser import HtmlParser
|
|
92
|
+
|
|
93
|
+
self._todos = {}
|
|
94
|
+
|
|
95
|
+
for html_file in self._todos_dir.glob("*.html"):
|
|
96
|
+
try:
|
|
97
|
+
parser = HtmlParser(filepath=html_file)
|
|
98
|
+
article = parser.get_article()
|
|
99
|
+
if not article:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Get type to verify this is a todo
|
|
103
|
+
node_type = parser.get_data_attribute(article, "type")
|
|
104
|
+
if node_type != "todo":
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Get all data attributes
|
|
108
|
+
all_attrs = parser.get_all_data_attributes(article)
|
|
109
|
+
|
|
110
|
+
# Parse timestamps
|
|
111
|
+
created = datetime.now()
|
|
112
|
+
if all_attrs.get("created"):
|
|
113
|
+
try:
|
|
114
|
+
created = datetime.fromisoformat(all_attrs["created"])
|
|
115
|
+
except ValueError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
updated = datetime.now()
|
|
119
|
+
if all_attrs.get("updated"):
|
|
120
|
+
try:
|
|
121
|
+
updated = datetime.fromisoformat(all_attrs["updated"])
|
|
122
|
+
except ValueError:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
started_at = None
|
|
126
|
+
if all_attrs.get("started-at"):
|
|
127
|
+
try:
|
|
128
|
+
started_at = datetime.fromisoformat(all_attrs["started-at"])
|
|
129
|
+
except ValueError:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
completed_at = None
|
|
133
|
+
if all_attrs.get("completed-at"):
|
|
134
|
+
try:
|
|
135
|
+
completed_at = datetime.fromisoformat(all_attrs["completed-at"])
|
|
136
|
+
except ValueError:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
# Get content from data attributes
|
|
140
|
+
content = all_attrs.get("todo-content", "")
|
|
141
|
+
active_form = all_attrs.get("todo-active-form", content)
|
|
142
|
+
|
|
143
|
+
# Get status with default
|
|
144
|
+
status = all_attrs.get("status", "pending")
|
|
145
|
+
if status not in ("pending", "in_progress", "completed"):
|
|
146
|
+
status = "pending"
|
|
147
|
+
|
|
148
|
+
todo = Todo(
|
|
149
|
+
id=article.attrs.get("id", html_file.stem),
|
|
150
|
+
content=content,
|
|
151
|
+
active_form=active_form,
|
|
152
|
+
status=status, # type: ignore
|
|
153
|
+
created=created,
|
|
154
|
+
updated=updated,
|
|
155
|
+
started_at=started_at,
|
|
156
|
+
completed_at=completed_at,
|
|
157
|
+
session_id=all_attrs.get("session-id"),
|
|
158
|
+
feature_id=all_attrs.get("feature-id"),
|
|
159
|
+
agent=all_attrs.get("agent"),
|
|
160
|
+
completed_by=all_attrs.get("completed-by"),
|
|
161
|
+
priority=int(all_attrs.get("priority", 0)),
|
|
162
|
+
duration_seconds=float(all_attrs["duration"])
|
|
163
|
+
if all_attrs.get("duration")
|
|
164
|
+
else None,
|
|
165
|
+
)
|
|
166
|
+
self._todos[todo.id] = todo
|
|
167
|
+
except Exception:
|
|
168
|
+
# Skip malformed files
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def _save_todo(self, todo: Todo) -> None:
|
|
172
|
+
"""Save a todo to disk."""
|
|
173
|
+
html_path = self._todos_dir / f"{todo.id}.html"
|
|
174
|
+
html_path.write_text(todo.to_html())
|
|
175
|
+
|
|
176
|
+
def _delete_todo_file(self, todo_id: str) -> bool:
|
|
177
|
+
"""Delete a todo file from disk."""
|
|
178
|
+
html_path = self._todos_dir / f"{todo_id}.html"
|
|
179
|
+
if html_path.exists():
|
|
180
|
+
html_path.unlink()
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def add(
|
|
185
|
+
self,
|
|
186
|
+
content: str,
|
|
187
|
+
active_form: str | None = None,
|
|
188
|
+
feature_id: str | None = None,
|
|
189
|
+
priority: int | None = None,
|
|
190
|
+
) -> Todo:
|
|
191
|
+
"""
|
|
192
|
+
Add a new todo.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
content: The imperative form (e.g., "Run tests")
|
|
196
|
+
active_form: The present continuous form (e.g., "Running tests")
|
|
197
|
+
Defaults to content if not provided
|
|
198
|
+
feature_id: Optional feature to link this todo to
|
|
199
|
+
priority: Order in the list (auto-assigned if not provided)
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The created Todo
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> todo = sdk.todos.add("Fix authentication bug", "Fixing authentication bug")
|
|
206
|
+
"""
|
|
207
|
+
from htmlgraph.models import Todo
|
|
208
|
+
|
|
209
|
+
todos = self._ensure_loaded()
|
|
210
|
+
|
|
211
|
+
# Generate ID
|
|
212
|
+
todo_id = generate_id(node_type="todo", title=content)
|
|
213
|
+
|
|
214
|
+
# Get current session/feature context
|
|
215
|
+
session_id = None
|
|
216
|
+
if (
|
|
217
|
+
hasattr(self._sdk, "session_manager")
|
|
218
|
+
and self._sdk.session_manager._active_session
|
|
219
|
+
):
|
|
220
|
+
session_id = self._sdk.session_manager._active_session.id
|
|
221
|
+
|
|
222
|
+
# Use provided feature_id or try to get from active work
|
|
223
|
+
if not feature_id and hasattr(self._sdk, "session_manager"):
|
|
224
|
+
# Try to get primary feature from session
|
|
225
|
+
active_session = self._sdk.session_manager._active_session
|
|
226
|
+
if active_session and active_session.worked_on:
|
|
227
|
+
feature_id = active_session.worked_on[0]
|
|
228
|
+
|
|
229
|
+
# Auto-assign priority if not provided
|
|
230
|
+
if priority is None:
|
|
231
|
+
priority = len([t for t in todos.values() if t.status != "completed"])
|
|
232
|
+
|
|
233
|
+
todo = Todo(
|
|
234
|
+
id=todo_id,
|
|
235
|
+
content=content,
|
|
236
|
+
active_form=active_form or content,
|
|
237
|
+
status="pending",
|
|
238
|
+
session_id=session_id,
|
|
239
|
+
feature_id=feature_id,
|
|
240
|
+
agent=self._sdk.agent,
|
|
241
|
+
priority=priority,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Save to cache and disk
|
|
245
|
+
todos[todo_id] = todo
|
|
246
|
+
self._save_todo(todo)
|
|
247
|
+
|
|
248
|
+
return todo
|
|
249
|
+
|
|
250
|
+
def get(self, todo_id: str) -> Todo | None:
|
|
251
|
+
"""
|
|
252
|
+
Get a todo by ID.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
todo_id: Todo ID to retrieve
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Todo if found, None otherwise
|
|
259
|
+
"""
|
|
260
|
+
return self._ensure_loaded().get(todo_id)
|
|
261
|
+
|
|
262
|
+
def all(self) -> list[Todo]:
|
|
263
|
+
"""
|
|
264
|
+
Get all todos.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of all todos, ordered by priority
|
|
268
|
+
"""
|
|
269
|
+
todos = list(self._ensure_loaded().values())
|
|
270
|
+
return sorted(todos, key=lambda t: t.priority)
|
|
271
|
+
|
|
272
|
+
def pending(self) -> list[Todo]:
|
|
273
|
+
"""
|
|
274
|
+
Get all pending todos.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
List of pending todos, ordered by priority
|
|
278
|
+
"""
|
|
279
|
+
todos = [t for t in self._ensure_loaded().values() if t.status == "pending"]
|
|
280
|
+
return sorted(todos, key=lambda t: t.priority)
|
|
281
|
+
|
|
282
|
+
def in_progress(self) -> list[Todo]:
|
|
283
|
+
"""
|
|
284
|
+
Get all in-progress todos.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of in-progress todos
|
|
288
|
+
"""
|
|
289
|
+
return [t for t in self._ensure_loaded().values() if t.status == "in_progress"]
|
|
290
|
+
|
|
291
|
+
def completed(self) -> list[Todo]:
|
|
292
|
+
"""
|
|
293
|
+
Get all completed todos.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of completed todos
|
|
297
|
+
"""
|
|
298
|
+
return [t for t in self._ensure_loaded().values() if t.status == "completed"]
|
|
299
|
+
|
|
300
|
+
def start(self, todo_id: str) -> Todo | None:
|
|
301
|
+
"""
|
|
302
|
+
Start working on a todo.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
todo_id: Todo ID to start
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Updated Todo, or None if not found
|
|
309
|
+
"""
|
|
310
|
+
todos = self._ensure_loaded()
|
|
311
|
+
todo = todos.get(todo_id)
|
|
312
|
+
if not todo:
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
todo.start()
|
|
316
|
+
self._save_todo(todo)
|
|
317
|
+
return todo
|
|
318
|
+
|
|
319
|
+
def complete(self, todo_id: str) -> Todo | None:
|
|
320
|
+
"""
|
|
321
|
+
Complete a todo.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
todo_id: Todo ID to complete
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Updated Todo, or None if not found
|
|
328
|
+
"""
|
|
329
|
+
todos = self._ensure_loaded()
|
|
330
|
+
todo = todos.get(todo_id)
|
|
331
|
+
if not todo:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
todo.complete(agent=self._sdk.agent)
|
|
335
|
+
self._save_todo(todo)
|
|
336
|
+
return todo
|
|
337
|
+
|
|
338
|
+
def delete(self, todo_id: str) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Delete a todo.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
todo_id: Todo ID to delete
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
True if deleted, False if not found
|
|
347
|
+
"""
|
|
348
|
+
todos = self._ensure_loaded()
|
|
349
|
+
if todo_id in todos:
|
|
350
|
+
del todos[todo_id]
|
|
351
|
+
return self._delete_todo_file(todo_id)
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
def clear_completed(self) -> int:
|
|
355
|
+
"""
|
|
356
|
+
Remove all completed todos.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Number of todos removed
|
|
360
|
+
"""
|
|
361
|
+
todos = self._ensure_loaded()
|
|
362
|
+
completed_ids = [t.id for t in todos.values() if t.status == "completed"]
|
|
363
|
+
|
|
364
|
+
for todo_id in completed_ids:
|
|
365
|
+
del todos[todo_id]
|
|
366
|
+
self._delete_todo_file(todo_id)
|
|
367
|
+
|
|
368
|
+
return len(completed_ids)
|
|
369
|
+
|
|
370
|
+
def sync_from_todowrite(
|
|
371
|
+
self,
|
|
372
|
+
todowrite_list: list[dict[str, str]],
|
|
373
|
+
feature_id: str | None = None,
|
|
374
|
+
clear_existing: bool = False,
|
|
375
|
+
) -> list[Todo]:
|
|
376
|
+
"""
|
|
377
|
+
Sync todos from TodoWrite format.
|
|
378
|
+
|
|
379
|
+
This enables capturing ephemeral TodoWrite data into persistent storage.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
todowrite_list: List of dicts with 'content', 'status', 'activeForm' keys
|
|
383
|
+
feature_id: Optional feature to link all todos to
|
|
384
|
+
clear_existing: If True, removes existing session todos first
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
List of created/updated todos
|
|
388
|
+
|
|
389
|
+
Example:
|
|
390
|
+
>>> sdk.todos.sync_from_todowrite([
|
|
391
|
+
... {"content": "Run tests", "status": "pending", "activeForm": "Running tests"},
|
|
392
|
+
... {"content": "Fix errors", "status": "completed", "activeForm": "Fixing errors"},
|
|
393
|
+
... ])
|
|
394
|
+
"""
|
|
395
|
+
from htmlgraph.models import Todo
|
|
396
|
+
|
|
397
|
+
todos = self._ensure_loaded()
|
|
398
|
+
|
|
399
|
+
# Get current session
|
|
400
|
+
session_id = None
|
|
401
|
+
if (
|
|
402
|
+
hasattr(self._sdk, "session_manager")
|
|
403
|
+
and self._sdk.session_manager._active_session
|
|
404
|
+
):
|
|
405
|
+
session_id = self._sdk.session_manager._active_session.id
|
|
406
|
+
|
|
407
|
+
# Clear existing session todos if requested
|
|
408
|
+
if clear_existing and session_id:
|
|
409
|
+
session_todo_ids = [
|
|
410
|
+
t.id for t in todos.values() if t.session_id == session_id
|
|
411
|
+
]
|
|
412
|
+
for todo_id in session_todo_ids:
|
|
413
|
+
del todos[todo_id]
|
|
414
|
+
self._delete_todo_file(todo_id)
|
|
415
|
+
|
|
416
|
+
result = []
|
|
417
|
+
for i, item in enumerate(todowrite_list):
|
|
418
|
+
todo_id = generate_id(node_type="todo", title=item.get("content", ""))
|
|
419
|
+
|
|
420
|
+
todo = Todo.from_todowrite(
|
|
421
|
+
todo_dict=item,
|
|
422
|
+
todo_id=todo_id,
|
|
423
|
+
session_id=session_id,
|
|
424
|
+
feature_id=feature_id,
|
|
425
|
+
agent=self._sdk.agent,
|
|
426
|
+
priority=i,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
todos[todo_id] = todo
|
|
430
|
+
self._save_todo(todo)
|
|
431
|
+
result.append(todo)
|
|
432
|
+
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
def to_todowrite_format(self) -> list[dict[str, str]]:
|
|
436
|
+
"""
|
|
437
|
+
Export todos to TodoWrite format.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
List of dicts compatible with TodoWrite tool
|
|
441
|
+
|
|
442
|
+
Example:
|
|
443
|
+
>>> todowrite_list = sdk.todos.to_todowrite_format()
|
|
444
|
+
>>> # Can be used with TodoWrite tool
|
|
445
|
+
"""
|
|
446
|
+
todos = self.all() # Already sorted by priority
|
|
447
|
+
return [t.to_todowrite_format() for t in todos if t.status != "completed"]
|
|
448
|
+
|
|
449
|
+
def for_feature(self, feature_id: str) -> list[Todo]:
|
|
450
|
+
"""
|
|
451
|
+
Get all todos for a specific feature.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
feature_id: Feature ID to filter by
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
List of todos linked to this feature
|
|
458
|
+
"""
|
|
459
|
+
todos = [
|
|
460
|
+
t for t in self._ensure_loaded().values() if t.feature_id == feature_id
|
|
461
|
+
]
|
|
462
|
+
return sorted(todos, key=lambda t: t.priority)
|
|
463
|
+
|
|
464
|
+
def for_session(self, session_id: str) -> list[Todo]:
|
|
465
|
+
"""
|
|
466
|
+
Get all todos for a specific session.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
session_id: Session ID to filter by
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
List of todos from this session
|
|
473
|
+
"""
|
|
474
|
+
todos = [
|
|
475
|
+
t for t in self._ensure_loaded().values() if t.session_id == session_id
|
|
476
|
+
]
|
|
477
|
+
return sorted(todos, key=lambda t: t.priority)
|
|
478
|
+
|
|
479
|
+
def summary(self) -> dict[str, Any]:
|
|
480
|
+
"""
|
|
481
|
+
Get summary statistics for todos.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Dict with counts by status and other stats
|
|
485
|
+
"""
|
|
486
|
+
todos = list(self._ensure_loaded().values())
|
|
487
|
+
|
|
488
|
+
pending_count = sum(1 for t in todos if t.status == "pending")
|
|
489
|
+
in_progress_count = sum(1 for t in todos if t.status == "in_progress")
|
|
490
|
+
completed_count = sum(1 for t in todos if t.status == "completed")
|
|
491
|
+
|
|
492
|
+
# Calculate average completion time
|
|
493
|
+
completed_durations = [
|
|
494
|
+
t.duration_seconds
|
|
495
|
+
for t in todos
|
|
496
|
+
if t.status == "completed" and t.duration_seconds is not None
|
|
497
|
+
]
|
|
498
|
+
avg_duration = (
|
|
499
|
+
sum(completed_durations) / len(completed_durations)
|
|
500
|
+
if completed_durations
|
|
501
|
+
else None
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
"total": len(todos),
|
|
506
|
+
"pending": pending_count,
|
|
507
|
+
"in_progress": in_progress_count,
|
|
508
|
+
"completed": completed_count,
|
|
509
|
+
"completion_rate": completed_count / len(todos) if todos else 0.0,
|
|
510
|
+
"avg_duration_seconds": avg_duration,
|
|
511
|
+
}
|