cli-mem 0.2.0__tar.gz → 0.2.1__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.
Files changed (30) hide show
  1. {cli_mem-0.2.0 → cli_mem-0.2.1}/PKG-INFO +1 -1
  2. {cli_mem-0.2.0 → cli_mem-0.2.1}/pyproject.toml +1 -1
  3. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/__init__.py +1 -1
  4. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/cli.py +28 -3
  5. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/groups.py +2 -4
  6. {cli_mem-0.2.0 → cli_mem-0.2.1}/tests/test_groups.py +59 -6
  7. {cli_mem-0.2.0 → cli_mem-0.2.1}/.github/workflows/ci.yml +0 -0
  8. {cli_mem-0.2.0 → cli_mem-0.2.1}/.github/workflows/release.yml +0 -0
  9. {cli_mem-0.2.0 → cli_mem-0.2.1}/.gitignore +0 -0
  10. {cli_mem-0.2.0 → cli_mem-0.2.1}/ARCHITECTURE.md +0 -0
  11. {cli_mem-0.2.0 → cli_mem-0.2.1}/LICENSE +0 -0
  12. {cli_mem-0.2.0 → cli_mem-0.2.1}/PHILOSOPHY.md +0 -0
  13. {cli_mem-0.2.0 → cli_mem-0.2.1}/README.md +0 -0
  14. {cli_mem-0.2.0 → cli_mem-0.2.1}/assets/.gitkeep +0 -0
  15. {cli_mem-0.2.0 → cli_mem-0.2.1}/docs/decisions/001-jsonl-over-sqlite.md +0 -0
  16. {cli_mem-0.2.0 → cli_mem-0.2.1}/docs/decisions/002-apple-fm-sdk-for-patterns.md +0 -0
  17. {cli_mem-0.2.0 → cli_mem-0.2.1}/docs/decisions/003-no-daemon.md +0 -0
  18. {cli_mem-0.2.0 → cli_mem-0.2.1}/docs/decisions/004-per-repo-jsonl.md +0 -0
  19. {cli_mem-0.2.0 → cli_mem-0.2.1}/hooks/mem.zsh +0 -0
  20. {cli_mem-0.2.0 → cli_mem-0.2.1}/install.sh +0 -0
  21. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/capture.py +0 -0
  22. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/models.py +0 -0
  23. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/patterns.py +0 -0
  24. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/search.py +0 -0
  25. {cli_mem-0.2.0 → cli_mem-0.2.1}/src/mem/storage.py +0 -0
  26. {cli_mem-0.2.0 → cli_mem-0.2.1}/tests/conftest.py +0 -0
  27. {cli_mem-0.2.0 → cli_mem-0.2.1}/tests/test_capture.py +0 -0
  28. {cli_mem-0.2.0 → cli_mem-0.2.1}/tests/test_patterns.py +0 -0
  29. {cli_mem-0.2.0 → cli_mem-0.2.1}/tests/test_search.py +0 -0
  30. {cli_mem-0.2.0 → cli_mem-0.2.1}/tests/test_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-mem
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Privacy-first CLI that turns shell history into searchable memory
5
5
  Project-URL: GitHub, https://github.com/matinsaurralde/mem
6
6
  Author: Matias Insaurralde
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cli-mem"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Privacy-first CLI that turns shell history into searchable memory"
9
9
  license = "MIT"
10
10
  readme = "README.md"
@@ -1,3 +1,3 @@
1
1
  """mem — your shell history, understood."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -400,10 +400,11 @@ def save(command: str, group_name: str | None, global_flag: bool, comment: str |
400
400
 
401
401
 
402
402
  @cli.command(name="list")
403
+ @click.argument("group_name", required=False, default=None)
403
404
  @click.option("--global", "global_flag", is_flag=True, help="Show only global scope")
404
405
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
405
- def list_cmd(global_flag: bool, as_json: bool) -> None:
406
- """List saved commands and groups."""
406
+ def list_cmd(group_name: str | None, global_flag: bool, as_json: bool) -> None:
407
+ """List saved commands and groups, or show a group's commands."""
407
408
  from mem import groups, storage
408
409
 
409
410
  global_path = storage.GROUPS_GLOBAL_FILE
