devcouncil 0.2.0 → 0.2.1
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.
- package/README.md +191 -253
- package/bin/devcouncil.js +130 -32
- package/package.json +6 -17
- package/pyproject.toml +0 -66
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -320
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -163
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -208
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -52
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from functools import lru_cache
|
|
2
|
-
from importlib import resources
|
|
3
|
-
from typing import Dict
|
|
4
|
-
|
|
5
|
-
import yaml
|
|
6
|
-
|
|
7
|
-
MODEL_PRICING_RESOURCE = "model_pricing.yaml"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@lru_cache(maxsize=1)
|
|
11
|
-
def load_model_pricing() -> Dict[str, Dict[str, float]]:
|
|
12
|
-
data = resources.files(__package__).joinpath(MODEL_PRICING_RESOURCE).read_text(encoding="utf-8")
|
|
13
|
-
loaded = yaml.safe_load(data) or {}
|
|
14
|
-
return {
|
|
15
|
-
str(model): {
|
|
16
|
-
"prompt_per_1k": float(pricing.get("prompt_per_1k", 0.0)),
|
|
17
|
-
"completion_per_1k": float(pricing.get("completion_per_1k", 0.0)),
|
|
18
|
-
}
|
|
19
|
-
for model, pricing in loaded.items()
|
|
20
|
-
if isinstance(pricing, dict)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def pricing_for_model(model: str, default: Dict[str, float] | None = None) -> Dict[str, float]:
|
|
25
|
-
pricing = load_model_pricing().get(model)
|
|
26
|
-
if pricing is not None:
|
|
27
|
-
return pricing
|
|
28
|
-
return default or {"prompt_per_1k": 0.0, "completion_per_1k": 0.0}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
import uuid
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
TRACE_SCHEMA_VERSION = "devcouncil.trace.v1"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TraceEvent(BaseModel):
|
|
16
|
-
"""Stable JSONL event shape for DevCouncil and external visualizers."""
|
|
17
|
-
|
|
18
|
-
model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True)
|
|
19
|
-
|
|
20
|
-
schema_version: str = Field(TRACE_SCHEMA_VERSION, alias="schema")
|
|
21
|
-
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
22
|
-
timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
23
|
-
runtime: str = "devcouncil"
|
|
24
|
-
type: str
|
|
25
|
-
run_id: Optional[str] = None
|
|
26
|
-
task_id: Optional[str] = None
|
|
27
|
-
summary: str = ""
|
|
28
|
-
details: Dict[str, Any] = Field(default_factory=dict)
|
|
29
|
-
|
|
30
|
-
@classmethod
|
|
31
|
-
def from_legacy(cls, raw: Dict[str, Any]) -> "TraceEvent":
|
|
32
|
-
if raw.get("schema") == TRACE_SCHEMA_VERSION:
|
|
33
|
-
return cls.model_validate(raw)
|
|
34
|
-
raw_details = raw.get("details")
|
|
35
|
-
details: Dict[str, Any] = raw_details if isinstance(raw_details, dict) else {}
|
|
36
|
-
raw_task_id = details.get("task_id")
|
|
37
|
-
raw_summary = details.get("summary")
|
|
38
|
-
raw_run_id = raw.get("run_id")
|
|
39
|
-
return cls(
|
|
40
|
-
schema=TRACE_SCHEMA_VERSION,
|
|
41
|
-
type=str(raw.get("type", "legacy_event")),
|
|
42
|
-
run_id=raw_run_id if isinstance(raw_run_id, str) else None,
|
|
43
|
-
task_id=raw_task_id if isinstance(raw_task_id, str) else None,
|
|
44
|
-
summary=raw_summary if isinstance(raw_summary, str) else "",
|
|
45
|
-
details=details,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class TraceLogger:
|
|
50
|
-
"""Manages appending execution traces to a local log file."""
|
|
51
|
-
|
|
52
|
-
def __init__(self, project_root: Path):
|
|
53
|
-
self.log_dir = project_root / ".devcouncil" / "logs"
|
|
54
|
-
self.trace_file = self.log_dir / "traces.jsonl"
|
|
55
|
-
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
def log_event(
|
|
58
|
-
self,
|
|
59
|
-
event_type: str,
|
|
60
|
-
details: Dict[str, Any],
|
|
61
|
-
run_id: Optional[str] = None,
|
|
62
|
-
task_id: Optional[str] = None,
|
|
63
|
-
summary: str = "",
|
|
64
|
-
) -> TraceEvent:
|
|
65
|
-
"""Append an orchestration event trace."""
|
|
66
|
-
trace = TraceEvent(
|
|
67
|
-
schema=TRACE_SCHEMA_VERSION,
|
|
68
|
-
type=event_type,
|
|
69
|
-
details=details,
|
|
70
|
-
run_id=run_id,
|
|
71
|
-
task_id=task_id,
|
|
72
|
-
summary=summary,
|
|
73
|
-
)
|
|
74
|
-
try:
|
|
75
|
-
with open(self.trace_file, "a", encoding="utf-8") as f:
|
|
76
|
-
f.write(trace.model_dump_json() + "\n")
|
|
77
|
-
except Exception as e:
|
|
78
|
-
logger.debug("Failed to write trace event %s: %s", event_type, e)
|
|
79
|
-
return trace
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def read_trace_events(project_root: Path) -> Iterable[TraceEvent]:
|
|
83
|
-
trace_file = project_root / ".devcouncil" / "logs" / "traces.jsonl"
|
|
84
|
-
if not trace_file.exists():
|
|
85
|
-
return []
|
|
86
|
-
|
|
87
|
-
events: list[TraceEvent] = []
|
|
88
|
-
for line in trace_file.read_text(encoding="utf-8").splitlines():
|
|
89
|
-
if not line.strip():
|
|
90
|
-
continue
|
|
91
|
-
try:
|
|
92
|
-
events.append(TraceEvent.from_legacy(json.loads(line)))
|
|
93
|
-
except Exception as exc:
|
|
94
|
-
logger.debug("Skipping invalid trace line: %s", exc)
|
|
95
|
-
return events
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def read_trace_events_since(
|
|
99
|
-
project_root: Path, cursor: Optional[int] = None
|
|
100
|
-
) -> Tuple[List[TraceEvent], int]:
|
|
101
|
-
"""Incrementally read trace events appended after ``cursor``.
|
|
102
|
-
|
|
103
|
-
The cursor is a byte offset into the trace file, which is robust to appends
|
|
104
|
-
(the file is append-only) and makes repeated polling O(new bytes) rather than
|
|
105
|
-
O(all events). Returns ``(events, next_cursor)`` where ``next_cursor`` should
|
|
106
|
-
be passed back on the next call to fetch only newer events.
|
|
107
|
-
|
|
108
|
-
Never raises: on any error it returns an empty batch and a safe cursor. A
|
|
109
|
-
``cursor`` past the current end-of-file (e.g. the log was rotated/truncated)
|
|
110
|
-
is treated as a reset so the caller still makes progress.
|
|
111
|
-
"""
|
|
112
|
-
trace_file = project_root / ".devcouncil" / "logs" / "traces.jsonl"
|
|
113
|
-
start = cursor if isinstance(cursor, int) and cursor >= 0 else 0
|
|
114
|
-
if not trace_file.exists():
|
|
115
|
-
return [], start
|
|
116
|
-
|
|
117
|
-
try:
|
|
118
|
-
size = trace_file.stat().st_size
|
|
119
|
-
# File shrank (rotation/truncation) — restart from the beginning.
|
|
120
|
-
if start > size:
|
|
121
|
-
start = 0
|
|
122
|
-
|
|
123
|
-
with open(trace_file, "rb") as f:
|
|
124
|
-
f.seek(start)
|
|
125
|
-
chunk = f.read()
|
|
126
|
-
except Exception as exc:
|
|
127
|
-
logger.debug("Failed incremental trace read: %s", exc)
|
|
128
|
-
return [], start
|
|
129
|
-
|
|
130
|
-
# Only consume up to the last complete line so a half-written final line is
|
|
131
|
-
# re-read (not skipped) on the next poll once it is fully flushed.
|
|
132
|
-
last_newline = chunk.rfind(b"\n")
|
|
133
|
-
if last_newline == -1:
|
|
134
|
-
return [], start
|
|
135
|
-
consumed = chunk[: last_newline + 1]
|
|
136
|
-
next_cursor = start + len(consumed)
|
|
137
|
-
|
|
138
|
-
events: List[TraceEvent] = []
|
|
139
|
-
for raw_line in consumed.decode("utf-8", errors="replace").splitlines():
|
|
140
|
-
if not raw_line.strip():
|
|
141
|
-
continue
|
|
142
|
-
try:
|
|
143
|
-
events.append(TraceEvent.from_legacy(json.loads(raw_line)))
|
|
144
|
-
except Exception as exc:
|
|
145
|
-
logger.debug("Skipping invalid trace line: %s", exc)
|
|
146
|
-
return events, next_cursor
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import Dict, Any
|
|
4
|
-
|
|
5
|
-
from devcouncil.telemetry.pricing import pricing_for_model
|
|
6
|
-
|
|
7
|
-
class TelemetryTracker:
|
|
8
|
-
def __init__(self, project_root: Path):
|
|
9
|
-
self.log_file = project_root / ".devcouncil" / "logs" / "telemetry.json"
|
|
10
|
-
self.stats = self._load()
|
|
11
|
-
|
|
12
|
-
def _load(self) -> Dict[str, Any]:
|
|
13
|
-
if self.log_file.exists():
|
|
14
|
-
try:
|
|
15
|
-
with open(self.log_file, "r") as f:
|
|
16
|
-
return json.load(f)
|
|
17
|
-
except Exception:
|
|
18
|
-
pass
|
|
19
|
-
return {"total_cost": 0.0, "total_prompt_tokens": 0, "total_completion_tokens": 0, "models": {}}
|
|
20
|
-
|
|
21
|
-
def _save(self):
|
|
22
|
-
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
with open(self.log_file, "w") as f:
|
|
24
|
-
json.dump(self.stats, f, indent=2)
|
|
25
|
-
|
|
26
|
-
def log_usage(self, model: str, usage: Dict[str, int], *, local: bool = False):
|
|
27
|
-
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
28
|
-
completion_tokens = usage.get("completion_tokens", 0)
|
|
29
|
-
|
|
30
|
-
if local:
|
|
31
|
-
# On-device providers (Ollama) incur no per-token cost. Zero by provider, not
|
|
32
|
-
# by model-id matching — consistent with telemetry/cost.py — so a local tag
|
|
33
|
-
# that happens to collide with a priced entry is never billed.
|
|
34
|
-
cost = 0.0
|
|
35
|
-
else:
|
|
36
|
-
rates = pricing_for_model(model)
|
|
37
|
-
cost = (prompt_tokens / 1000.0) * rates["prompt_per_1k"] + (
|
|
38
|
-
completion_tokens / 1000.0
|
|
39
|
-
) * rates["completion_per_1k"]
|
|
40
|
-
|
|
41
|
-
self.stats["total_cost"] += cost
|
|
42
|
-
self.stats["total_prompt_tokens"] += prompt_tokens
|
|
43
|
-
self.stats["total_completion_tokens"] += completion_tokens
|
|
44
|
-
|
|
45
|
-
if model not in self.stats["models"]:
|
|
46
|
-
self.stats["models"][model] = {"cost": 0.0, "prompt_tokens": 0, "completion_tokens": 0}
|
|
47
|
-
|
|
48
|
-
self.stats["models"][model]["cost"] += cost
|
|
49
|
-
self.stats["models"][model]["prompt_tokens"] += prompt_tokens
|
|
50
|
-
self.stats["models"][model]["completion_tokens"] += completion_tokens
|
|
51
|
-
|
|
52
|
-
self._save()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Live DevCouncil dashboard helpers."""
|
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import secrets
|
|
5
|
-
import time
|
|
6
|
-
from importlib import resources
|
|
7
|
-
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from urllib.parse import urlparse
|
|
10
|
-
|
|
11
|
-
from devcouncil.app.project_status import compute_phase
|
|
12
|
-
from devcouncil.integrations.actions import apply_integration_target
|
|
13
|
-
from devcouncil.integrations.check import build_integration_check_report, integration_status_summary
|
|
14
|
-
from devcouncil.storage.db import get_db
|
|
15
|
-
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository, TaskRepository
|
|
16
|
-
from devcouncil.telemetry.traces import read_trace_events
|
|
17
|
-
|
|
18
|
-
LOGO_ASSET = "devcouncil_logo_premium.png"
|
|
19
|
-
LEGACY_LOGO_ASSET = "devcouncil-logo.svg"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# mtime-keyed cache so the dashboard's poll loop doesn't re-read and re-parse
|
|
23
|
-
# every run manifest on each refresh.
|
|
24
|
-
_RUN_MANIFEST_CACHE: dict[str, tuple[float, dict]] = {}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _load_run_manifest(manifest_path: Path) -> dict | None:
|
|
28
|
-
key = str(manifest_path)
|
|
29
|
-
try:
|
|
30
|
-
mtime = manifest_path.stat().st_mtime
|
|
31
|
-
except OSError:
|
|
32
|
-
return None
|
|
33
|
-
cached = _RUN_MANIFEST_CACHE.get(key)
|
|
34
|
-
if cached is not None and cached[0] == mtime:
|
|
35
|
-
return dict(cached[1])
|
|
36
|
-
try:
|
|
37
|
-
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) or {}
|
|
38
|
-
except (json.JSONDecodeError, OSError):
|
|
39
|
-
return None
|
|
40
|
-
_RUN_MANIFEST_CACHE[key] = (mtime, manifest)
|
|
41
|
-
return dict(manifest)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def recent_run_artifacts(project_root: Path, *, limit: int = 10) -> list[dict]:
|
|
45
|
-
runs_dir = project_root / ".devcouncil" / "runs"
|
|
46
|
-
if not runs_dir.exists():
|
|
47
|
-
return []
|
|
48
|
-
manifests: list[dict] = []
|
|
49
|
-
for manifest_path in sorted(
|
|
50
|
-
runs_dir.glob("*/agent-run.json"),
|
|
51
|
-
key=lambda path: path.stat().st_mtime,
|
|
52
|
-
reverse=True,
|
|
53
|
-
):
|
|
54
|
-
manifest = _load_run_manifest(manifest_path)
|
|
55
|
-
if manifest is None:
|
|
56
|
-
continue
|
|
57
|
-
manifest["manifest_path"] = str(manifest_path)
|
|
58
|
-
manifests.append(manifest)
|
|
59
|
-
if len(manifests) >= limit:
|
|
60
|
-
break
|
|
61
|
-
return manifests
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Integration status probes the filesystem (and optionally CLIs) — far too
|
|
65
|
-
# expensive to recompute on every 2-second dashboard poll.
|
|
66
|
-
_INTEGRATION_SUMMARY_TTL_SECONDS = 5.0
|
|
67
|
-
_INTEGRATION_SUMMARY_CACHE: dict[str, tuple[float, dict]] = {}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _integration_summary_cached(project_root: Path) -> dict:
|
|
71
|
-
key = str(project_root)
|
|
72
|
-
now = time.monotonic()
|
|
73
|
-
cached = _INTEGRATION_SUMMARY_CACHE.get(key)
|
|
74
|
-
if cached is not None and now - cached[0] < _INTEGRATION_SUMMARY_TTL_SECONDS:
|
|
75
|
-
return cached[1]
|
|
76
|
-
summary = integration_status_summary(project_root)
|
|
77
|
-
_INTEGRATION_SUMMARY_CACHE[key] = (now, summary)
|
|
78
|
-
return summary
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _invalidate_integration_summary(project_root: Path) -> None:
|
|
82
|
-
_INTEGRATION_SUMMARY_CACHE.pop(str(project_root), None)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def logo_svg() -> str:
|
|
86
|
-
return resources.files("devcouncil.assets").joinpath(LEGACY_LOGO_ASSET).read_text(encoding="utf-8")
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def logo_asset_bytes() -> bytes:
|
|
90
|
-
return resources.files("devcouncil.assets").joinpath(LOGO_ASSET).read_bytes()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def dashboard_payload(project_root: Path) -> dict:
|
|
94
|
-
db = get_db(project_root)
|
|
95
|
-
if not db:
|
|
96
|
-
return {
|
|
97
|
-
"initialized": False,
|
|
98
|
-
"phase": "UNINITIALIZED",
|
|
99
|
-
"tasks": [],
|
|
100
|
-
"coverage": {},
|
|
101
|
-
"events": [],
|
|
102
|
-
"integrations": _integration_summary_cached(project_root),
|
|
103
|
-
"recent_runs": recent_run_artifacts(project_root),
|
|
104
|
-
}
|
|
105
|
-
with db.get_session() as session:
|
|
106
|
-
graph = ArtifactGraphRepository(session).load_graph()
|
|
107
|
-
state = StateRepository(session).get_state()
|
|
108
|
-
phase = compute_phase(graph, state.current_phase if state else None)
|
|
109
|
-
tasks = [task.model_dump() for task in TaskRepository(session).get_all()]
|
|
110
|
-
return {
|
|
111
|
-
"initialized": True,
|
|
112
|
-
"phase": phase,
|
|
113
|
-
"coverage": graph.coverage_summary(),
|
|
114
|
-
"tasks": tasks,
|
|
115
|
-
"events": [event.model_dump(by_alias=True) for event in list(read_trace_events(project_root))[-50:]],
|
|
116
|
-
"integrations": _integration_summary_cached(project_root),
|
|
117
|
-
"recent_runs": recent_run_artifacts(project_root),
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
|
|
122
|
-
body = json.dumps(payload).encode("utf-8")
|
|
123
|
-
handler.send_response(status)
|
|
124
|
-
handler.send_header("Content-Type", "application/json")
|
|
125
|
-
handler.send_header("Content-Length", str(len(body)))
|
|
126
|
-
handler.end_headers()
|
|
127
|
-
handler.wfile.write(body)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _is_loopback_client(handler: BaseHTTPRequestHandler) -> bool:
|
|
131
|
-
host = handler.client_address[0] if handler.client_address else ""
|
|
132
|
-
return host in {"127.0.0.1", "::1", "localhost"}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def dashboard_apply_payload(
|
|
136
|
-
project_root: Path,
|
|
137
|
-
raw_body: bytes,
|
|
138
|
-
*,
|
|
139
|
-
token: str,
|
|
140
|
-
provided_token: str | None,
|
|
141
|
-
) -> dict:
|
|
142
|
-
if not token or provided_token != token:
|
|
143
|
-
return {"ok": False, "error": "invalid dashboard token"}
|
|
144
|
-
try:
|
|
145
|
-
request = json.loads(raw_body.decode("utf-8") or "{}")
|
|
146
|
-
except json.JSONDecodeError:
|
|
147
|
-
return {"ok": False, "error": "invalid JSON body"}
|
|
148
|
-
if not isinstance(request, dict):
|
|
149
|
-
return {"ok": False, "error": "request body must be a JSON object"}
|
|
150
|
-
target = str(request.get("target") or "").strip()
|
|
151
|
-
include_hooks = bool(request.get("include_hooks", True))
|
|
152
|
-
strict = bool(request.get("strict", False))
|
|
153
|
-
try:
|
|
154
|
-
report = apply_integration_target(
|
|
155
|
-
project_root,
|
|
156
|
-
target,
|
|
157
|
-
include_hooks=include_hooks,
|
|
158
|
-
strict=strict,
|
|
159
|
-
)
|
|
160
|
-
except ValueError as exc:
|
|
161
|
-
return {"ok": False, "error": str(exc)}
|
|
162
|
-
# The next poll should reflect the freshly applied integration.
|
|
163
|
-
_invalidate_integration_summary(project_root)
|
|
164
|
-
return report.as_dict()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def dashboard_html(token: str = "") -> str:
|
|
168
|
-
safe_token = token.replace('"', "")
|
|
169
|
-
return f"""<!doctype html>
|
|
170
|
-
<html lang="en">
|
|
171
|
-
<head>
|
|
172
|
-
<meta charset="utf-8">
|
|
173
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
174
|
-
<meta name="devcouncil-dashboard-token" content="{safe_token}">
|
|
175
|
-
<title>DevCouncil Dashboard</title>
|
|
176
|
-
<style>
|
|
177
|
-
body {{ margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f7f5; color: #202124; }}
|
|
178
|
-
header {{ padding: 18px 28px; border-bottom: 1px solid #d9d9d4; background: #ffffff; display: flex; align-items: center; justify-content: space-between; gap: 18px; }}
|
|
179
|
-
main {{ padding: 24px 28px; display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }}
|
|
180
|
-
section {{ background: #ffffff; border: 1px solid #d9d9d4; border-radius: 8px; padding: 16px; }}
|
|
181
|
-
h1 {{ font-size: 20px; margin: 0; }}
|
|
182
|
-
h2 {{ font-size: 15px; margin: 0 0 12px; }}
|
|
183
|
-
.brand {{ display: flex; align-items: center; gap: 12px; min-width: 0; }}
|
|
184
|
-
.brand img {{ width: 54px; height: 54px; flex: 0 0 auto; filter: drop-shadow(0 8px 12px rgba(12, 36, 52, 0.22)); }}
|
|
185
|
-
.phase-line {{ white-space: nowrap; }}
|
|
186
|
-
.phase {{ font-weight: 700; }}
|
|
187
|
-
table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
|
|
188
|
-
th, td {{ text-align: left; border-bottom: 1px solid #ededeb; padding: 8px; vertical-align: top; }}
|
|
189
|
-
pre {{ margin: 0; white-space: pre-wrap; font-size: 12px; }}
|
|
190
|
-
.toolbar {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }}
|
|
191
|
-
button {{ border: 1px solid #b9b9b2; background: #ffffff; color: #202124; border-radius: 6px; padding: 6px 10px; font: inherit; cursor: pointer; }}
|
|
192
|
-
button:disabled {{ color: #898984; cursor: default; background: #f3f3f0; }}
|
|
193
|
-
.status-pill {{ display: inline-block; min-width: 58px; padding: 2px 6px; border-radius: 999px; font-size: 12px; text-align: center; background: #ededeb; }}
|
|
194
|
-
.status-ok {{ background: #dff3e4; color: #155724; }}
|
|
195
|
-
.status-warn {{ background: #fff3cd; color: #664d03; }}
|
|
196
|
-
.status-fail {{ background: #f8d7da; color: #842029; }}
|
|
197
|
-
#integration-result {{ margin-top: 10px; font-size: 12px; }}
|
|
198
|
-
@media (max-width: 800px) {{ main {{ grid-template-columns: 1fr; padding: 16px; }} header {{ padding: 16px; }} }}
|
|
199
|
-
</style>
|
|
200
|
-
</head>
|
|
201
|
-
<body>
|
|
202
|
-
<header><div class="brand"><img src="/assets/devcouncil_logo_premium.png" alt="DevCouncil logo"><h1>DevCouncil Dashboard</h1></div><div class="phase-line">Phase: <span id="phase" class="phase">loading</span></div></header>
|
|
203
|
-
<main>
|
|
204
|
-
<section><h2>Coverage</h2><pre id="coverage">{{}}</pre></section>
|
|
205
|
-
<section><h2>Tasks</h2><table><thead><tr><th>ID</th><th>Status</th><th>Title</th></tr></thead><tbody id="tasks"></tbody></table></section>
|
|
206
|
-
<section style="grid-column: 1 / -1;">
|
|
207
|
-
<h2>CLI Integrations</h2>
|
|
208
|
-
<div class="toolbar">
|
|
209
|
-
<button type="button" data-target="all">Apply Detected</button>
|
|
210
|
-
<button type="button" data-target="hooks">Install Hooks</button>
|
|
211
|
-
<button type="button" id="run-check">Run Check</button>
|
|
212
|
-
</div>
|
|
213
|
-
<pre id="integration-result"></pre>
|
|
214
|
-
<table>
|
|
215
|
-
<thead><tr><th>Client</th><th>PATH</th><th>MCP</th><th>Hooks</th><th>Launcher</th><th>Configured</th><th>Notes</th><th>Action</th></tr></thead>
|
|
216
|
-
<tbody id="integrations"></tbody>
|
|
217
|
-
</table>
|
|
218
|
-
</section>
|
|
219
|
-
<section style="grid-column: 1 / -1;">
|
|
220
|
-
<h2>Recent Agent Runs</h2>
|
|
221
|
-
<table>
|
|
222
|
-
<thead><tr><th>Run</th><th>Task</th><th>Agent</th><th>Status</th><th>Transcript</th></tr></thead>
|
|
223
|
-
<tbody id="runs"></tbody>
|
|
224
|
-
</table>
|
|
225
|
-
</section>
|
|
226
|
-
<section style="grid-column: 1 / -1;"><h2>Recent Trace Events</h2><pre id="events"></pre></section>
|
|
227
|
-
</main>
|
|
228
|
-
<script>
|
|
229
|
-
function setText(cell, value) {{
|
|
230
|
-
cell.textContent = value == null ? '' : String(value);
|
|
231
|
-
return cell;
|
|
232
|
-
}}
|
|
233
|
-
|
|
234
|
-
function statusClass(value) {{
|
|
235
|
-
if (value === 'ok') return 'status-pill status-ok';
|
|
236
|
-
if (value === 'missing' || value === 'drifted') return 'status-pill status-warn';
|
|
237
|
-
return 'status-pill';
|
|
238
|
-
}}
|
|
239
|
-
|
|
240
|
-
function statusCell(value) {{
|
|
241
|
-
const cell = document.createElement('td');
|
|
242
|
-
const pill = document.createElement('span');
|
|
243
|
-
pill.className = statusClass(value);
|
|
244
|
-
pill.textContent = value || 'n/a';
|
|
245
|
-
cell.appendChild(pill);
|
|
246
|
-
return cell;
|
|
247
|
-
}}
|
|
248
|
-
|
|
249
|
-
const token = document.querySelector('meta[name="devcouncil-dashboard-token"]').content;
|
|
250
|
-
|
|
251
|
-
async function applyIntegration(target) {{
|
|
252
|
-
const resultBox = document.getElementById('integration-result');
|
|
253
|
-
resultBox.textContent = `Running ${{target}}...`;
|
|
254
|
-
const res = await fetch('/api/integrations/apply', {{
|
|
255
|
-
method: 'POST',
|
|
256
|
-
headers: {{
|
|
257
|
-
'Content-Type': 'application/json',
|
|
258
|
-
'X-DevCouncil-Dashboard-Token': token,
|
|
259
|
-
}},
|
|
260
|
-
body: JSON.stringify({{ target }}),
|
|
261
|
-
}});
|
|
262
|
-
const payload = await res.json();
|
|
263
|
-
resultBox.textContent = JSON.stringify(payload, null, 2);
|
|
264
|
-
await refresh();
|
|
265
|
-
}}
|
|
266
|
-
|
|
267
|
-
async function runIntegrationCheck() {{
|
|
268
|
-
const resultBox = document.getElementById('integration-result');
|
|
269
|
-
const res = await fetch('/api/integrations/check');
|
|
270
|
-
const payload = await res.json();
|
|
271
|
-
resultBox.textContent = JSON.stringify(payload, null, 2);
|
|
272
|
-
}}
|
|
273
|
-
|
|
274
|
-
async function refresh() {{
|
|
275
|
-
const res = await fetch('/api/status');
|
|
276
|
-
const data = await res.json();
|
|
277
|
-
document.getElementById('phase').textContent = data.phase;
|
|
278
|
-
document.getElementById('coverage').textContent = JSON.stringify(data.coverage, null, 2);
|
|
279
|
-
const body = document.getElementById('tasks');
|
|
280
|
-
body.replaceChildren(...(data.tasks || []).map(t => {{
|
|
281
|
-
const row = document.createElement('tr');
|
|
282
|
-
row.appendChild(setText(document.createElement('td'), t.id));
|
|
283
|
-
row.appendChild(setText(document.createElement('td'), t.status));
|
|
284
|
-
row.appendChild(setText(document.createElement('td'), t.title));
|
|
285
|
-
return row;
|
|
286
|
-
}}));
|
|
287
|
-
const integrationsBody = document.getElementById('integrations');
|
|
288
|
-
const capabilities = ((data.integrations || {{}}).capabilities || []);
|
|
289
|
-
integrationsBody.replaceChildren(...capabilities.map(item => {{
|
|
290
|
-
const row = document.createElement('tr');
|
|
291
|
-
row.appendChild(setText(document.createElement('td'), item.label || item.name));
|
|
292
|
-
row.appendChild(setText(document.createElement('td'), item.on_path ? 'yes' : 'no'));
|
|
293
|
-
row.appendChild(setText(document.createElement('td'), item.mcp ? 'yes' : 'no'));
|
|
294
|
-
row.appendChild(setText(document.createElement('td'), item.hooks ? 'yes' : 'verification'));
|
|
295
|
-
row.appendChild(setText(document.createElement('td'), item.launcher_shim ? 'yes' : 'no'));
|
|
296
|
-
row.appendChild(statusCell(item.config_status));
|
|
297
|
-
row.appendChild(setText(document.createElement('td'), item.notes || ''));
|
|
298
|
-
const action = document.createElement('td');
|
|
299
|
-
const button = document.createElement('button');
|
|
300
|
-
button.type = 'button';
|
|
301
|
-
button.dataset.target = item.apply_target || item.name;
|
|
302
|
-
button.textContent = item.config_status === 'ok' ? 'Reapply' : 'Fix';
|
|
303
|
-
button.disabled = !item.fixable;
|
|
304
|
-
action.appendChild(button);
|
|
305
|
-
row.appendChild(action);
|
|
306
|
-
return row;
|
|
307
|
-
}}));
|
|
308
|
-
|
|
309
|
-
const runsBody = document.getElementById('runs');
|
|
310
|
-
runsBody.replaceChildren(...(data.recent_runs || []).map(run => {{
|
|
311
|
-
const row = document.createElement('tr');
|
|
312
|
-
row.appendChild(setText(document.createElement('td'), run.run_id));
|
|
313
|
-
row.appendChild(setText(document.createElement('td'), run.task_id));
|
|
314
|
-
row.appendChild(setText(document.createElement('td'), run.agent));
|
|
315
|
-
row.appendChild(setText(document.createElement('td'), run.status || 'unknown'));
|
|
316
|
-
row.appendChild(setText(document.createElement('td'), run.transcript || ''));
|
|
317
|
-
return row;
|
|
318
|
-
}}));
|
|
319
|
-
|
|
320
|
-
document.getElementById('events').textContent = JSON.stringify(data.events || [], null, 2);
|
|
321
|
-
}}
|
|
322
|
-
|
|
323
|
-
document.addEventListener('click', event => {{
|
|
324
|
-
const button = event.target;
|
|
325
|
-
if (!(button instanceof HTMLButtonElement)) return;
|
|
326
|
-
if (button.id === 'run-check') {{
|
|
327
|
-
runIntegrationCheck();
|
|
328
|
-
return;
|
|
329
|
-
}}
|
|
330
|
-
const target = button.dataset.target;
|
|
331
|
-
if (target) applyIntegration(target);
|
|
332
|
-
}});
|
|
333
|
-
|
|
334
|
-
refresh();
|
|
335
|
-
setInterval(refresh, 2000);
|
|
336
|
-
</script>
|
|
337
|
-
</body>
|
|
338
|
-
</html>"""
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def run_dashboard(project_root: Path, host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
342
|
-
dashboard_token = secrets.token_urlsafe(24)
|
|
343
|
-
|
|
344
|
-
class DashboardServer(ThreadingHTTPServer):
|
|
345
|
-
allow_reuse_address = True
|
|
346
|
-
daemon_threads = True
|
|
347
|
-
|
|
348
|
-
class Handler(BaseHTTPRequestHandler):
|
|
349
|
-
def do_GET(self): # noqa: N802
|
|
350
|
-
parsed = urlparse(self.path)
|
|
351
|
-
if parsed.path == f"/assets/{LOGO_ASSET}":
|
|
352
|
-
body = logo_asset_bytes()
|
|
353
|
-
self.send_response(200)
|
|
354
|
-
self.send_header("Content-Type", "image/png")
|
|
355
|
-
self.send_header("Cache-Control", "public, max-age=3600")
|
|
356
|
-
self.send_header("Content-Length", str(len(body)))
|
|
357
|
-
self.end_headers()
|
|
358
|
-
self.wfile.write(body)
|
|
359
|
-
return
|
|
360
|
-
if parsed.path == f"/assets/{LEGACY_LOGO_ASSET}":
|
|
361
|
-
body = logo_svg().encode("utf-8")
|
|
362
|
-
self.send_response(200)
|
|
363
|
-
self.send_header("Content-Type", "image/svg+xml; charset=utf-8")
|
|
364
|
-
self.send_header("Cache-Control", "public, max-age=3600")
|
|
365
|
-
self.send_header("Content-Length", str(len(body)))
|
|
366
|
-
self.end_headers()
|
|
367
|
-
self.wfile.write(body)
|
|
368
|
-
return
|
|
369
|
-
if parsed.path == "/api/integrations/check":
|
|
370
|
-
body = json.dumps(build_integration_check_report(project_root).as_dict()).encode("utf-8")
|
|
371
|
-
self.send_response(200)
|
|
372
|
-
self.send_header("Content-Type", "application/json")
|
|
373
|
-
self.send_header("Content-Length", str(len(body)))
|
|
374
|
-
self.end_headers()
|
|
375
|
-
self.wfile.write(body)
|
|
376
|
-
return
|
|
377
|
-
if parsed.path == "/api/status":
|
|
378
|
-
body = json.dumps(dashboard_payload(project_root)).encode("utf-8")
|
|
379
|
-
self.send_response(200)
|
|
380
|
-
self.send_header("Content-Type", "application/json")
|
|
381
|
-
self.send_header("Content-Length", str(len(body)))
|
|
382
|
-
self.end_headers()
|
|
383
|
-
self.wfile.write(body)
|
|
384
|
-
return
|
|
385
|
-
if parsed.path.startswith("/api/"):
|
|
386
|
-
body = json.dumps({"error": "Not found"}).encode("utf-8")
|
|
387
|
-
self.send_response(404)
|
|
388
|
-
self.send_header("Content-Type", "application/json")
|
|
389
|
-
self.send_header("Content-Length", str(len(body)))
|
|
390
|
-
self.end_headers()
|
|
391
|
-
self.wfile.write(body)
|
|
392
|
-
return
|
|
393
|
-
body = dashboard_html(dashboard_token).encode("utf-8")
|
|
394
|
-
self.send_response(200)
|
|
395
|
-
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
396
|
-
self.send_header("Content-Length", str(len(body)))
|
|
397
|
-
self.end_headers()
|
|
398
|
-
self.wfile.write(body)
|
|
399
|
-
|
|
400
|
-
def do_POST(self): # noqa: N802
|
|
401
|
-
parsed = urlparse(self.path)
|
|
402
|
-
if parsed.path != "/api/integrations/apply":
|
|
403
|
-
_json_response(self, 404, {"ok": False, "error": "Not found"})
|
|
404
|
-
return
|
|
405
|
-
if not _is_loopback_client(self):
|
|
406
|
-
_json_response(self, 403, {"ok": False, "error": "dashboard mutations require loopback client"})
|
|
407
|
-
return
|
|
408
|
-
length = int(self.headers.get("Content-Length") or "0")
|
|
409
|
-
raw_body = self.rfile.read(length)
|
|
410
|
-
provided = self.headers.get("X-DevCouncil-Dashboard-Token")
|
|
411
|
-
payload = dashboard_apply_payload(
|
|
412
|
-
project_root,
|
|
413
|
-
raw_body,
|
|
414
|
-
token=dashboard_token,
|
|
415
|
-
provided_token=provided,
|
|
416
|
-
)
|
|
417
|
-
_json_response(self, 200 if payload.get("ok") else 403, payload)
|
|
418
|
-
|
|
419
|
-
def log_message(self, format, *args): # noqa: A002
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
server = DashboardServer((host, port), Handler)
|
|
423
|
-
server.serve_forever()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Utilities package: redaction, paths, hashing, subprocess helpers."""
|