memoryhub-cli 0.2.0__tar.gz → 0.3.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.
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/CHANGELOG.md +12 -0
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/PKG-INFO +7 -1
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/README.md +6 -0
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/pyproject.toml +1 -1
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/src/memoryhub_cli/__init__.py +1 -1
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/src/memoryhub_cli/main.py +49 -3
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/tests/test_project_config.py +87 -2
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/.gitignore +0 -0
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/src/memoryhub_cli/config.py +0 -0
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/src/memoryhub_cli/project_config.py +0 -0
- {memoryhub_cli-0.2.0 → memoryhub_cli-0.3.0}/tests/__init__.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `memoryhub-cli` package.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] — 2026-04-09
|
|
6
|
+
|
|
7
|
+
- **Campaign & domain parameter support (#164)**: Added `--project-id` flag to
|
|
8
|
+
search, read, write, delete, and history commands. Added `--domain` flag to
|
|
9
|
+
search and write. When `.memoryhub.yaml` has campaigns configured, `project_id`
|
|
10
|
+
is auto-loaded from the config so the flag can be omitted.
|
|
11
|
+
|
|
12
|
+
## [0.2.0] — 2026-04-09
|
|
13
|
+
|
|
14
|
+
- Added campaign enrollment prompt to `memoryhub config init` (#160).
|
|
15
|
+
- API key check after config init (#153).
|
|
16
|
+
|
|
5
17
|
## [0.1.1] — 2026-04-09
|
|
6
18
|
|
|
7
19
|
- Fix ruff lint errors (import sorting, `Optional` → `X | Y` annotations,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memoryhub-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -52,11 +52,17 @@ memoryhub read <memory-id>
|
|
|
52
52
|
# Write a new memory
|
|
53
53
|
memoryhub write "Use Podman, not Docker" --scope user --weight 0.9
|
|
54
54
|
|
|
55
|
+
# Campaign-scoped operations (requires project enrollment)
|
|
56
|
+
memoryhub search "shared patterns" --project-id my-project --domain React
|
|
57
|
+
memoryhub write "Use vLLM for embeddings" --project-id my-project --domain ML
|
|
58
|
+
|
|
55
59
|
# Set up project-level memory loading
|
|
56
60
|
memoryhub config init
|
|
57
61
|
memoryhub config regenerate
|
|
58
62
|
```
|
|
59
63
|
|
|
64
|
+
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.
|
|
65
|
+
|
|
60
66
|
## Project configuration
|
|
61
67
|
|
|
62
68
|
`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.
|
|
@@ -23,11 +23,17 @@ memoryhub read <memory-id>
|
|
|
23
23
|
# Write a new memory
|
|
24
24
|
memoryhub write "Use Podman, not Docker" --scope user --weight 0.9
|
|
25
25
|
|
|
26
|
+
# Campaign-scoped operations (requires project enrollment)
|
|
27
|
+
memoryhub search "shared patterns" --project-id my-project --domain React
|
|
28
|
+
memoryhub write "Use vLLM for embeddings" --project-id my-project --domain ML
|
|
29
|
+
|
|
26
30
|
# Set up project-level memory loading
|
|
27
31
|
memoryhub config init
|
|
28
32
|
memoryhub config regenerate
|
|
29
33
|
```
|
|
30
34
|
|
|
35
|
+
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.
|
|
36
|
+
|
|
31
37
|
## Project configuration
|
|
32
38
|
|
|
33
39
|
`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.
|
|
@@ -58,6 +58,21 @@ def _get_client():
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _get_project_id_default() -> str | None:
|
|
62
|
+
"""Try to load project_id from .memoryhub.yaml campaigns config.
|
|
63
|
+
|
|
64
|
+
Returns the project directory name as the project identifier when
|
|
65
|
+
campaigns are configured, or None when no config/campaigns exist.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
config = load_project_config() # auto-discovers .memoryhub.yaml
|
|
69
|
+
if config.memory_loading.campaigns:
|
|
70
|
+
return Path.cwd().name
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
61
76
|
def _run(coro):
|
|
62
77
|
"""Run an async coroutine."""
|
|
63
78
|
return asyncio.run(coro)
|
|
@@ -110,15 +125,23 @@ def search(
|
|
|
110
125
|
query: str = typer.Argument(..., help="Search query"),
|
|
111
126
|
scope: str | None = typer.Option(None, "--scope", "-s", help="Filter by scope"),
|
|
112
127
|
max_results: int = typer.Option(10, "--max", "-n", help="Maximum results"),
|
|
128
|
+
project_id: str | None = typer.Option(
|
|
129
|
+
None, "--project-id", "-p", help="Project ID for campaign access",
|
|
130
|
+
),
|
|
131
|
+
domains: list[str] | None = typer.Option(
|
|
132
|
+
None, "--domain", help="Domain tags to boost",
|
|
133
|
+
),
|
|
113
134
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
114
135
|
):
|
|
115
136
|
"""Search memories using semantic similarity."""
|
|
116
137
|
client = _get_client()
|
|
138
|
+
_project_id = project_id or _get_project_id_default()
|
|
117
139
|
|
|
118
140
|
async def _do():
|
|
119
141
|
async with client:
|
|
120
142
|
return await client.search(
|
|
121
143
|
query, scope=scope, max_results=max_results,
|
|
144
|
+
project_id=_project_id, domains=domains or None,
|
|
122
145
|
)
|
|
123
146
|
|
|
124
147
|
result = _run(_do())
|
|
@@ -158,14 +181,18 @@ def search(
|
|
|
158
181
|
@app.command()
|
|
159
182
|
def read(
|
|
160
183
|
memory_id: str = typer.Argument(..., help="Memory UUID"),
|
|
184
|
+
project_id: str | None = typer.Option(
|
|
185
|
+
None, "--project-id", "-p", help="Project ID for campaign access",
|
|
186
|
+
),
|
|
161
187
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
162
188
|
):
|
|
163
189
|
"""Read a memory by ID."""
|
|
164
190
|
client = _get_client()
|
|
191
|
+
_project_id = project_id or _get_project_id_default()
|
|
165
192
|
|
|
166
193
|
async def _do():
|
|
167
194
|
async with client:
|
|
168
|
-
return await client.read(memory_id)
|
|
195
|
+
return await client.read(memory_id, project_id=_project_id)
|
|
169
196
|
|
|
170
197
|
memory = _run(_do())
|
|
171
198
|
|
|
@@ -193,6 +220,12 @@ def write(
|
|
|
193
220
|
weight: float = typer.Option(0.7, "--weight", "-w", help="Priority weight 0.0-1.0"),
|
|
194
221
|
parent_id: str | None = typer.Option(None, "--parent", help="Parent memory ID"),
|
|
195
222
|
branch_type: str | None = typer.Option(None, "--branch-type", help="Branch type"),
|
|
223
|
+
project_id: str | None = typer.Option(
|
|
224
|
+
None, "--project-id", "-p", help="Project ID for campaign access",
|
|
225
|
+
),
|
|
226
|
+
domains: list[str] | None = typer.Option(
|
|
227
|
+
None, "--domain", help="Domain tags",
|
|
228
|
+
),
|
|
196
229
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
197
230
|
):
|
|
198
231
|
"""Write a new memory.
|
|
@@ -210,12 +243,14 @@ def write(
|
|
|
210
243
|
raise typer.Exit(1)
|
|
211
244
|
|
|
212
245
|
client = _get_client()
|
|
246
|
+
_project_id = project_id or _get_project_id_default()
|
|
213
247
|
|
|
214
248
|
async def _do():
|
|
215
249
|
async with client:
|
|
216
250
|
return await client.write(
|
|
217
251
|
content, scope=scope, weight=weight,
|
|
218
252
|
parent_id=parent_id, branch_type=branch_type,
|
|
253
|
+
project_id=_project_id, domains=domains or None,
|
|
219
254
|
)
|
|
220
255
|
|
|
221
256
|
result = _run(_do())
|
|
@@ -240,6 +275,9 @@ def write(
|
|
|
240
275
|
def delete(
|
|
241
276
|
memory_id: str = typer.Argument(..., help="Memory UUID to delete"),
|
|
242
277
|
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
278
|
+
project_id: str | None = typer.Option(
|
|
279
|
+
None, "--project-id", "-p", help="Project ID for campaign access",
|
|
280
|
+
),
|
|
243
281
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
244
282
|
):
|
|
245
283
|
"""Soft-delete a memory and its version chain."""
|
|
@@ -249,10 +287,11 @@ def delete(
|
|
|
249
287
|
raise typer.Abort()
|
|
250
288
|
|
|
251
289
|
client = _get_client()
|
|
290
|
+
_project_id = project_id or _get_project_id_default()
|
|
252
291
|
|
|
253
292
|
async def _do():
|
|
254
293
|
async with client:
|
|
255
|
-
return await client.delete(memory_id)
|
|
294
|
+
return await client.delete(memory_id, project_id=_project_id)
|
|
256
295
|
|
|
257
296
|
result = _run(_do())
|
|
258
297
|
|
|
@@ -270,14 +309,21 @@ def delete(
|
|
|
270
309
|
def history(
|
|
271
310
|
memory_id: str = typer.Argument(..., help="Memory UUID"),
|
|
272
311
|
max_versions: int = typer.Option(20, "--max", "-n", help="Maximum versions to show"),
|
|
312
|
+
project_id: str | None = typer.Option(
|
|
313
|
+
None, "--project-id", "-p", help="Project ID for campaign access",
|
|
314
|
+
),
|
|
273
315
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
274
316
|
):
|
|
275
317
|
"""Show version history for a memory."""
|
|
276
318
|
client = _get_client()
|
|
319
|
+
_project_id = project_id or _get_project_id_default()
|
|
277
320
|
|
|
278
321
|
async def _do():
|
|
279
322
|
async with client:
|
|
280
|
-
return await client.get_history(
|
|
323
|
+
return await client.get_history(
|
|
324
|
+
memory_id, max_versions=max_versions,
|
|
325
|
+
project_id=_project_id,
|
|
326
|
+
)
|
|
281
327
|
|
|
282
328
|
result = _run(_do())
|
|
283
329
|
|
|
@@ -6,12 +6,11 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
import yaml
|
|
9
|
-
|
|
10
9
|
from memoryhub import (
|
|
11
10
|
CONFIG_FILENAME,
|
|
12
|
-
ProjectConfig,
|
|
13
11
|
load_project_config,
|
|
14
12
|
)
|
|
13
|
+
|
|
15
14
|
from memoryhub_cli.project_config import (
|
|
16
15
|
GENERATED_RULE_NAME,
|
|
17
16
|
LEGACY_RULE_NAME,
|
|
@@ -425,3 +424,89 @@ def test_rewrite_rule_file_does_not_touch_yaml(tmp_path: Path):
|
|
|
425
424
|
# YAML untouched.
|
|
426
425
|
assert yaml_path.stat().st_mtime == yaml_mtime_before
|
|
427
426
|
assert "pattern: eager" in yaml_path.read_text()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ── CLI option wiring: project_id and domains ────────────────────────────
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _strip_ansi(text: str) -> str:
|
|
433
|
+
"""Remove ANSI escape codes from text for reliable assertions."""
|
|
434
|
+
import re
|
|
435
|
+
|
|
436
|
+
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_search_accepts_project_id_option():
|
|
440
|
+
"""--project-id is recognized by the search command."""
|
|
441
|
+
from typer.testing import CliRunner
|
|
442
|
+
|
|
443
|
+
from memoryhub_cli.main import app
|
|
444
|
+
|
|
445
|
+
runner = CliRunner()
|
|
446
|
+
result = runner.invoke(app, ["search", "--help"])
|
|
447
|
+
assert result.exit_code == 0
|
|
448
|
+
text = _strip_ansi(result.stdout)
|
|
449
|
+
assert "--project-id" in text, f"--project-id not in: {text}"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_search_accepts_domain_option():
|
|
453
|
+
"""--domain is recognized by the search command."""
|
|
454
|
+
from typer.testing import CliRunner
|
|
455
|
+
|
|
456
|
+
from memoryhub_cli.main import app
|
|
457
|
+
|
|
458
|
+
runner = CliRunner()
|
|
459
|
+
result = runner.invoke(app, ["search", "--help"])
|
|
460
|
+
assert result.exit_code == 0
|
|
461
|
+
text = _strip_ansi(result.stdout)
|
|
462
|
+
assert "--domain" in text, f"--domain not in: {text}"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_write_accepts_project_id_and_domain_options():
|
|
466
|
+
"""--project-id and --domain are recognized by the write command."""
|
|
467
|
+
from typer.testing import CliRunner
|
|
468
|
+
|
|
469
|
+
from memoryhub_cli.main import app
|
|
470
|
+
|
|
471
|
+
runner = CliRunner()
|
|
472
|
+
result = runner.invoke(app, ["write", "--help"])
|
|
473
|
+
assert result.exit_code == 0
|
|
474
|
+
text = _strip_ansi(result.stdout)
|
|
475
|
+
assert "--project-id" in text, f"--project-id not in: {text}"
|
|
476
|
+
assert "--domain" in text, f"--domain not in: {text}"
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_read_accepts_project_id_option():
|
|
480
|
+
from typer.testing import CliRunner
|
|
481
|
+
|
|
482
|
+
from memoryhub_cli.main import app
|
|
483
|
+
|
|
484
|
+
runner = CliRunner()
|
|
485
|
+
result = runner.invoke(app, ["read", "--help"])
|
|
486
|
+
assert result.exit_code == 0
|
|
487
|
+
text = _strip_ansi(result.stdout)
|
|
488
|
+
assert "--project-id" in text, f"--project-id not in: {text}"
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def test_delete_accepts_project_id_option():
|
|
492
|
+
from typer.testing import CliRunner
|
|
493
|
+
|
|
494
|
+
from memoryhub_cli.main import app
|
|
495
|
+
|
|
496
|
+
runner = CliRunner()
|
|
497
|
+
result = runner.invoke(app, ["delete", "--help"])
|
|
498
|
+
assert result.exit_code == 0
|
|
499
|
+
text = _strip_ansi(result.stdout)
|
|
500
|
+
assert "--project-id" in text, f"--project-id not in: {text}"
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def test_history_accepts_project_id_option():
|
|
504
|
+
from typer.testing import CliRunner
|
|
505
|
+
|
|
506
|
+
from memoryhub_cli.main import app
|
|
507
|
+
|
|
508
|
+
runner = CliRunner()
|
|
509
|
+
result = runner.invoke(app, ["history", "--help"])
|
|
510
|
+
assert result.exit_code == 0
|
|
511
|
+
text = _strip_ansi(result.stdout)
|
|
512
|
+
assert "--project-id" in text, f"--project-id not in: {text}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|