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.
Files changed (95) hide show
  1. devmemory/__about__.py +3 -0
  2. devmemory/__init__.py +14 -0
  3. devmemory/__main__.py +6 -0
  4. devmemory/adapters/__init__.py +6 -0
  5. devmemory/adapters/databricks.py +346 -0
  6. devmemory/adapters/entire.py +444 -0
  7. devmemory/adapters/git.py +408 -0
  8. devmemory/adapters/graph.py +251 -0
  9. devmemory/adapters/metrics.py +150 -0
  10. devmemory/adapters/tests.py +227 -0
  11. devmemory/analysis/__init__.py +19 -0
  12. devmemory/analysis/base.py +128 -0
  13. devmemory/analysis/chain.py +53 -0
  14. devmemory/analysis/llm.py +236 -0
  15. devmemory/analysis/rules.py +110 -0
  16. devmemory/api/__init__.py +10 -0
  17. devmemory/api/app.py +390 -0
  18. devmemory/api/mappers.py +187 -0
  19. devmemory/api/schemas.py +201 -0
  20. devmemory/cli/__init__.py +1 -0
  21. devmemory/cli/_errors.py +36 -0
  22. devmemory/cli/_render.py +79 -0
  23. devmemory/cli/analytics.py +136 -0
  24. devmemory/cli/analyze.py +58 -0
  25. devmemory/cli/app.py +163 -0
  26. devmemory/cli/checkpoint.py +199 -0
  27. devmemory/cli/compare.py +104 -0
  28. devmemory/cli/doctor.py +151 -0
  29. devmemory/cli/history.py +56 -0
  30. devmemory/cli/impact.py +95 -0
  31. devmemory/cli/init.py +91 -0
  32. devmemory/cli/mcp.py +66 -0
  33. devmemory/cli/memory.py +70 -0
  34. devmemory/cli/restore.py +91 -0
  35. devmemory/cli/search.py +48 -0
  36. devmemory/cli/serve.py +64 -0
  37. devmemory/cli/show.py +139 -0
  38. devmemory/cli/status.py +72 -0
  39. devmemory/cli/task.py +333 -0
  40. devmemory/config.py +302 -0
  41. devmemory/domain/__init__.py +5 -0
  42. devmemory/domain/enums.py +151 -0
  43. devmemory/domain/errors.py +188 -0
  44. devmemory/domain/models.py +452 -0
  45. devmemory/domain/taskloop.py +212 -0
  46. devmemory/environment.py +67 -0
  47. devmemory/logging.py +148 -0
  48. devmemory/mcp/__init__.py +12 -0
  49. devmemory/mcp/server.py +225 -0
  50. devmemory/paths.py +112 -0
  51. devmemory/pipeline/__init__.py +7 -0
  52. devmemory/pipeline/checkpoint.py +443 -0
  53. devmemory/pipeline/feature_detect.py +53 -0
  54. devmemory/pipeline/regression.py +141 -0
  55. devmemory/pipeline/runlog.py +73 -0
  56. devmemory/pipeline/status_rules.py +44 -0
  57. devmemory/py.typed +0 -0
  58. devmemory/services/__init__.py +9 -0
  59. devmemory/services/agent_context.py +287 -0
  60. devmemory/services/analysis.py +116 -0
  61. devmemory/services/analytics.py +328 -0
  62. devmemory/services/brief.py +53 -0
  63. devmemory/services/context.py +88 -0
  64. devmemory/services/databricks_sync.py +121 -0
  65. devmemory/services/features.py +85 -0
  66. devmemory/services/impact.py +47 -0
  67. devmemory/services/memory.py +212 -0
  68. devmemory/services/projects.py +226 -0
  69. devmemory/services/restore.py +194 -0
  70. devmemory/services/taskloop/__init__.py +39 -0
  71. devmemory/services/taskloop/collectors.py +263 -0
  72. devmemory/services/taskloop/engine.py +426 -0
  73. devmemory/services/taskloop/requirements.py +358 -0
  74. devmemory/services/trace.py +152 -0
  75. devmemory/services/versions.py +287 -0
  76. devmemory/storage/__init__.py +9 -0
  77. devmemory/storage/artifacts.py +113 -0
  78. devmemory/storage/db.py +205 -0
  79. devmemory/storage/graph_impacts.py +63 -0
  80. devmemory/storage/migrations/0001_init.sql +15 -0
  81. devmemory/storage/migrations/0002_versions.sql +210 -0
  82. devmemory/storage/migrations/0003_graph.sql +14 -0
  83. devmemory/storage/migrations/0004_taskloop.sql +82 -0
  84. devmemory/storage/migrations/0005_project_brief.sql +12 -0
  85. devmemory/storage/repositories.py +286 -0
  86. devmemory/storage/tasks.py +342 -0
  87. devmemory/storage/versions.py +604 -0
  88. devmemory/web/static/assets/index-CbV5njRH.js +78 -0
  89. devmemory/web/static/assets/index-DD-7ceZx.css +1 -0
  90. devmemory/web/static/index.html +18 -0
  91. devmemory_cli-0.1.0.dev0.dist-info/METADATA +174 -0
  92. devmemory_cli-0.1.0.dev0.dist-info/RECORD +95 -0
  93. devmemory_cli-0.1.0.dev0.dist-info/WHEEL +4 -0
  94. devmemory_cli-0.1.0.dev0.dist-info/entry_points.txt +3 -0
  95. devmemory_cli-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
@@ -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."""
@@ -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"]
@@ -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"]
@@ -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()