@@ -415,6 +416,28 @@ def list_cmd(global_flag: bool, as_json: bool) -> None:
415
416
  sanitized = storage.sanitize_repo_name(repo)
416
417
  repo_path = storage.group_file_path(sanitized)
417
418
 
419
+ # Show a specific group's commands
420
+ if group_name is not None:
421
+ grp, scope_label, _file_path, shadows = groups.resolve_group(
422
+ group_name, repo_path, global_path, force_global=global_flag,
423
+ )
424
+
425
+ if as_json:
426
+ click.echo(groups.export_json(group_name, grp))
427
+ return
428
+
429
+ console.print(f"\n● {scope_label} / {group_name}")
430
+ if grp.description:
431
+ console.print(f' "{grp.description}"')
432
+ if group_name in shadows and scope_label != "global":
433
+ console.print(" [dim](global group with same name exists — use --global to see it)[/]")
434
+ console.print(" " + "─" * 50)
435
+ for i, cmd in enumerate(grp.commands, 1):
436
+ comment_str = f" # {cmd.comment}" if cmd.comment else ""
437
+ console.print(f" {i}. {cmd.cmd}{comment_str}")
438
+ console.print()
439
+ return
440
+
418
441
  result = groups.list_all(repo_path, global_path)
419
442
 
420
443
  if as_json:
@@ -528,6 +551,7 @@ def run(group_name: str, global_flag: bool, yes: bool) -> None:
528
551
  return
529
552
 
530
553
  # Determine which commands to run
554
+ run_all = yes
531
555
  if not yes:
