memoryhub-cli 0.4.0__tar.gz → 0.11.0__tar.gz

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.
@@ -32,6 +32,18 @@ seed-clients.json
32
32
  .cache_ggshield
33
33
  .mcp.json
34
34
 
35
- NEXT_SESSION_old.md
36
- NEXT_SESSION.md
35
+ NEXT_SESSION*.md
37
36
  planning/rfe_draft.md
37
+
38
+
39
+ # Research drafts not ready for public commit
40
+ research/agent-memory-benchmarks/
41
+
42
+ # Claude Code worktrees
43
+ .claude/worktrees/
44
+
45
+ # Local DB backups (created by scripts/backup-db.sh)
46
+ backups/
47
+
48
+ medium_article_1.pdf
49
+
@@ -0,0 +1,115 @@
1
+ # Changelog — memoryhub-cli
2
+
3
+ All notable changes to the `memoryhub-cli` package.
4
+
5
+ ## [0.11.0] — 2026-06-12
6
+
7
+ - **Hook scaffolding**: `memoryhub config init` now generates the full
8
+ hook integration -- `.claude/hooks/load-memories.sh` (portable,
9
+ executable) and `.claude/settings.json` SessionStart entries.
10
+ - **Hook script portability**: CLI discovery checks PATH first (pipx/global)
11
+ before project venv paths. JSON parsing uses jq/python3/grep fallback chain.
12
+ - **Loading rule refinement**: Rule templates frame hooks as the expected
13
+ path, with manual MCP calls as a degraded fallback.
14
+ - **`memoryhub reconstruct`**: Retrieve behavioral memories sorted by
15
+ weight. Supports table, json, quiet, and compact output.
16
+ - **`memoryhub admin backfill-entities`**: Run entity extraction on
17
+ memories without extraction_status. Accepts `--limit` and
18
+ `--include-failed`.
19
+ - **Thread commands** (from unreleased 0.10.0): `memoryhub thread`
20
+ subgroup with create, append, get, list, archive, extract, fork,
21
+ share, and delete commands.
22
+ - **Entity commands** (from unreleased 0.10.0): `memoryhub entity`
23
+ subgroup with list, merge, and rename commands.
24
+ - **Session commands** (from unreleased 0.10.0): `memoryhub session`
25
+ subgroup with status, focus, and focus-history commands.
26
+ - **Graph commands** (from unreleased 0.10.0): `memoryhub graph`
27
+ subgroup with relate, list, and similar commands.
28
+ - **Curation commands** (from unreleased 0.10.0): `memoryhub curation`
29
+ subgroup with report, resolve, and rule commands.
30
+ - **Project commands** (from unreleased 0.10.0): `memoryhub project`
31
+ subgroup with list, create, add-member, remove-member, and describe.
32
+ - **SDK dependency**: Requires `memoryhub>=0.14.0` (was `>=0.3.0`).
33
+
34
+ ## [0.9.0] — 2026-06-08
35
+
36
+ - **CLI/SDK parity (#257)**: `memoryhub promote`, `memoryhub graduate`,
37
+ `memoryhub checkpoint`, and `memoryhub project describe` complete
38
+ feature parity with the MCP server.
39
+ - **Fix**: Search command `--output` help text now lists `compact` format.
40
+ - **Skill**: Added `memoryhub-manage` Claude Code skill for cold-path
41
+ operations (#203 prototype).
42
+
43
+ ## [0.8.0] — 2026-06-03
44
+
45
+ - **Obsidian export (#245)**: `memoryhub export --format obsidian` generates
46
+ Obsidian-compatible markdown with wikilinks and frontmatter.
47
+ - **Hook-aware rule templates**: `memoryhub config init` and
48
+ `memoryhub config regenerate` now generate rule files that check for a
49
+ `<memoryhub-context>` block from SessionStart hooks before falling back
50
+ to the manual `register_session` + `search_memory` flow.
51
+
52
+ ## [0.7.0] — 2026-05-19
53
+
54
+ - **Content type support (#237)**: Write and search commands accept
55
+ `--content-type` for behavioral memory classification.
56
+
57
+ ## [0.6.0] — 2026-05-07
58
+
59
+ - **API key authentication (#256)**: The CLI now supports API key auth
60
+ via `MEMORYHUB_API_KEY` env var, `--api-key` flag, or the file at
61
+ `~/.config/memoryhub/api-key`. This enables non-interactive use cases
62
+ like SessionStart hooks.
63
+ - **Compact output (#255)**: `--output compact` on search and list commands
64
+ produces content-only text wrapped in `<memoryhub-context>` tags for
65
+ zero-overhead LLM injection.
66
+ - **List command**: `memoryhub list` enumerates memories without semantic
67
+ ranking, with cursor-based pagination.
68
+ - **Server URL prompt**: `memoryhub config init` now prompts for the
69
+ server URL and saves it to `~/.config/memoryhub/config.json`.
70
+
71
+ ## [0.5.0] — 2026-04-22
72
+
73
+ - **Structured output (#200)**: Replaced `--json` flag with `--output`
74
+ accepting `table`, `json`, `quiet`, and `compact` formats. All commands
75
+ now return structured JSON envelopes with `--output json`.
76
+ - **New sub-apps**: `memoryhub graph` (relate, list, similar),
77
+ `memoryhub curation` (report, resolve), `memoryhub session` (status,
78
+ focus), `memoryhub project` (list, describe, join, leave).
79
+ - **Update command**: `memoryhub update` for modifying existing memories
80
+ with version history preservation.
81
+ - **Config enhancements**: `--project` and `--non-interactive` flags on
82
+ `memoryhub config init`.
83
+
84
+ ## [0.4.0] — 2026-04-14
85
+
86
+ - **Admin subcommands (#186)**: Added `memoryhub admin` command group with
87
+ `create-agent`, `list-agents`, `rotate-secret`, and `disable-agent` for
88
+ self-serve agent provisioning via the auth service REST API.
89
+ - **`--version` flag**: `memoryhub --version` now prints the installed version.
90
+
91
+ ## [0.3.0] — 2026-04-09
92
+
93
+ - **Campaign & domain parameter support (#164)**: Added `--project-id` flag to
94
+ search, read, write, delete, and history commands. Added `--domain` flag to
95
+ search and write. When `.memoryhub.yaml` has campaigns configured, `project_id`
96
+ is auto-loaded from the config so the flag can be omitted.
97
+
98
+ ## [0.2.0] — 2026-04-09
99
+
100
+ - Added campaign enrollment prompt to `memoryhub config init` (#160).
101
+ - API key check after config init (#153).
102
+
103
+ ## [0.1.1] — 2026-04-09
104
+
105
+ - Fix ruff lint errors (import sorting, `Optional` → `X | Y` annotations,
106
+ line length). No functional changes from 0.1.0.
107
+
108
+ ## [0.1.0] — 2026-04-09
109
+
110
+ - Initial release. Terminal client for MemoryHub with search, read, write,
111
+ delete, and history commands.
112
+ - `memoryhub config init` — interactive wizard for generating
113
+ `.memoryhub.yaml` and `.claude/rules/memoryhub-loading.md`.
114
+ - `memoryhub config regenerate` — re-render rule file after editing YAML.
115
+ - `memoryhub login` — one-time credential setup.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memoryhub-cli
3
- Version: 0.4.0
3
+ Version: 0.11.0
4
4
  Summary: CLI client for MemoryHub — centralized, governed memory for AI agents
5
5
  Project-URL: Homepage, https://github.com/redhat-ai-americas/memory-hub
6
6
  Project-URL: Repository, https://github.com/redhat-ai-americas/memory-hub
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Requires-Python: >=3.10
21
21
  Requires-Dist: httpx>=0.27
22
- Requires-Dist: memoryhub>=0.3.0
22
+ Requires-Dist: memoryhub>=0.14.0
23
23
  Requires-Dist: pyyaml>=6.0
24
24
  Requires-Dist: rich>=13.0
25
25
  Requires-Dist: typer[all]>=0.15
@@ -38,15 +38,54 @@ Command-line client for MemoryHub — centralized, governed memory for AI agents
38
38
  pip install memoryhub-cli
39
39
  ```
40
40
 
41
- ## Usage
41
+ ## Authentication
42
+
43
+ The CLI supports two authentication modes:
44
+
45
+ **API key (recommended for non-interactive use):**
46
+
47
+ ```bash
48
+ # Via environment variable
49
+ export MEMORYHUB_API_KEY=mh-dev-abc123
50
+
51
+ # Or place your key at ~/.config/memoryhub/api-key (mode 0600)
52
+ echo "mh-dev-abc123" > ~/.config/memoryhub/api-key
53
+ ```
54
+
55
+ API key resolution order: `MEMORYHUB_API_KEY` env var > `~/.config/memoryhub/api-key` file > `api_key` in config.json.
56
+
57
+ **Server URL:**
58
+
59
+ The CLI needs to know where the MemoryHub server is. The SessionStart hook for Claude Code also reads from this config.
60
+
61
+ ```bash
62
+ # Via environment variable
63
+ export MEMORYHUB_URL=https://memoryhub.example.com/mcp/
64
+
65
+ # Or save to ~/.config/memoryhub/config.json (also set by memoryhub config init)
66
+ echo '{"url": "https://memoryhub.example.com/mcp/"}' > ~/.config/memoryhub/config.json
67
+ chmod 600 ~/.config/memoryhub/config.json
68
+ ```
69
+
70
+ URL resolution: `MEMORYHUB_URL` env var > `url` in `~/.config/memoryhub/config.json`.
71
+
72
+ **OAuth (interactive setup):**
42
73
 
43
74
  ```bash
44
- # Authenticate to a MemoryHub instance
45
75
  memoryhub login
76
+ ```
77
+
78
+ When both are available, API key takes precedence.
79
+
80
+ ## Usage
46
81
 
82
+ ```bash
47
83
  # Search for memories
48
84
  memoryhub search "deployment patterns"
49
85
 
86
+ # List memories by creation time (no semantic search)
87
+ memoryhub list --project-id my-project --max 20
88
+
50
89
  # Read a specific memory
51
90
  memoryhub read <memory-id>
52
91
 
@@ -57,6 +96,9 @@ memoryhub write "Use Podman, not Docker" --scope user --weight 0.9
57
96
  memoryhub search "shared patterns" --project-id my-project --domain React
58
97
  memoryhub write "Use vLLM for embeddings" --project-id my-project --domain ML
59
98
 
99
+ # Compact output for LLM context injection (content only, no metadata)
100
+ memoryhub search "project conventions" --output compact
101
+
60
102
  # Set up project-level memory loading
61
103
  memoryhub config init
62
104
  memoryhub config regenerate
@@ -70,6 +112,15 @@ memoryhub admin disable-agent my-agent
70
112
 
71
113
  The `--project-id` flag enables campaign-scoped memory access. When your project is enrolled in campaigns via `.memoryhub.yaml`, the CLI auto-loads the project identifier from config, so you can omit the flag in most cases. Use `--domain` to tag writes or boost domain-matching results in search.
72
114
 
115
+ ## Output formats
116
+
117
+ The `--output` / `-o` flag controls output format on most commands:
118
+
119
+ - `table` (default) -- Rich-formatted tables for interactive use
120
+ - `json` -- Machine-readable JSON envelope (`{"status": "ok", "data": {...}}`)
121
+ - `quiet` -- No output (exit code only)
122
+ - `compact` -- Content-only text for LLM context injection, no IDs or metadata
123
+
73
124
  ## Project configuration
74
125
 
75
126
  `memoryhub config` generates a project-local `.memoryhub.yaml` and a companion `.claude/rules/memoryhub-loading.md` rule file. Both files are meant to be committed so every contributor's agent inherits the same loading policy.
@@ -8,15 +8,54 @@ Command-line client for MemoryHub — centralized, governed memory for AI agents
8
8
  pip install memoryhub-cli
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Authentication
12
+
13
+ The CLI supports two authentication modes:
14
+
15
+ **API key (recommended for non-interactive use):**
16
+
17
+ ```bash
18
+ # Via environment variable
19
+ export MEMORYHUB_API_KEY=mh-dev-abc123
20
+
21
+ # Or place your key at ~/.config/memoryhub/api-key (mode 0600)
22
+ echo "mh-dev-abc123" > ~/.config/memoryhub/api-key
23
+ ```
24
+
25
+ API key resolution order: `MEMORYHUB_API_KEY` env var > `~/.config/memoryhub/api-key` file > `api_key` in config.json.
26
+
27
+ **Server URL:**
28
+
29
+ The CLI needs to know where the MemoryHub server is. The SessionStart hook for Claude Code also reads from this config.
30
+
31
+ ```bash
32
+ # Via environment variable
33
+ export MEMORYHUB_URL=https://memoryhub.example.com/mcp/
34
+
35
+ # Or save to ~/.config/memoryhub/config.json (also set by memoryhub config init)
36
+ echo '{"url": "https://memoryhub.example.com/mcp/"}' > ~/.config/memoryhub/config.json
37
+ chmod 600 ~/.config/memoryhub/config.json
38
+ ```
39
+
40
+ URL resolution: `MEMORYHUB_URL` env var > `url` in `~/.config/memoryhub/config.json`.
41
+
42
+ **OAuth (interactive setup):**
12
43
 
13
44
  ```bash
14
- # Authenticate to a MemoryHub instance
15
45
  memoryhub login
46
+ ```
47
+
48
+ When both are available, API key takes precedence.
49
+
50
+ ## Usage
16
51
 
52
+ ```bash
17
53
  # Search for memories
18
54
  memoryhub search "deployment patterns"
19
55
 
56
+ # List memories by creation time (no semantic search)
57
+ memoryhub list --project-id my-project --max 20
58
+
20
59
  # Read a specific memory
21
60
  memoryhub read <memory-id>
22
61
 
@@ -27,6 +66,9 @@ memoryhub write "Use Podman, not Docker" --scope user --weight 0.9
27
66
  memoryhub search "shared patterns" --project-id my-project --domain React
28
67
  memoryhub write "Use vLLM for embeddings" --project-id my-project --domain ML
29
68
 
69
+ # Compact output for LLM context injection (content only, no metadata)
70
+ memoryhub search "project conventions" --output compact
71
+
30
72
  # Set up project-level memory loading
31
73
  memoryhub config init
32
74
  memoryhub config regenerate
@@ -40,6 +82,15 @@ memoryhub admin disable-agent my-agent
40
82
 
41
83
  The `--project-id` flag enables campaign-scoped memory access. When your project is enrolled in campaigns via `.memoryhub.yaml`, the CLI auto-loads the project identifier from config, so you can omit the flag in most cases. Use `--domain` to tag writes or boost domain-matching results in search.
42
84
 
85
+ ## Output formats
86
+
87
+ The `--output` / `-o` flag controls output format on most commands:
88
+
89
+ - `table` (default) -- Rich-formatted tables for interactive use
90
+ - `json` -- Machine-readable JSON envelope (`{"status": "ok", "data": {...}}`)
91
+ - `quiet` -- No output (exit code only)
92
+ - `compact` -- Content-only text for LLM context injection, no IDs or metadata
93
+
43
94
  ## Project configuration
44
95
 
45
96
  `memoryhub config` generates a project-local `.memoryhub.yaml` and a companion `.claude/rules/memoryhub-loading.md` rule file. Both files are meant to be committed so every contributor's agent inherits the same loading policy.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memoryhub-cli"
7
- version = "0.4.0"
7
+ version = "0.11.0"
8
8
  description = "CLI client for MemoryHub — centralized, governed memory for AI agents"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -14,7 +14,7 @@ authors = [
14
14
  ]
15
15
  keywords = ["ai", "agents", "memory", "cli", "mcp"]
16
16
  dependencies = [
17
- "memoryhub>=0.3.0",
17
+ "memoryhub>=0.14.0",
18
18
  "typer[all]>=0.15",
19
19
  "rich>=13.0",
20
20
  "pyyaml>=6.0",
@@ -1,3 +1,3 @@
1
1
  """MemoryHub CLI client."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.11.0"
@@ -3,23 +3,28 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import json
7
6
  import os
8
7
 
9
8
  import httpx
10
9
  import typer
11
- from rich.console import Console
12
10
  from rich.table import Table
13
11
 
14
12
  from memoryhub_cli.config import CONFIG_DIR, load_config
13
+ from memoryhub_cli.output import (
14
+ EXIT_AUTH_ERROR,
15
+ EXIT_CLIENT_ERROR,
16
+ EXIT_SERVER_ERROR,
17
+ OutputFormat,
18
+ console,
19
+ handle_error,
20
+ json_success,
21
+ )
15
22
 
16
23
  admin_app = typer.Typer(
17
24
  name="admin",
18
25
  help="Manage agents and OAuth clients.",
19
26
  no_args_is_help=True,
20
27
  )
21
- console = Console()
22
- err_console = Console(stderr=True)
23
28
 
24
29
 
25
30
  def _run(coro):
@@ -27,7 +32,7 @@ def _run(coro):
27
32
  return asyncio.run(coro)
28
33
 
29
34
 
30
- def _get_admin_key() -> str:
35
+ def _get_admin_key(output: OutputFormat) -> str:
31
36
  """Resolve the admin key from env var or config file."""
32
37
  key = os.environ.get("MEMORYHUB_ADMIN_KEY")
33
38
  if key:
@@ -38,15 +43,16 @@ def _get_admin_key() -> str:
38
43
  if key:
39
44
  return key
40
45
 
41
- err_console.print(
42
- "[red]No admin key found.[/red]\n"
43
- "Set MEMORYHUB_ADMIN_KEY or add 'admin_key' to "
44
- "~/.config/memoryhub/config.json."
46
+ handle_error(
47
+ "missing_config",
48
+ "No admin key found. Set MEMORYHUB_ADMIN_KEY or add 'admin_key' to "
49
+ "~/.config/memoryhub/config.json.",
50
+ output,
51
+ EXIT_CLIENT_ERROR,
45
52
  )
46
- raise typer.Exit(1)
47
53
 
48
54
 
49
- def _get_auth_url() -> str:
55
+ def _get_auth_url(output: OutputFormat) -> str:
50
56
  """Resolve the auth service base URL (without trailing slash)."""
51
57
  url = os.environ.get("MEMORYHUB_AUTH_URL")
52
58
  if url:
@@ -59,16 +65,15 @@ def _get_auth_url() -> str:
59
65
 
60
66
  url = config.get("url")
61
67
  if url:
62
- # Fall back to base MCP URL with /admin stripped — assume auth
63
- # is co-located at the base URL.
64
68
  return url.rstrip("/")
65
69
 
66
- err_console.print(
67
- "[red]No auth URL found.[/red]\n"
68
- "Set MEMORYHUB_AUTH_URL, or add 'auth_url' to "
69
- "~/.config/memoryhub/config.json."
70
+ handle_error(
71
+ "missing_config",
72
+ "No auth URL found. Set MEMORYHUB_AUTH_URL, or add 'auth_url' to "
73
+ "~/.config/memoryhub/config.json.",
74
+ output,
75
+ EXIT_CLIENT_ERROR,
70
76
  )
71
- raise typer.Exit(1)
72
77
 
73
78
 
74
79
  def _admin_headers(admin_key: str) -> dict[str, str]:
@@ -97,14 +102,16 @@ def create_agent(
97
102
  "--write-config",
98
103
  help="Write the client_secret to ~/.config/memoryhub/api-key",
99
104
  ),
100
- json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
105
+ output: OutputFormat = typer.Option(
106
+ OutputFormat.table, "--output", "-o", help="Output format: table, json, quiet",
107
+ ),
101
108
  ):
102
109
  """Create a new agent (OAuth client).
103
110
 
104
111
  The client_secret is shown only once. Save it immediately.
105
112
  """
106
- admin_key = _get_admin_key()
107
- auth_url = _get_auth_url()
113
+ admin_key = _get_admin_key(output)
114
+ auth_url = _get_auth_url(output)
108
115
  scope_list = [s.strip() for s in scopes.split(",") if s.strip()]
109
116
 
110
117
  body = {
@@ -128,11 +135,13 @@ def create_agent(
128
135
  try:
129
136
  data = _run(_do())
130
137
  except httpx.HTTPStatusError as exc:
131
- _handle_http_error(exc)
138
+ _handle_http_error(exc, output)
132
139
  return
133
140
 
134
- if json_output:
135
- console.print_json(json.dumps(data))
141
+ if output == OutputFormat.json:
142
+ json_success(data)
143
+ return
144
+ if output == OutputFormat.quiet:
136
145
  return
137
146
 
138
147
  console.print("[green]Agent created successfully.[/green]\n")
@@ -159,11 +168,13 @@ def create_agent(
159
168
 
160
169
  @admin_app.command("list-agents")
161
170
  def list_agents(
162
- json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
171
+ output: OutputFormat = typer.Option(
172
+ OutputFormat.table, "--output", "-o", help="Output format: table, json, quiet",
173
+ ),
163
174
  ):
164
175
  """List all registered agents (OAuth clients)."""
165
- admin_key = _get_admin_key()
166
- auth_url = _get_auth_url()
176
+ admin_key = _get_admin_key(output)
177
+ auth_url = _get_auth_url(output)
167
178
 
168
179
  async def _do():
169
180
  async with httpx.AsyncClient() as client:
@@ -178,11 +189,13 @@ def list_agents(
178
189
  try:
179
190
  data = _run(_do())
180
191
  except httpx.HTTPStatusError as exc:
181
- _handle_http_error(exc)
192
+ _handle_http_error(exc, output)
182
193
  return
183
194
 
184
- if json_output:
185
- console.print_json(json.dumps(data))
195
+ if output == OutputFormat.json:
196
+ json_success(data)
197
+ return
198
+ if output == OutputFormat.quiet:
186
199
  return
187
200
 
188
201
  if not data:
@@ -214,14 +227,16 @@ def list_agents(
214
227
  @admin_app.command("rotate-secret")
215
228
  def rotate_secret(
216
229
  client_id: str = typer.Argument(..., help="Client ID of the agent"),
217
- json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
230
+ output: OutputFormat = typer.Option(
231
+ OutputFormat.table, "--output", "-o", help="Output format: table, json, quiet",
232
+ ),
218
233
  ):
219
234
  """Rotate the client secret for an agent.
220
235
 
221
236
  The new secret is shown only once. Save it immediately.
222
237
  """
223
- admin_key = _get_admin_key()
224
- auth_url = _get_auth_url()
238
+ admin_key = _get_admin_key(output)
239
+ auth_url = _get_auth_url(output)
225
240
 
226
241
  async def _do():
227
242
  async with httpx.AsyncClient() as client:
@@ -236,11 +251,13 @@ def rotate_secret(
236
251
  try:
237
252
  data = _run(_do())
238
253
  except httpx.HTTPStatusError as exc:
239
- _handle_http_error(exc)
254
+ _handle_http_error(exc, output)
240
255
  return
241
256
 
242
- if json_output:
243
- console.print_json(json.dumps(data))
257
+ if output == OutputFormat.json:
258
+ json_success(data)
259
+ return
260
+ if output == OutputFormat.quiet:
244
261
  return
245
262
 
246
263
  console.print(f"[green]Secret rotated for {data['client_id']}.[/green]\n")
@@ -255,10 +272,13 @@ def rotate_secret(
255
272
  @admin_app.command("disable-agent")
256
273
  def disable_agent(
257
274
  client_id: str = typer.Argument(..., help="Client ID of the agent to disable"),
275
+ output: OutputFormat = typer.Option(
276
+ OutputFormat.table, "--output", "-o", help="Output format: table, json, quiet",
277
+ ),
258
278
  ):
259
279
  """Disable an agent (set active=false)."""
260
- admin_key = _get_admin_key()
261
- auth_url = _get_auth_url()
280
+ admin_key = _get_admin_key(output)
281
+ auth_url = _get_auth_url(output)
262
282
 
263
283
  async def _do():
264
284
  async with httpx.AsyncClient() as client:
@@ -274,7 +294,13 @@ def disable_agent(
274
294
  try:
275
295
  data = _run(_do())
276
296
  except httpx.HTTPStatusError as exc:
277
- _handle_http_error(exc)
297
+ _handle_http_error(exc, output)
298
+ return
299
+
300
+ if output == OutputFormat.json:
301
+ json_success(data)
302
+ return
303
+ if output == OutputFormat.quiet:
278
304
  return
279
305
 
280
306
  console.print(
@@ -282,11 +308,50 @@ def disable_agent(
282
308
  )
283
309
 
284
310
 
311
+ # ── Entity backfill (via MCP, not REST) ─────────────────────────────────────
312
+
313
+
314
+ @admin_app.command("backfill-entities")
315
+ def backfill_entities(
316
+ limit: int = typer.Option(50, "--limit", "-n", help="Max memories to process"),
317
+ include_failed: bool = typer.Option(
318
+ False, "--include-failed", help="Reprocess previously failed memories",
319
+ ),
320
+ output: OutputFormat = typer.Option(
321
+ OutputFormat.table, "--output", "-o", help="Output format: table, json, quiet",
322
+ ),
323
+ ):
324
+ """Run entity extraction on memories without extraction_status."""
325
+ from memoryhub_cli.main import _get_client, _run_command
326
+
327
+ client = _get_client(output)
328
+
329
+ async def _do():
330
+ async with client:
331
+ return await client.backfill_entities(
332
+ limit=limit, include_failed=include_failed,
333
+ )
334
+
335
+ result = _run_command(_do(), output)
336
+
337
+ if output == OutputFormat.json:
338
+ json_success(result)
339
+ return
340
+ if output == OutputFormat.quiet:
341
+ return
342
+
343
+ console.print("[bold]Entity Backfill Results[/bold]\n")
344
+ console.print(f" Candidates: {result.get('candidates', '-')}")
345
+ console.print(f" Processed: {result.get('processed', '-')}")
346
+ console.print(f" Succeeded: {result.get('succeeded', '-')}")
347
+ console.print(f" Failed: {result.get('failed', '-')}")
348
+
349
+
285
350
  # ── Helpers ──────────────────────────────────────────────────────────────────
286
351
 
287
352
 
288
- def _handle_http_error(exc: httpx.HTTPStatusError) -> None:
289
- """Print a user-friendly error for HTTP failures."""
353
+ def _handle_http_error(exc: httpx.HTTPStatusError, output: OutputFormat) -> None:
354
+ """Emit a structured error for HTTP failures and exit."""
290
355
  status = exc.response.status_code
291
356
  try:
292
357
  detail = exc.response.json().get("detail", exc.response.text)
@@ -294,14 +359,17 @@ def _handle_http_error(exc: httpx.HTTPStatusError) -> None:
294
359
  detail = exc.response.text
295
360
 
296
361
  if status == 401:
297
- err_console.print(
298
- "[red]Authentication failed.[/red] Check your admin key."
362
+ handle_error(
363
+ "auth_failed",
364
+ "Authentication failed. Check your admin key.",
365
+ output,
366
+ EXIT_AUTH_ERROR,
299
367
  )
300
368
  elif status == 404:
301
- err_console.print(f"[red]Not found:[/red] {detail}")
369
+ handle_error("not_found", str(detail), output, EXIT_CLIENT_ERROR)
302
370
  elif status == 409:
303
- err_console.print(f"[red]Conflict:[/red] {detail}")
371
+ handle_error("conflict", str(detail), output, EXIT_CLIENT_ERROR)
372
+ elif status >= 500:
373
+ handle_error("server_error", f"HTTP {status}: {detail}", output, EXIT_SERVER_ERROR)
304
374
  else:
305
- err_console.print(f"[red]HTTP {status}:[/red] {detail}")
306
-
307
- raise typer.Exit(1)
375
+ handle_error("http_error", f"HTTP {status}: {detail}", output, EXIT_CLIENT_ERROR)