devmemory-cli 0.1.0.dev0__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.
- devmemory/__about__.py +3 -0
- devmemory/__init__.py +14 -0
- devmemory/__main__.py +6 -0
- devmemory/adapters/__init__.py +6 -0
- devmemory/adapters/databricks.py +346 -0
- devmemory/adapters/entire.py +444 -0
- devmemory/adapters/git.py +408 -0
- devmemory/adapters/graph.py +251 -0
- devmemory/adapters/metrics.py +150 -0
- devmemory/adapters/tests.py +227 -0
- devmemory/analysis/__init__.py +19 -0
- devmemory/analysis/base.py +128 -0
- devmemory/analysis/chain.py +53 -0
- devmemory/analysis/llm.py +236 -0
- devmemory/analysis/rules.py +110 -0
- devmemory/api/__init__.py +10 -0
- devmemory/api/app.py +390 -0
- devmemory/api/mappers.py +187 -0
- devmemory/api/schemas.py +201 -0
- devmemory/cli/__init__.py +1 -0
- devmemory/cli/_errors.py +36 -0
- devmemory/cli/_render.py +79 -0
- devmemory/cli/analytics.py +136 -0
- devmemory/cli/analyze.py +58 -0
- devmemory/cli/app.py +163 -0
- devmemory/cli/checkpoint.py +199 -0
- devmemory/cli/compare.py +104 -0
- devmemory/cli/doctor.py +151 -0
- devmemory/cli/history.py +56 -0
- devmemory/cli/impact.py +95 -0
- devmemory/cli/init.py +91 -0
- devmemory/cli/mcp.py +66 -0
- devmemory/cli/memory.py +70 -0
- devmemory/cli/restore.py +91 -0
- devmemory/cli/search.py +48 -0
- devmemory/cli/serve.py +64 -0
- devmemory/cli/show.py +139 -0
- devmemory/cli/status.py +72 -0
- devmemory/cli/task.py +333 -0
- devmemory/config.py +302 -0
- devmemory/domain/__init__.py +5 -0
- devmemory/domain/enums.py +151 -0
- devmemory/domain/errors.py +188 -0
- devmemory/domain/models.py +452 -0
- devmemory/domain/taskloop.py +212 -0
- devmemory/environment.py +67 -0
- devmemory/logging.py +148 -0
- devmemory/mcp/__init__.py +12 -0
- devmemory/mcp/server.py +225 -0
- devmemory/paths.py +112 -0
- devmemory/pipeline/__init__.py +7 -0
- devmemory/pipeline/checkpoint.py +443 -0
- devmemory/pipeline/feature_detect.py +53 -0
- devmemory/pipeline/regression.py +141 -0
- devmemory/pipeline/runlog.py +73 -0
- devmemory/pipeline/status_rules.py +44 -0
- devmemory/py.typed +0 -0
- devmemory/services/__init__.py +9 -0
- devmemory/services/agent_context.py +287 -0
- devmemory/services/analysis.py +116 -0
- devmemory/services/analytics.py +328 -0
- devmemory/services/brief.py +53 -0
- devmemory/services/context.py +88 -0
- devmemory/services/databricks_sync.py +121 -0
- devmemory/services/features.py +85 -0
- devmemory/services/impact.py +47 -0
- devmemory/services/memory.py +212 -0
- devmemory/services/projects.py +226 -0
- devmemory/services/restore.py +194 -0
- devmemory/services/taskloop/__init__.py +39 -0
- devmemory/services/taskloop/collectors.py +263 -0
- devmemory/services/taskloop/engine.py +426 -0
- devmemory/services/taskloop/requirements.py +358 -0
- devmemory/services/trace.py +152 -0
- devmemory/services/versions.py +287 -0
- devmemory/storage/__init__.py +9 -0
- devmemory/storage/artifacts.py +113 -0
- devmemory/storage/db.py +205 -0
- devmemory/storage/graph_impacts.py +63 -0
- devmemory/storage/migrations/0001_init.sql +15 -0
- devmemory/storage/migrations/0002_versions.sql +210 -0
- devmemory/storage/migrations/0003_graph.sql +14 -0
- devmemory/storage/migrations/0004_taskloop.sql +82 -0
- devmemory/storage/migrations/0005_project_brief.sql +12 -0
- devmemory/storage/repositories.py +286 -0
- devmemory/storage/tasks.py +342 -0
- devmemory/storage/versions.py +604 -0
- devmemory/web/static/assets/index-CbV5njRH.js +78 -0
- devmemory/web/static/assets/index-DD-7ceZx.css +1 -0
- devmemory/web/static/index.html +18 -0
- devmemory_cli-0.1.0.dev0.dist-info/METADATA +174 -0
- devmemory_cli-0.1.0.dev0.dist-info/RECORD +95 -0
- devmemory_cli-0.1.0.dev0.dist-info/WHEEL +4 -0
- devmemory_cli-0.1.0.dev0.dist-info/entry_points.txt +3 -0
- devmemory_cli-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
devmemory/api/schemas.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""API response models. Domain models are reused where they already serialize
|
|
2
|
+
cleanly; these add dashboard-shaped aggregates."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from devmemory.domain.models import ChangedFile, DiffStat
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProjectSummary(BaseModel):
|
|
12
|
+
project_id: str
|
|
13
|
+
name: str
|
|
14
|
+
repo_path: str
|
|
15
|
+
branch: str | None
|
|
16
|
+
head_sha: str | None
|
|
17
|
+
head_subject: str | None
|
|
18
|
+
working_tree_clean: bool
|
|
19
|
+
version_count: int
|
|
20
|
+
latest_version_id: str | None
|
|
21
|
+
latest_status: str | None
|
|
22
|
+
latest_intent: str | None
|
|
23
|
+
head_has_version: bool
|
|
24
|
+
last_regression_id: str | None
|
|
25
|
+
open_features: list[str]
|
|
26
|
+
latest_metrics: dict[str, float | None]
|
|
27
|
+
entire_installed: bool
|
|
28
|
+
entire_enabled: bool
|
|
29
|
+
entire_version: str | None
|
|
30
|
+
entire_agents: list[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VersionListItem(BaseModel):
|
|
34
|
+
version_id: str
|
|
35
|
+
version_number: int
|
|
36
|
+
status: str
|
|
37
|
+
intent: str | None
|
|
38
|
+
agent: str | None
|
|
39
|
+
model: str | None
|
|
40
|
+
feature: str | None
|
|
41
|
+
git_commit: str
|
|
42
|
+
branch: str | None
|
|
43
|
+
files_changed: int
|
|
44
|
+
lines_added: int
|
|
45
|
+
lines_removed: int
|
|
46
|
+
checkpoint_id: str | None
|
|
47
|
+
association_method: str
|
|
48
|
+
association_confidence: float
|
|
49
|
+
metrics: dict[str, float | None]
|
|
50
|
+
tests_passed: int | None
|
|
51
|
+
tests_failed: int | None
|
|
52
|
+
has_regression: bool
|
|
53
|
+
committed_at: str | None
|
|
54
|
+
created_at: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MetricChange(BaseModel):
|
|
58
|
+
name: str
|
|
59
|
+
before: float | None
|
|
60
|
+
after: float | None
|
|
61
|
+
delta: float | None
|
|
62
|
+
unit: str | None
|
|
63
|
+
direction: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ComparisonResponse(BaseModel):
|
|
67
|
+
from_version: str
|
|
68
|
+
to_version: str
|
|
69
|
+
from_commit: str
|
|
70
|
+
to_commit: str
|
|
71
|
+
stat: DiffStat
|
|
72
|
+
files: list[ChangedFile]
|
|
73
|
+
diff_text: str
|
|
74
|
+
metric_changes: list[MetricChange]
|
|
75
|
+
test_changes: dict[str, int | None]
|
|
76
|
+
status_from: str
|
|
77
|
+
status_to: str
|
|
78
|
+
feature_from: str | None = None
|
|
79
|
+
feature_to: str | None = None
|
|
80
|
+
checkpoint_from: str | None = None
|
|
81
|
+
checkpoint_to: str | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FeatureHistoryPoint(BaseModel):
|
|
85
|
+
version_id: str
|
|
86
|
+
version_number: int
|
|
87
|
+
status: str
|
|
88
|
+
metrics: dict[str, float | None]
|
|
89
|
+
committed_at: str | None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class FeatureDetail(BaseModel):
|
|
93
|
+
feature_id: str
|
|
94
|
+
name: str
|
|
95
|
+
status: str
|
|
96
|
+
derived_from: str | None
|
|
97
|
+
version_count: int
|
|
98
|
+
latest_metrics: dict[str, float | None]
|
|
99
|
+
history: list[FeatureHistoryPoint]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SearchHit(BaseModel):
|
|
103
|
+
version_id: str
|
|
104
|
+
version_number: int
|
|
105
|
+
status: str
|
|
106
|
+
intent: str | None
|
|
107
|
+
agent: str | None
|
|
108
|
+
feature: str | None
|
|
109
|
+
git_commit: str
|
|
110
|
+
snippet: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SearchResponse(BaseModel):
|
|
114
|
+
query: str
|
|
115
|
+
count: int
|
|
116
|
+
results: list[SearchHit]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AgentCheckRequest(BaseModel):
|
|
120
|
+
"""Body for ``POST /api/agent/check`` - the pre-flight risk read."""
|
|
121
|
+
|
|
122
|
+
files: list[str] = []
|
|
123
|
+
intent: str | None = None
|
|
124
|
+
feature: str | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --- state-aware coding loop ---------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TaskCreateRequest(BaseModel):
|
|
131
|
+
goal: str
|
|
132
|
+
test_command: str | None = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class IssueRequest(BaseModel):
|
|
136
|
+
description: str
|
|
137
|
+
blocking: bool = False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class RequirementUpdateRequest(BaseModel):
|
|
141
|
+
status: str
|
|
142
|
+
note: str = ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TaskSummary(BaseModel):
|
|
146
|
+
"""One row in the tasks list."""
|
|
147
|
+
|
|
148
|
+
id: str
|
|
149
|
+
goal: str
|
|
150
|
+
status: str
|
|
151
|
+
branch: str
|
|
152
|
+
requirements_total: int
|
|
153
|
+
requirements_complete: int
|
|
154
|
+
test_command: str | None
|
|
155
|
+
updated_at: str | None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SnapshotSummary(BaseModel):
|
|
159
|
+
"""One point on a task's state timeline (the full state stays server-side)."""
|
|
160
|
+
|
|
161
|
+
id: int | None
|
|
162
|
+
overall_status: str
|
|
163
|
+
commit_sha: str | None
|
|
164
|
+
checkpoint_id: str | None
|
|
165
|
+
tests_passed: int
|
|
166
|
+
tests_failed: int
|
|
167
|
+
tests_status: str
|
|
168
|
+
requirements_total: int
|
|
169
|
+
requirements_complete: int
|
|
170
|
+
created_at: str | None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProjectBriefDoc(BaseModel):
|
|
174
|
+
"""The project's single source of truth: one editable markdown document."""
|
|
175
|
+
|
|
176
|
+
content: str
|
|
177
|
+
updated_at: str | None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ProjectBriefRequest(BaseModel):
|
|
181
|
+
content: str
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
__all__ = [
|
|
185
|
+
"AgentCheckRequest",
|
|
186
|
+
"ComparisonResponse",
|
|
187
|
+
"FeatureDetail",
|
|
188
|
+
"FeatureHistoryPoint",
|
|
189
|
+
"IssueRequest",
|
|
190
|
+
"MetricChange",
|
|
191
|
+
"ProjectBriefDoc",
|
|
192
|
+
"ProjectBriefRequest",
|
|
193
|
+
"ProjectSummary",
|
|
194
|
+
"RequirementUpdateRequest",
|
|
195
|
+
"SearchHit",
|
|
196
|
+
"SearchResponse",
|
|
197
|
+
"SnapshotSummary",
|
|
198
|
+
"TaskCreateRequest",
|
|
199
|
+
"TaskSummary",
|
|
200
|
+
"VersionListItem",
|
|
201
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command-line interface. A thin Typer layer over ``devmemory`` services."""
|
devmemory/cli/_errors.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Turn :class:`DevMemoryError` into a clean CLI message + hint + exit code.
|
|
2
|
+
|
|
3
|
+
Applied to every command at registration so ``CliRunner`` (which invokes the
|
|
4
|
+
Typer app directly, not ``main()``) still sees the right exit status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import ParamSpec, TypeVar
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from devmemory.cli._render import err_console
|
|
16
|
+
from devmemory.domain.errors import DevMemoryError
|
|
17
|
+
|
|
18
|
+
_P = ParamSpec("_P")
|
|
19
|
+
_R = TypeVar("_R")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_errors(func: Callable[_P, _R]) -> Callable[_P, _R]:
|
|
23
|
+
@functools.wraps(func)
|
|
24
|
+
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
|
25
|
+
try:
|
|
26
|
+
return func(*args, **kwargs)
|
|
27
|
+
except DevMemoryError as exc:
|
|
28
|
+
err_console.print(f"[bold red]error:[/bold red] {exc.message}")
|
|
29
|
+
if exc.hint:
|
|
30
|
+
err_console.print(f"[dim]hint:[/dim] {exc.hint}")
|
|
31
|
+
raise typer.Exit(code=exc.exit_code) from exc
|
|
32
|
+
|
|
33
|
+
return wrapper
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["handle_errors"]
|
devmemory/cli/_render.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Shared Rich rendering helpers for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from devmemory.domain.enums import VersionStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _force_utf8() -> None:
|
|
17
|
+
"""Windows consoles default to cp1252; make our Unicode output survive pipes."""
|
|
18
|
+
for stream in (sys.stdout, sys.stderr):
|
|
19
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
20
|
+
if callable(reconfigure):
|
|
21
|
+
with contextlib.suppress(ValueError, OSError):
|
|
22
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_force_utf8()
|
|
26
|
+
|
|
27
|
+
console = Console(soft_wrap=False)
|
|
28
|
+
err_console = Console(stderr=True, soft_wrap=False)
|
|
29
|
+
|
|
30
|
+
_STATUS_STYLE: dict[str, str] = {
|
|
31
|
+
VersionStatus.SUCCESS: "bold green",
|
|
32
|
+
VersionStatus.PARTIAL_SUCCESS: "yellow",
|
|
33
|
+
VersionStatus.ERROR: "bold red",
|
|
34
|
+
VersionStatus.REGRESSION: "bold red",
|
|
35
|
+
VersionStatus.IN_PROGRESS: "cyan",
|
|
36
|
+
VersionStatus.NEEDS_REVIEW: "dim",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def status_text(status: str | VersionStatus) -> Text:
|
|
41
|
+
key = str(status)
|
|
42
|
+
return Text(key, style=_STATUS_STYLE.get(key, "white"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check(ok: bool) -> Text:
|
|
46
|
+
return Text("yes", style="green") if ok else Text("no", style="red")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def kv_table(rows: Iterable[tuple[str, object]], *, title: str | None = None) -> Table:
|
|
50
|
+
table = Table(show_header=False, box=None, pad_edge=False, title=title, title_justify="left")
|
|
51
|
+
table.add_column(style="bold cyan", no_wrap=True)
|
|
52
|
+
table.add_column(overflow="fold")
|
|
53
|
+
for key, value in rows:
|
|
54
|
+
table.add_row(key, value if isinstance(value, Text) else str(value))
|
|
55
|
+
return table
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def hint(text: str) -> None:
|
|
59
|
+
console.print(f"[dim]hint:[/dim] {text}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def success(text: str) -> None:
|
|
63
|
+
console.print(f"[bold green]✓[/bold green] {text}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def warn(text: str) -> None:
|
|
67
|
+
console.print(f"[yellow]![/yellow] {text}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"check",
|
|
72
|
+
"console",
|
|
73
|
+
"err_console",
|
|
74
|
+
"hint",
|
|
75
|
+
"kv_table",
|
|
76
|
+
"status_text",
|
|
77
|
+
"success",
|
|
78
|
+
"warn",
|
|
79
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""``devmemory analytics`` and ``devmemory databricks``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from devmemory.cli._render import console, hint, success, warn
|
|
13
|
+
from devmemory.services.analytics import analytics_summary
|
|
14
|
+
from devmemory.services.context import ProjectContext
|
|
15
|
+
from devmemory.services.databricks_sync import drain, sync_status
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def analytics_command(
|
|
19
|
+
as_json: Annotated[bool, typer.Option("--json")] = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Development intelligence: regressions, feature attempts, file churn, agents, trend."""
|
|
22
|
+
with ProjectContext.load() as ctx:
|
|
23
|
+
summary = analytics_summary(ctx)
|
|
24
|
+
|
|
25
|
+
if as_json:
|
|
26
|
+
console.print_json(json.dumps(summary.model_dump(mode="json")))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
console.print()
|
|
30
|
+
console.print(
|
|
31
|
+
f"[bold]{summary.project}[/bold] "
|
|
32
|
+
f"[dim]source: {summary.source}[/dim] "
|
|
33
|
+
f"{summary.version_count} versions · {summary.regression_count} regressions · "
|
|
34
|
+
f"{summary.success_rate}% success\n"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if summary.regressions:
|
|
38
|
+
t = Table(title="Regressions", title_justify="left", box=None, header_style="dim")
|
|
39
|
+
t.add_column("")
|
|
40
|
+
t.add_column("sev")
|
|
41
|
+
t.add_column("feature")
|
|
42
|
+
t.add_column("detail", overflow="fold")
|
|
43
|
+
for r in summary.regressions[:10]:
|
|
44
|
+
t.add_row(r.version_id.upper(), r.severity, r.feature or "-", r.detail)
|
|
45
|
+
console.print(t)
|
|
46
|
+
console.print()
|
|
47
|
+
|
|
48
|
+
if summary.features:
|
|
49
|
+
t = Table(title="Feature attempts", title_justify="left", box=None, header_style="dim")
|
|
50
|
+
t.add_column("feature")
|
|
51
|
+
t.add_column("attempts", justify="right")
|
|
52
|
+
t.add_column("success", justify="right")
|
|
53
|
+
t.add_column("regressions", justify="right")
|
|
54
|
+
for feat in summary.features:
|
|
55
|
+
t.add_row(
|
|
56
|
+
feat.feature, str(feat.attempts), f"{feat.success_rate}%", str(feat.regressions)
|
|
57
|
+
)
|
|
58
|
+
console.print(t)
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
if summary.file_churn:
|
|
62
|
+
t = Table(title="File churn", title_justify="left", box=None, header_style="dim")
|
|
63
|
+
t.add_column("file")
|
|
64
|
+
t.add_column("changes", justify="right")
|
|
65
|
+
t.add_column("adverse", justify="right")
|
|
66
|
+
for churn in summary.file_churn[:10]:
|
|
67
|
+
t.add_row(churn.path, str(churn.changes), str(churn.adverse_changes))
|
|
68
|
+
console.print(t)
|
|
69
|
+
console.print()
|
|
70
|
+
|
|
71
|
+
if summary.agents:
|
|
72
|
+
t = Table(title="Agent effectiveness", title_justify="left", box=None, header_style="dim")
|
|
73
|
+
t.add_column("agent")
|
|
74
|
+
t.add_column("versions", justify="right")
|
|
75
|
+
t.add_column("success", justify="right")
|
|
76
|
+
t.add_column("tokens/success", justify="right")
|
|
77
|
+
for row in summary.agents:
|
|
78
|
+
t.add_row(
|
|
79
|
+
row.agent,
|
|
80
|
+
str(row.versions),
|
|
81
|
+
f"{row.success_rate}%",
|
|
82
|
+
f"{row.tokens_per_success / 1000:.0f}k" if row.tokens_per_success else "-",
|
|
83
|
+
)
|
|
84
|
+
console.print(t)
|
|
85
|
+
|
|
86
|
+
if summary.failed_approaches:
|
|
87
|
+
console.print()
|
|
88
|
+
for fa in summary.failed_approaches[:5]:
|
|
89
|
+
console.print(
|
|
90
|
+
f"[red]repeatedly failed[/red] ({fa.occurrences}x) "
|
|
91
|
+
f"{', '.join(fa.signature[:3])} [dim]{fa.example_intent or ''}[/dim]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
databricks_app = typer.Typer(help="Databricks analytics sync.", no_args_is_help=True)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@databricks_app.command("status")
|
|
99
|
+
def databricks_status() -> None:
|
|
100
|
+
"""Show Databricks configuration and the pending outbox."""
|
|
101
|
+
with ProjectContext.load() as ctx:
|
|
102
|
+
s = sync_status(ctx)
|
|
103
|
+
console.print(
|
|
104
|
+
f"configured: {s.configured}\n"
|
|
105
|
+
f"detail: {s.detail}\n"
|
|
106
|
+
f"queued: {len(s.queued)}"
|
|
107
|
+
+ (f" ({', '.join(x.upper() for x in s.queued)})" if s.queued else "")
|
|
108
|
+
)
|
|
109
|
+
if not s.configured:
|
|
110
|
+
hint(
|
|
111
|
+
"Set DATABRICKS_HOST, DATABRICKS_TOKEN, DATABRICKS_WAREHOUSE_ID, then `devmemory databricks push`."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@databricks_app.command("push")
|
|
116
|
+
def databricks_push() -> None:
|
|
117
|
+
"""Publish every queued development version to Databricks."""
|
|
118
|
+
with ProjectContext.load() as ctx:
|
|
119
|
+
result = drain(ctx)
|
|
120
|
+
if not result.configured:
|
|
121
|
+
warn(result.detail or "Databricks is not configured.")
|
|
122
|
+
return
|
|
123
|
+
if result.pushed:
|
|
124
|
+
success(f"published {len(result.pushed)}: {', '.join(x.upper() for x in result.pushed)}")
|
|
125
|
+
if result.failed:
|
|
126
|
+
warn(f"failed: {', '.join(result.failed)} — {result.detail}")
|
|
127
|
+
if not result.pushed and not result.failed:
|
|
128
|
+
if result.detail:
|
|
129
|
+
warn(result.detail)
|
|
130
|
+
else:
|
|
131
|
+
console.print("[dim]nothing queued[/dim]")
|
|
132
|
+
if result.queued:
|
|
133
|
+
console.print(Panel(", ".join(x.upper() for x in result.queued), title="still queued"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = ["analytics_command", "databricks_app"]
|
devmemory/cli/analyze.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""``devmemory analyze <version>`` - (re)generate the AI analysis for a version."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from devmemory.cli._render import console, warn
|
|
12
|
+
from devmemory.services.analysis import analyze_version
|
|
13
|
+
from devmemory.services.context import ProjectContext
|
|
14
|
+
|
|
15
|
+
_RISK_STYLE = {"low": "green", "medium": "yellow", "high": "bold red"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def analyze_command(
|
|
19
|
+
version: Annotated[str, typer.Argument(help="Version ref: v7, 7, or a commit prefix.")],
|
|
20
|
+
provider: Annotated[
|
|
21
|
+
list[str] | None,
|
|
22
|
+
typer.Option("--provider", "-p", help="Override the provider chain (repeatable)."),
|
|
23
|
+
] = None,
|
|
24
|
+
no_save: Annotated[
|
|
25
|
+
bool, typer.Option("--no-save", help="Print the analysis without storing it.")
|
|
26
|
+
] = False,
|
|
27
|
+
as_json: Annotated[bool, typer.Option("--json", help="Emit as JSON.")] = False,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Run the analysis provider chain for a version. Interpretation only - it can
|
|
30
|
+
never change the recorded facts."""
|
|
31
|
+
with ProjectContext.load() as ctx:
|
|
32
|
+
analysis = analyze_version(ctx, version, providers=provider or None, persist=not no_save)
|
|
33
|
+
|
|
34
|
+
if as_json:
|
|
35
|
+
console.print_json(json.dumps(analysis.model_dump(mode="json")))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
risk = (analysis.risk or "unknown").lower()
|
|
39
|
+
console.print()
|
|
40
|
+
console.print(
|
|
41
|
+
f"[bold cyan]{version.upper()}[/bold cyan] "
|
|
42
|
+
f"[dim]{analysis.provider}"
|
|
43
|
+
f"{f' · {analysis.model}' if analysis.model else ''}[/dim] "
|
|
44
|
+
f"risk [{_RISK_STYLE.get(risk, 'white')}]{risk}[/{_RISK_STYLE.get(risk, 'white')}]"
|
|
45
|
+
)
|
|
46
|
+
console.print()
|
|
47
|
+
console.print(Panel(analysis.summary or "(no summary)", title="summary", title_align="left"))
|
|
48
|
+
if analysis.reasoning:
|
|
49
|
+
console.print(f"\n[dim]reasoning:[/dim] {analysis.reasoning}")
|
|
50
|
+
if analysis.recommendation:
|
|
51
|
+
console.print(f"\n[bold]recommendation:[/bold] {analysis.recommendation}")
|
|
52
|
+
for w in analysis.warnings:
|
|
53
|
+
console.print(f"[yellow]![/yellow] {w}")
|
|
54
|
+
if no_save:
|
|
55
|
+
warn("not saved (--no-save)")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["analyze_command"]
|
devmemory/cli/app.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""The ``devmemory`` / ``dm`` entry point.
|
|
2
|
+
|
|
3
|
+
Thin Typer layer: each command lives in its own module and is registered here.
|
|
4
|
+
Top-level error handling turns :class:`DevMemoryError` into a clean message +
|
|
5
|
+
hint + exit code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Annotated
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from devmemory.__about__ import __version__
|
|
20
|
+
from devmemory.cli._errors import handle_errors
|
|
21
|
+
from devmemory.cli._render import console, err_console
|
|
22
|
+
from devmemory.cli.analytics import analytics_command, databricks_app
|
|
23
|
+
from devmemory.cli.analyze import analyze_command
|
|
24
|
+
from devmemory.cli.checkpoint import checkpoint_command
|
|
25
|
+
from devmemory.cli.compare import compare_command, diff_command
|
|
26
|
+
from devmemory.cli.doctor import doctor_command
|
|
27
|
+
from devmemory.cli.history import history_command
|
|
28
|
+
from devmemory.cli.impact import impact_command
|
|
29
|
+
from devmemory.cli.init import init_command
|
|
30
|
+
from devmemory.cli.mcp import mcp_command
|
|
31
|
+
from devmemory.cli.memory import memory_command
|
|
32
|
+
from devmemory.cli.restore import restore_command
|
|
33
|
+
from devmemory.cli.search import search_command
|
|
34
|
+
from devmemory.cli.serve import serve_command
|
|
35
|
+
from devmemory.cli.show import show_command
|
|
36
|
+
from devmemory.cli.status import status_command
|
|
37
|
+
from devmemory.cli.task import state_command, task_app
|
|
38
|
+
from devmemory.domain.errors import DevMemoryError
|
|
39
|
+
from devmemory.logging import configure_logging
|
|
40
|
+
|
|
41
|
+
app = typer.Typer(
|
|
42
|
+
name="devmemory",
|
|
43
|
+
help=(
|
|
44
|
+
"DevMemory - development-memory and version-intelligence for AI-assisted "
|
|
45
|
+
"software development.\n\n"
|
|
46
|
+
"Git remembers what changed. Entire remembers the AI-assisted context. "
|
|
47
|
+
"DevMemory connects them with results, so you and the next agent can see "
|
|
48
|
+
"the whole story."
|
|
49
|
+
),
|
|
50
|
+
no_args_is_help=True,
|
|
51
|
+
add_completion=False,
|
|
52
|
+
rich_markup_mode="rich",
|
|
53
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_version(value: bool) -> None:
|
|
58
|
+
if value:
|
|
59
|
+
console.print(f"devmemory {__version__}")
|
|
60
|
+
raise typer.Exit()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.callback()
|
|
64
|
+
def _main(
|
|
65
|
+
_version: Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
typer.Option(
|
|
68
|
+
"--version",
|
|
69
|
+
"-V",
|
|
70
|
+
callback=_print_version,
|
|
71
|
+
is_eager=True,
|
|
72
|
+
help="Show the DevMemory version and exit.",
|
|
73
|
+
),
|
|
74
|
+
] = False,
|
|
75
|
+
verbose: Annotated[
|
|
76
|
+
bool, typer.Option("--verbose", "-v", help="Show info-level diagnostic logs.")
|
|
77
|
+
] = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""DevMemory command-line interface."""
|
|
80
|
+
_load_dotenv()
|
|
81
|
+
level = os.environ.get("DEVMEMORY_LOG_LEVEL") or ("INFO" if verbose else "WARNING")
|
|
82
|
+
configure_logging(level=level, json_logs=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _load_dotenv() -> None:
|
|
86
|
+
"""Load a ``.env`` (searched from the cwd upward) so credentials can live in a
|
|
87
|
+
file. Real environment variables always win - ``.env`` never overrides them."""
|
|
88
|
+
from dotenv import find_dotenv, load_dotenv
|
|
89
|
+
|
|
90
|
+
found = find_dotenv(usecwd=True)
|
|
91
|
+
if found:
|
|
92
|
+
load_dotenv(found, override=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _tool_version(executable: str, args: list[str]) -> str:
|
|
96
|
+
path = shutil.which(executable)
|
|
97
|
+
if path is None:
|
|
98
|
+
return "[dim]not found[/dim]"
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run( # noqa: S603 - fixed executable, no shell
|
|
101
|
+
[path, *args],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
timeout=5,
|
|
105
|
+
check=False,
|
|
106
|
+
)
|
|
107
|
+
except (OSError, subprocess.SubprocessError):
|
|
108
|
+
return "[yellow]error[/yellow]"
|
|
109
|
+
output = (result.stdout or result.stderr).strip().splitlines()
|
|
110
|
+
return output[0].strip() if output else "[green]present[/green]"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def version() -> None:
|
|
115
|
+
"""Show the DevMemory version and the toolchain it detects."""
|
|
116
|
+
table = Table(show_header=False, box=None, pad_edge=False)
|
|
117
|
+
table.add_column(style="bold cyan")
|
|
118
|
+
table.add_column()
|
|
119
|
+
table.add_row("DevMemory", __version__)
|
|
120
|
+
table.add_row("Python", platform.python_version())
|
|
121
|
+
table.add_row("Platform", f"{platform.system()} {platform.release()} ({platform.machine()})")
|
|
122
|
+
table.add_row("git", _tool_version("git", ["--version"]))
|
|
123
|
+
table.add_row("entire", _tool_version("entire", ["version"]))
|
|
124
|
+
console.print(table)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
app.command(name="init")(handle_errors(init_command))
|
|
128
|
+
app.command(name="doctor")(handle_errors(doctor_command))
|
|
129
|
+
app.command(name="status")(handle_errors(status_command))
|
|
130
|
+
app.command(name="checkpoint")(handle_errors(checkpoint_command))
|
|
131
|
+
app.command(name="history")(handle_errors(history_command))
|
|
132
|
+
app.command(name="show")(handle_errors(show_command))
|
|
133
|
+
app.command(name="diff")(handle_errors(diff_command))
|
|
134
|
+
app.command(name="compare")(handle_errors(compare_command))
|
|
135
|
+
app.command(name="impact")(handle_errors(impact_command))
|
|
136
|
+
app.command(name="analyze")(handle_errors(analyze_command))
|
|
137
|
+
app.command(name="search")(handle_errors(search_command))
|
|
138
|
+
app.command(name="memory")(handle_errors(memory_command))
|
|
139
|
+
app.command(name="restore")(handle_errors(restore_command))
|
|
140
|
+
app.command(name="analytics")(handle_errors(analytics_command))
|
|
141
|
+
app.add_typer(databricks_app, name="databricks")
|
|
142
|
+
app.command(name="serve")(handle_errors(serve_command))
|
|
143
|
+
app.command(name="mcp")(handle_errors(mcp_command))
|
|
144
|
+
app.add_typer(task_app, name="task")
|
|
145
|
+
app.command(name="state")(handle_errors(state_command))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main() -> None:
|
|
149
|
+
"""Console-script entry point with top-level error handling."""
|
|
150
|
+
try:
|
|
151
|
+
app()
|
|
152
|
+
except DevMemoryError as exc:
|
|
153
|
+
err_console.print(f"[bold red]error:[/bold red] {exc.message}")
|
|
154
|
+
if exc.hint:
|
|
155
|
+
err_console.print(f"[dim]hint:[/dim] {exc.hint}")
|
|
156
|
+
raise SystemExit(exc.exit_code) from exc
|
|
157
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
158
|
+
err_console.print("[dim]interrupted[/dim]")
|
|
159
|
+
raise SystemExit(130) from None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__": # pragma: no cover
|
|
163
|
+
main()
|