532
556
  choice = click.prompt(
533
557
  f" Run all? [y/N] or pick [1-{len(grp.commands)}]",
@@ -539,6 +563,7 @@ def run(group_name: str, global_flag: bool, yes: bool) -> None:
539
563
  return
540
564
 
541
565
  if choice.lower() == "y":
566
+ run_all = True
542
567
  commands_to_run = list(enumerate(grp.commands, 1))
543
568
  else:
544
569
  try:
@@ -557,7 +582,7 @@ def run(group_name: str, global_flag: bool, yes: bool) -> None:
557
582
  # Execute
558
583
  console.print()
559
584
  for i, cmd in commands_to_run:
560
- if not yes and len(commands_to_run) > 1:
585
+ if not run_all and len(commands_to_run) > 1:
561
586
  if not click.confirm(f" Run [{i}] {cmd.cmd}?", default=True, err=True):
562
587
  continue
563
588
 
@@ -32,16 +32,14 @@ def resolve_scope(global_flag: bool) -> Path:
32
32
  """Return the path to the data file for the target scope.
33
33
 
34
34
  Uses the current git repo for scoping unless --global is set.
35
- Raises ClickException if outside a git repo without --global.
35
+ Falls back to global scope when outside a git repo.
36
36
  """
37
37
  if global_flag:
38
38
  return storage.GROUPS_GLOBAL_FILE
39
39
 
40
40
  repo = get_git_repo(os.getcwd())
41
41
  if repo is None:
42
- raise click.ClickException(
43
- "Not in a git repository. Use --global to save globally."
44
- )
42
+ return storage.GROUPS_GLOBAL_FILE
45
43
  sanitized = storage.sanitize_repo_name(repo)
46
44
  return storage.group_file_path(sanitized)
47
45
 
@@ -603,13 +603,14 @@ class TestSaveCLI:
603
603
  assert result.exit_code == 0
604
604
  assert "Already saved" in result.output
605
605
 
606
- def test_save_outside_repo_without_global_errors(self, tmp_mem_dir: Path):
606
+ def test_save_outside_repo_falls_back_to_global(self, tmp_mem_dir: Path):
607
607
  runner = CliRunner()
608
608
  with patch("mem.cli._current_repo", return_value=None), \
609
609
  patch("mem.groups.get_git_repo", return_value=None):
610
610
  result = runner.invoke(cli, ["save", "echo hello"])
611
- assert result.exit_code != 0
612
- assert "git repository" in result.output.lower() or "git repository" in str(result.exception).lower()
611
+ assert result.exit_code == 0
612
+ data = storage.read_group_file(storage.GROUPS_GLOBAL_FILE)
613
+ assert any(s.cmd == "echo hello" for s in data.saved)
613
614
 
614
615
  def test_save_in_repo(self, tmp_mem_dir: Path):
615
616
  runner = CliRunner()
@@ -675,6 +676,44 @@ class TestListCLI:
675
676
  assert result.exit_code == 0
676
677
  assert "shadowed" in result.output
677
678
 
679
+ def test_list_specific_group(self, tmp_mem_dir: Path):
680
+ storage.write_group_file(
681
+ storage.GROUPS_GLOBAL_FILE,
682
+ GroupFile(groups={"deploy": Group(
683
+ description="deploy steps",
684
+ commands=[
685
+ GroupCommand(cmd="make build", comment="compile"),
686
+ GroupCommand(cmd="make push"),
687
+ ],
688
+ )}),
689
+ )
690
+ runner = CliRunner()
691
+ result = runner.invoke(cli, ["list", "deploy", "--global"])
692
+ assert result.exit_code == 0
693
+ assert "deploy" in result.output
694
+ assert "make build" in result.output
695
+ assert "compile" in result.output
696
+ assert "make push" in result.output
697
+
698
+ def test_list_specific_group_not_found(self, tmp_mem_dir: Path):
699
+ runner = CliRunner()
700
+ result = runner.invoke(cli, ["list", "nope", "--global"])
701
+ assert result.exit_code != 0
702
+ assert "not found" in result.output.lower()
703
+
704
+ def test_list_specific_group_json(self, tmp_mem_dir: Path):
705
+ storage.write_group_file(
706
+ storage.GROUPS_GLOBAL_FILE,
707
+ GroupFile(groups={"g": Group(
708
+ commands=[GroupCommand(cmd="echo hi")],
709
+ )}),
710
+ )
711
+ runner = CliRunner()
712
+ result = runner.invoke(cli, ["list", "g", "--global", "--json"])
713
+ assert result.exit_code == 0
714
+ data = json.loads(result.output)
715
+ assert "g" in data
716
+
678
717
 
679
718
  class TestRunCLI:
680
719
  def _setup_group(self, tmp_mem_dir: Path):
@@ -723,6 +762,20 @@ class TestRunCLI:
723
762
  result = runner.invoke(cli, ["run", "test", "--global"], input="n\n")
724
763
  assert result.exit_code == 0
725
764
 
765
+ def test_run_all_skips_per_command_confirm(self, tmp_mem_dir: Path):
766
+ self._setup_group(tmp_mem_dir)
767
+ runner = CliRunner()
768
+ with patch("mem.cli._is_interactive", return_value=True):
769
+ # "y\n" = run all — should NOT prompt for each command
770
+ result = runner.invoke(cli, ["run", "test", "--global"], input="y\n")
771
+ assert result.exit_code == 0
772
+ # All 3 commands should appear as executed ($ echo N)
773
+ assert "$ echo 1" in result.output
774
+ assert "$ echo 2" in result.output
775
+ assert "$ echo 3" in result.output
776
+ # Should NOT contain per-command confirmation prompts
777
+ assert "Run [1]" not in result.output
778
+
726
779
  def test_run_non_tty_without_yes_errors(self, tmp_mem_dir: Path):
727
780
  self._setup_group(tmp_mem_dir)
728
781
  runner = CliRunner()
@@ -1159,10 +1212,10 @@ class TestResolveScope:
1159
1212
  expected_name = storage.sanitize_repo_name(FAKE_REPO)
1160
1213
  assert path == storage.group_file_path(expected_name)
1161
1214
 
1162
- def test_outside_repo_raises(self, tmp_mem_dir: Path):
1215
+ def test_outside_repo_falls_back_to_global(self, tmp_mem_dir: Path):
1163
1216
  with patch("mem.groups.get_git_repo", return_value=None):
1164
- with pytest.raises(click.ClickException, match="git repository"):
1165
- groups.resolve_scope(global_flag=False)
1217
+ path = groups.resolve_scope(global_flag=False)
1218
+ assert path == storage.GROUPS_GLOBAL_FILE
1166
1219
 
1167
1220
 
1168
1221
  class TestSaveNonInteractive:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes