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
devmemory/cli/init.py ADDED
@@ -0,0 +1,91 @@
1
+ """``devmemory init`` - set DevMemory up in the current repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from devmemory.adapters.entire import EntireAdapter
12
+ from devmemory.cli._render import check, console, hint, kv_table, success, warn
13
+ from devmemory.domain.models import EntireStatus
14
+ from devmemory.services.projects import init_project
15
+
16
+
17
+ def init_command(
18
+ name: Annotated[str | None, typer.Option("--name", help="Human-readable project name.")] = None,
19
+ project_id: Annotated[
20
+ str | None,
21
+ typer.Option("--project-id", help="Stable slug id (default: derived from name)."),
22
+ ] = None,
23
+ force: Annotated[
24
+ bool, typer.Option("--force", help="Re-create configuration even if already initialized.")
25
+ ] = False,
26
+ as_json: Annotated[bool, typer.Option("--json", help="Emit the init report as JSON.")] = False,
27
+ ) -> None:
28
+ """Initialize DevMemory tracking in the current git repository.
29
+
30
+ Creates a `.devmemory/` directory (config, SQLite database), registers the
31
+ project, and reports what git and Entire look like here.
32
+ """
33
+ repo_path = Path.cwd()
34
+ entire = EntireAdapter(repo_path).probe()
35
+ report = init_project(
36
+ repo_path,
37
+ name=name,
38
+ project_id=project_id,
39
+ force=force,
40
+ entire_probe=entire,
41
+ )
42
+
43
+ if as_json:
44
+ console.print_json(json.dumps(report.model_dump(mode="json")))
45
+ return
46
+
47
+ success(f"DevMemory initialized for [bold]{report.project.name}[/bold]")
48
+ console.print(
49
+ kv_table(
50
+ [
51
+ ("project id", report.project.project_id),
52
+ ("repository", report.project.repo_path),
53
+ ("config", report.config_path),
54
+ ("database", report.db_path),
55
+ ("git", report.git_version or "detected"),
56
+ ("entire", _entire_line(report.entire)),
57
+ ("python", report.environment.python_version),
58
+ ]
59
+ )
60
+ )
61
+
62
+ if report.gitignore_updated:
63
+ console.print("[dim]added DevMemory entries to .gitignore[/dim]")
64
+ if not report.entire.installed:
65
+ warn("Entire CLI not found - versions will be recorded without checkpoint context.")
66
+ hint("Install Entire from https://entire.io, then run `entire enable`.")
67
+ elif not report.entire.enabled:
68
+ warn("Entire is installed but not enabled in this repository.")
69
+ hint("Run `entire enable` so AI sessions are captured as checkpoints.")
70
+
71
+ console.print()
72
+ console.print(
73
+ "Next: make an AI-assisted change, commit it, then run [bold]devmemory checkpoint[/bold]."
74
+ )
75
+
76
+
77
+ def _entire_line(status: EntireStatus) -> object:
78
+ if not status.installed:
79
+ return check(False)
80
+ label = f"{status.cli_version or 'installed'}"
81
+ if status.enabled:
82
+ label += " (enabled"
83
+ if status.agents:
84
+ label += f", {', '.join(status.agents)}"
85
+ label += ")"
86
+ else:
87
+ label += " (not enabled)"
88
+ return label
89
+
90
+
91
+ __all__ = ["init_command"]
devmemory/cli/mcp.py ADDED
@@ -0,0 +1,66 @@
1
+ """``devmemory mcp`` - run the Model Context Protocol server over stdio."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from devmemory.cli._render import err_console
13
+ from devmemory.domain.errors import DevMemoryError
14
+ from devmemory.services.context import ProjectContext
15
+
16
+
17
+ def mcp_command(
18
+ repo: Annotated[
19
+ Path | None,
20
+ typer.Option("--repo", help="Repository to serve (default: discover from the cwd)."),
21
+ ] = None,
22
+ print_config: Annotated[
23
+ bool,
24
+ typer.Option(
25
+ "--print-config",
26
+ help="Print an .mcp.json fragment for this repo and exit (no server).",
27
+ ),
28
+ ] = False,
29
+ ) -> None:
30
+ """Expose this project's development memory to an AI agent over MCP (stdio).
31
+
32
+ Wire it into an MCP client (Claude Code, Cursor, ...) with the fragment from
33
+ `devmemory mcp --print-config`. The server is read-only.
34
+ """
35
+ with ProjectContext.load(repo) as ctx:
36
+ root = ctx.paths.repo_root
37
+
38
+ if print_config:
39
+ fragment = {
40
+ "mcpServers": {
41
+ "devmemory": {
42
+ "command": "devmemory",
43
+ "args": ["mcp", "--repo", str(root)],
44
+ }
45
+ }
46
+ }
47
+ # plain stdout, not Rich - this is meant to be copy-pasted into a JSON file
48
+ sys.stdout.write(json.dumps(fragment, indent=2) + "\n")
49
+ return
50
+
51
+ try:
52
+ import fastmcp # noqa: F401
53
+ except ImportError as exc: # pragma: no cover - extra not installed
54
+ raise DevMemoryError(
55
+ "the 'mcp' extra is not installed",
56
+ hint="pip install 'devmemory[mcp]'",
57
+ ) from exc
58
+
59
+ from devmemory.mcp.server import build_server
60
+
61
+ # stdio transport owns stdout; keep our chatter on stderr.
62
+ err_console.print(f"[dim]devmemory mcp: serving {root} over stdio[/dim]")
63
+ build_server(root).run(transport="stdio", show_banner=False)
64
+
65
+
66
+ __all__ = ["mcp_command"]
@@ -0,0 +1,70 @@
1
+ """``devmemory memory`` - what failed before, so it isn't tried again."""
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, status_text
12
+ from devmemory.services.context import ProjectContext
13
+ from devmemory.services.memory import MemoryQuery, previous_attempts
14
+
15
+
16
+ def memory_command(
17
+ files: Annotated[
18
+ list[str] | None,
19
+ typer.Option("--file", "-F", help="A path being changed (repeatable)."),
20
+ ] = None,
21
+ feature: Annotated[str | None, typer.Option("--feature", "-f", help="Feature area.")] = None,
22
+ intent: Annotated[
23
+ str | None, typer.Option("--intent", "-i", help="What you're about to do.")
24
+ ] = None,
25
+ successes: Annotated[
26
+ bool, typer.Option("--include-successes", help="Also show relevant successful attempts.")
27
+ ] = False,
28
+ limit: Annotated[int, typer.Option("--limit", "-n")] = 10,
29
+ as_json: Annotated[bool, typer.Option("--json")] = False,
30
+ ) -> None:
31
+ """Show previous development attempts relevant to a change - especially failed ones."""
32
+ query = MemoryQuery(
33
+ files=list(files or []),
34
+ feature=feature,
35
+ intent=intent,
36
+ include_successes=successes,
37
+ limit=limit,
38
+ )
39
+ with ProjectContext.load() as ctx:
40
+ attempts = previous_attempts(ctx, query)
41
+
42
+ if as_json:
43
+ console.print_json(json.dumps([a.model_dump(mode="json") for a in attempts]))
44
+ return
45
+
46
+ if not attempts:
47
+ console.print("[dim]No relevant previous attempts. Nothing to avoid — yet.[/dim]")
48
+ return
49
+
50
+ for a in attempts:
51
+ head = f"[bold]{a.version_id.upper()}[/bold] "
52
+ body = (
53
+ f"[dim]intent[/dim] {a.intent or '—'}\n"
54
+ f"[dim]change[/dim] {a.change_summary}\n"
55
+ f"[dim]result[/dim] {a.result}\n"
56
+ f"[dim]matched[/dim] {'; '.join(a.matched_on)}"
57
+ )
58
+ if a.recommendation:
59
+ body += f"\n[dim]advice[/dim] [cyan]{a.recommendation}[/cyan]"
60
+ console.print(
61
+ Panel(
62
+ body,
63
+ title=head + str(status_text(a.status)),
64
+ title_align="left",
65
+ border_style="red" if a.is_adverse else "dim",
66
+ )
67
+ )
68
+
69
+
70
+ __all__ = ["memory_command"]
@@ -0,0 +1,91 @@
1
+ """``devmemory restore`` - move the working tree back to an earlier version, safely."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from devmemory.cli._render import console, hint, kv_table, success, warn
11
+ from devmemory.services.context import ProjectContext
12
+ from devmemory.services.restore import restore_preview, restore_version
13
+
14
+
15
+ def restore_command(
16
+ version: Annotated[str, typer.Argument(help="Version to restore: v7 / 7 / a commit prefix.")],
17
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip the confirmation prompt.")] = False,
18
+ hard: Annotated[
19
+ bool,
20
+ typer.Option(
21
+ "--hard", help="`git reset --hard` instead of a detached checkout (requires -y)."
22
+ ),
23
+ ] = False,
24
+ allow_dirty: Annotated[
25
+ bool,
26
+ typer.Option(
27
+ "--allow-dirty", help="Proceed with uncommitted changes (a safety stash is kept)."
28
+ ),
29
+ ] = False,
30
+ as_json: Annotated[bool, typer.Option("--json")] = False,
31
+ ) -> None:
32
+ """Restore the source tree to the git state of a previous development version.
33
+
34
+ Always previews first. A safety tag is created at the current HEAD (and a
35
+ ``git stash create`` reference if the tree is dirty) before anything moves.
36
+ """
37
+ with ProjectContext.load() as ctx:
38
+ preview = restore_preview(ctx, version)
39
+
40
+ if as_json and not yes:
41
+ console.print_json(json.dumps(preview.model_dump(mode="json")))
42
+ return
43
+
44
+ if preview.already_there:
45
+ success(f"Already at {preview.version_id.upper()} with a clean tree.")
46
+ return
47
+
48
+ console.print()
49
+ console.print(f"[bold]Restore {preview.version_id.upper()}[/bold]")
50
+ console.print(
51
+ kv_table(
52
+ [
53
+ ("target", f"{preview.target_commit[:12]} {preview.target_subject}"),
54
+ (
55
+ "current",
56
+ f"{(preview.current_commit or '-')[:12]}"
57
+ + (f" ({preview.current_branch})" if preview.current_branch else ""),
58
+ ),
59
+ ("mode", "reset --hard" if hard else "detached checkout"),
60
+ ("safety tag", preview.safety_tag),
61
+ ]
62
+ )
63
+ )
64
+ if preview.uncommitted:
65
+ warn(
66
+ f"{len(preview.uncommitted)} uncommitted change(s): {', '.join(preview.uncommitted[:5])}"
67
+ )
68
+ console.print(f"\n[yellow]{preview.warning}[/yellow]\n")
69
+
70
+ if hard and not yes:
71
+ raise typer.BadParameter("--hard requires --yes")
72
+ if not yes and not typer.confirm("Proceed with the restore?"):
73
+ console.print("[dim]cancelled[/dim]")
74
+ raise typer.Exit(1)
75
+
76
+ result = restore_version(
77
+ ctx,
78
+ version,
79
+ mode="hard" if hard else "detach",
80
+ allow_dirty=allow_dirty,
81
+ )
82
+
83
+ if as_json:
84
+ console.print_json(json.dumps(result.model_dump(mode="json")))
85
+ return
86
+ success(result.message)
87
+ if result.stash_ref:
88
+ hint(f"uncommitted work saved as {result.stash_ref[:12]}")
89
+
90
+
91
+ __all__ = ["restore_command"]
@@ -0,0 +1,48 @@
1
+ """``devmemory search`` - full-text search across development history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from devmemory.cli._render import console, status_text
11
+ from devmemory.services.context import ProjectContext
12
+ from devmemory.services.versions import search_versions
13
+
14
+
15
+ def search_command(
16
+ query: Annotated[
17
+ str, typer.Argument(help="Query: authentication, model.py, Codex, learning rate…")
18
+ ],
19
+ limit: Annotated[int, typer.Option("--limit", "-n")] = 25,
20
+ as_json: Annotated[bool, typer.Option("--json")] = False,
21
+ ) -> None:
22
+ """Search versions by intent, agent, feature, files, commit, checkpoint, or analysis."""
23
+ with ProjectContext.load() as ctx:
24
+ results = search_versions(ctx, query, limit=limit)
25
+
26
+ if as_json:
27
+ console.print_json(json.dumps([v.model_dump(mode="json") for v in results]))
28
+ return
29
+
30
+ if not results:
31
+ console.print(f"[dim]No matches for “{query}”.[/dim]")
32
+ return
33
+
34
+ console.print()
35
+ for v in results:
36
+ console.print(
37
+ f"[bold]{v.version_id.upper()}[/bold] ",
38
+ status_text(v.status),
39
+ f" [dim]{v.feature_id.split(':')[-1] if v.feature_id else ''}[/dim]",
40
+ )
41
+ console.print(f" {v.intent or '—'}")
42
+ console.print(
43
+ f" [dim]{v.agent or '—'} · {v.git_commit[:12]} · "
44
+ f"{', '.join(f.path for f in v.changed_files[:3])}[/dim]\n"
45
+ )
46
+
47
+
48
+ __all__ = ["search_command"]
devmemory/cli/serve.py ADDED
@@ -0,0 +1,64 @@
1
+ """``devmemory serve`` - the local dashboard + API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import socket
7
+ import webbrowser
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from devmemory.cli._render import console, success
13
+ from devmemory.services.context import ProjectContext
14
+
15
+
16
+ def serve_command(
17
+ host: Annotated[str | None, typer.Option("--host", help="Bind address.")] = None,
18
+ port: Annotated[int | None, typer.Option("--port", "-p", help="Preferred port.")] = None,
19
+ open_browser: Annotated[
20
+ bool, typer.Option("--open/--no-open", help="Open the dashboard in a browser.")
21
+ ] = True,
22
+ enable_restore: Annotated[
23
+ bool,
24
+ typer.Option("--enable-restore", help="Allow the restore endpoint (off by default)."),
25
+ ] = False,
26
+ reload: Annotated[bool, typer.Option("--reload", hidden=True)] = False,
27
+ ) -> None:
28
+ """Start the DevMemory web dashboard on localhost."""
29
+ # Imported here so `fastapi`/`uvicorn` don't slow down every other command.
30
+ import uvicorn
31
+
32
+ from devmemory.api.app import create_app
33
+
34
+ with ProjectContext.load() as ctx:
35
+ repo_root = ctx.paths.repo_root
36
+ bind_host = host or ctx.config.web.host
37
+ want_port = port or ctx.config.web.port
38
+ allow_restore = enable_restore or ctx.config.web.enable_restore
39
+
40
+ chosen = _pick_port(bind_host, want_port)
41
+ url = f"http://{bind_host}:{chosen}"
42
+
43
+ app = create_app(repo_root, enable_restore=allow_restore)
44
+ success(f"DevMemory dashboard → [bold]{url}[/bold]")
45
+ console.print(f"[dim]API docs: {url}/api/docs · Ctrl-C to stop[/dim]")
46
+ if open_browser:
47
+ with contextlib.suppress(Exception):
48
+ webbrowser.open(url)
49
+
50
+ uvicorn.run(app, host=bind_host, port=chosen, log_level="warning", reload=reload)
51
+
52
+
53
+ def _pick_port(host: str, preferred: int) -> int:
54
+ for candidate in range(preferred, preferred + 20):
55
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
56
+ try:
57
+ sock.bind((host, candidate))
58
+ except OSError:
59
+ continue
60
+ return candidate
61
+ return preferred
62
+
63
+
64
+ __all__ = ["serve_command"]
devmemory/cli/show.py ADDED
@@ -0,0 +1,139 @@
1
+ """``devmemory show <version>`` - the full development record for one 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
+ from rich.syntax import Syntax
11
+
12
+ from devmemory.cli._render import console, kv_table, status_text
13
+ from devmemory.services.context import ProjectContext
14
+ from devmemory.services.versions import get_version
15
+
16
+
17
+ def show_command(
18
+ version: Annotated[str, typer.Argument(help="Version ref: v7, 7, or a commit prefix.")],
19
+ diff: Annotated[bool, typer.Option("--diff", help="Also print the git diff.")] = False,
20
+ as_json: Annotated[bool, typer.Option("--json", help="Emit as JSON.")] = False,
21
+ ) -> None:
22
+ """Show one version: intent, agent, checkpoint, commit, files, tests, metrics, status."""
23
+ with ProjectContext.load() as ctx:
24
+ v = get_version(ctx, version)
25
+ diff_text = ctx.git.diff_text(v.parent_commit, v.git_commit) if diff else None
26
+
27
+ if as_json:
28
+ console.print_json(json.dumps(v.model_dump(mode="json")))
29
+ return
30
+
31
+ console.print()
32
+ console.print(f"[bold cyan]{v.version_id.upper()}[/bold cyan] ", status_text(v.status))
33
+ console.print()
34
+
35
+ rows: list[tuple[str, object]] = [("intent", v.intent or "-")]
36
+ if v.agent:
37
+ rows.append(("agent", f"{v.agent}" + (f" ({v.model})" if v.model else "")))
38
+ if v.primary_checkpoint:
39
+ cp = v.primary_checkpoint
40
+ line = f"{cp.checkpoint_id} [dim]{cp.association_method.value}[/dim]"
41
+ if cp.is_uncertain:
42
+ line += f" [yellow]confidence {cp.association_confidence:.2f}[/yellow]"
43
+ rows.append(("entire", line))
44
+ rows.append(
45
+ ("commit", f"{v.git_commit[:12]} [dim](parent {(v.parent_commit or '-')[:12]})[/dim]")
46
+ )
47
+ if v.branch:
48
+ rows.append(("branch", v.branch))
49
+ if v.feature_id:
50
+ rows.append(("feature", v.feature_id.split(":", 1)[-1]))
51
+ rows.append(
52
+ (
53
+ "changes",
54
+ f"{v.files_changed} files [green]+{v.lines_added}[/green] [red]-{v.lines_removed}[/red]",
55
+ )
56
+ )
57
+ if v.tests and v.tests.ran:
58
+ rows.append(
59
+ (
60
+ "tests",
61
+ f"{v.tests.passed} passed / {v.tests.failed} failed / {v.tests.skipped} skipped",
62
+ )
63
+ )
64
+ if v.artifacts:
65
+ a = v.artifacts[0]
66
+ kb = f"{a.size_bytes / 1024:.0f} KB" if a.size_bytes else "?"
67
+ rows.append(("snapshot", f"{a.path} [dim]{kb}[/dim]"))
68
+ if v.committed_at:
69
+ rows.append(("committed", v.committed_at.isoformat(timespec="minutes")))
70
+ console.print(kv_table(rows))
71
+
72
+ if v.metrics:
73
+ console.print()
74
+ mt = kv_table(
75
+ (
76
+ m.name,
77
+ f"{m.before} → {m.after}"
78
+ + (f" {m.unit}" if m.unit else "")
79
+ + (
80
+ " [green]improvement[/green]"
81
+ if m.is_improvement
82
+ else " [red]worse[/red]"
83
+ if m.is_worse
84
+ else ""
85
+ ),
86
+ )
87
+ for m in v.metrics
88
+ )
89
+ console.print(Panel(mt, title="metrics", title_align="left", border_style="dim"))
90
+
91
+ if v.changed_files:
92
+ console.print()
93
+ files = "\n".join(
94
+ f" {_mark(f.change_type.value)} {f.path}"
95
+ + (
96
+ f" [green]+{f.additions}[/green] [red]-{f.deletions}[/red]"
97
+ if not f.binary
98
+ else " [dim]binary[/dim]"
99
+ )
100
+ for f in v.changed_files
101
+ )
102
+ console.print(Panel(files, title="files", title_align="left", border_style="dim"))
103
+
104
+ if v.regressions:
105
+ console.print()
106
+ for r in v.regressions:
107
+ console.print(f"[bold red]regression[/bold red] {r.metric or r.kind}: {r.detail}")
108
+
109
+ if v.analysis and v.analysis.summary:
110
+ console.print()
111
+ body = v.analysis.summary
112
+ if v.analysis.recommendation:
113
+ body += f"\n\n[bold]recommendation:[/bold] {v.analysis.recommendation}"
114
+ console.print(
115
+ Panel(
116
+ body,
117
+ title=f"analysis ([dim]{v.analysis.provider}[/dim])",
118
+ title_align="left",
119
+ border_style="dim",
120
+ )
121
+ )
122
+
123
+ if diff_text:
124
+ console.print()
125
+ console.print(Syntax(diff_text, "diff", theme="ansi_dark", word_wrap=False))
126
+
127
+
128
+ def _mark(change_type: str) -> str:
129
+ return {
130
+ "added": "[green]A[/green]",
131
+ "modified": "[yellow]M[/yellow]",
132
+ "deleted": "[red]D[/red]",
133
+ "renamed": "[cyan]R[/cyan]",
134
+ "copied": "[cyan]C[/cyan]",
135
+ "type_changed": "[magenta]T[/magenta]",
136
+ }.get(change_type, "?")
137
+
138
+
139
+ __all__ = ["show_command"]
@@ -0,0 +1,72 @@
1
+ """``devmemory status`` - where the project stands right now."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from devmemory.cli._render import check, console, kv_table, warn
11
+ from devmemory.domain.models import EntireStatus
12
+ from devmemory.services.context import ProjectContext
13
+ from devmemory.services.projects import project_status
14
+
15
+
16
+ def status_command(
17
+ as_json: Annotated[bool, typer.Option("--json", help="Emit status as JSON.")] = False,
18
+ ) -> None:
19
+ """Show the current project, git HEAD, working-tree cleanliness, and Entire state."""
20
+ with ProjectContext.load() as ctx:
21
+ report = project_status(ctx, entire_probe=ctx.entire.probe())
22
+
23
+ if as_json:
24
+ console.print_json(json.dumps(report.model_dump(mode="json")))
25
+ return
26
+
27
+ head = report.head_sha[:12] if report.head_sha else "(no commits yet)"
28
+ rows: list[tuple[str, object]] = [
29
+ ("project", f"{report.project.name} [dim]({report.project.project_id})[/dim]"),
30
+ ("branch", report.branch or "[dim]detached[/dim]"),
31
+ ("HEAD", f"{head} {report.head_subject or ''}".rstrip()),
32
+ ("working tree", check(report.working_tree_clean)),
33
+ ("versions", str(report.version_count)),
34
+ ]
35
+ if report.latest_version_id:
36
+ rows.append(
37
+ (
38
+ "latest",
39
+ f"{report.latest_version_id.upper()} {report.latest_status}"
40
+ + (f" [dim]{report.latest_intent}[/dim]" if report.latest_intent else ""),
41
+ )
42
+ )
43
+ if report.latest_metrics:
44
+ rows.append(
45
+ (
46
+ "metrics",
47
+ " ".join(f"{k}={v}" for k, v in report.latest_metrics.items() if v is not None),
48
+ )
49
+ )
50
+ if report.open_features:
51
+ rows.append(("in progress", ", ".join(report.open_features)))
52
+ if report.last_regression_id:
53
+ rows.append(("last regression", report.last_regression_id.upper()))
54
+ rows.append(("entire", _entire_summary(report.entire)))
55
+ console.print(kv_table(rows))
56
+
57
+ if not report.working_tree_clean:
58
+ warn("uncommitted changes present")
59
+ if report.head_sha and not report.head_has_version:
60
+ warn("HEAD is not recorded yet - run `devmemory checkpoint`")
61
+
62
+
63
+ def _entire_summary(status: EntireStatus) -> str:
64
+ if not status.installed:
65
+ return "not installed"
66
+ if not status.enabled:
67
+ return f"{status.cli_version or 'installed'}, not enabled"
68
+ agents = f" [{', '.join(status.agents)}]" if status.agents else ""
69
+ return f"{status.cli_version or 'enabled'}, enabled{agents}"
70
+
71
+
72
+ __all__ = ["status_command"]