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.
@@ -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.2.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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memoryhub-cli"
7
- version = "0.2.0"
7
+ version = "0.3.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"
@@ -1,3 +1,3 @@
1
1
  """MemoryHub CLI client."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -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(memory_id, max_versions=max_versions)
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