ai-memory-cli 0.1.9__tar.gz → 0.1.11__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.
- {ai_memory_cli-0.1.9/src/ai_memory_cli.egg-info → ai_memory_cli-0.1.11}/PKG-INFO +2 -1
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/README.md +1 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/pyproject.toml +1 -1
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli/__init__.py +1 -1
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli/cli.py +178 -21
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11/src/ai_memory_cli.egg-info}/PKG-INFO +2 -1
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/LICENSE +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/bin/watch.cmd +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/setup.cfg +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli/__main__.py +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli.egg-info/SOURCES.txt +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli.egg-info/dependency_links.txt +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli.egg-info/entry_points.txt +0 -0
- {ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-memory-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Python CLI for AI Memory terminal capture and offline sync.
|
|
5
5
|
Author: AI Memory
|
|
6
6
|
License-Expression: MIT
|
|
@@ -70,6 +70,7 @@ On Windows, `python -m ai_memory_cli ...` is the safest form because it avoids P
|
|
|
70
70
|
Inside `watch`, type the real command you want to capture, for example `python --version`. Do not type `python -m ai_memory_cli run -- ...` inside `watch`, or you will capture the nested CLI command too.
|
|
71
71
|
Use `cls` on Windows or `clear` on Unix shells to clear the watch screen; those control commands are not stored or synced.
|
|
72
72
|
On Windows, commands run through PowerShell, so type `ls` directly. Do not type `powershell` or `cmd` inside `watch`; nested shell launchers are ignored and not stored.
|
|
73
|
+
The watch prompt shows the active folder, for example `ai-memory C:\work\repo>`. Use `cd`, `chdir`, `cd..`, `cd /d D:\path`, `cd ~`, or `cd -` normally; successful directory changes are tracked as hashed terminal events and become the working folder for the next command.
|
|
73
74
|
|
|
74
75
|
## Background agent
|
|
75
76
|
|
|
@@ -48,6 +48,7 @@ On Windows, `python -m ai_memory_cli ...` is the safest form because it avoids P
|
|
|
48
48
|
Inside `watch`, type the real command you want to capture, for example `python --version`. Do not type `python -m ai_memory_cli run -- ...` inside `watch`, or you will capture the nested CLI command too.
|
|
49
49
|
Use `cls` on Windows or `clear` on Unix shells to clear the watch screen; those control commands are not stored or synced.
|
|
50
50
|
On Windows, commands run through PowerShell, so type `ls` directly. Do not type `powershell` or `cmd` inside `watch`; nested shell launchers are ignored and not stored.
|
|
51
|
+
The watch prompt shows the active folder, for example `ai-memory C:\work\repo>`. Use `cd`, `chdir`, `cd..`, `cd /d D:\path`, `cd ~`, or `cd -` normally; successful directory changes are tracked as hashed terminal events and become the working folder for the next command.
|
|
51
52
|
|
|
52
53
|
## Background agent
|
|
53
54
|
|
|
@@ -78,7 +78,7 @@ def read_json(path: Path, default: Any) -> Any:
|
|
|
78
78
|
if not path.exists():
|
|
79
79
|
return default
|
|
80
80
|
try:
|
|
81
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
82
82
|
except (OSError, json.JSONDecodeError):
|
|
83
83
|
return default
|
|
84
84
|
|
|
@@ -327,6 +327,60 @@ def is_clear_command(command: str) -> bool:
|
|
|
327
327
|
return normalize_command(command).lower() in CLEAR_COMMANDS
|
|
328
328
|
|
|
329
329
|
|
|
330
|
+
def parse_cd_command(command: str) -> str | None:
|
|
331
|
+
stripped = command.strip()
|
|
332
|
+
lower = stripped.lower()
|
|
333
|
+
if lower in {"cd..", "chdir.."}:
|
|
334
|
+
return ".."
|
|
335
|
+
if os.name == "nt" and lower.startswith("cd\\"):
|
|
336
|
+
return stripped[2:].strip()
|
|
337
|
+
|
|
338
|
+
match = re.match(r"^(cd|chdir)(?:\s+(.*))?$", stripped, flags=re.IGNORECASE)
|
|
339
|
+
if not match:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
target = (match.group(2) or "").strip()
|
|
343
|
+
if os.name == "nt" and target.lower().startswith("/d"):
|
|
344
|
+
target = target[2:].strip()
|
|
345
|
+
if len(target) >= 2 and target[0] == target[-1] and target[0] in {"'", '"'}:
|
|
346
|
+
target = target[1:-1]
|
|
347
|
+
return target
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def resolve_cd_target(target: str, cwd: Path, previous_cwd: Path | None) -> tuple[Path | None, str, bool]:
|
|
351
|
+
current = cwd.resolve()
|
|
352
|
+
if not target:
|
|
353
|
+
return current, str(current) + os.linesep, False
|
|
354
|
+
if target == "-":
|
|
355
|
+
if previous_cwd:
|
|
356
|
+
return previous_cwd.resolve(), str(previous_cwd.resolve()) + os.linesep, True
|
|
357
|
+
return None, "ai-memory: no previous directory for cd -\n", True
|
|
358
|
+
|
|
359
|
+
expanded = os.path.expandvars(target)
|
|
360
|
+
if os.name == "nt" and re.fullmatch(r"[A-Za-z]:", expanded):
|
|
361
|
+
expanded = f"{expanded}\\"
|
|
362
|
+
|
|
363
|
+
candidate = Path(expanded).expanduser()
|
|
364
|
+
if not candidate.is_absolute():
|
|
365
|
+
candidate = current / candidate
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
resolved = candidate.resolve(strict=False)
|
|
369
|
+
except OSError:
|
|
370
|
+
resolved = candidate
|
|
371
|
+
|
|
372
|
+
if not resolved.exists() or not resolved.is_dir():
|
|
373
|
+
if os.name == "nt":
|
|
374
|
+
return None, "The system cannot find the path specified.\n", True
|
|
375
|
+
return None, f"cd: no such file or directory: {target}\n", True
|
|
376
|
+
|
|
377
|
+
return resolved, "", True
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def watch_prompt(cwd: Path) -> str:
|
|
381
|
+
return f"ai-memory {cwd.resolve()}> "
|
|
382
|
+
|
|
383
|
+
|
|
330
384
|
def is_interactive_shell_command(command: str) -> bool:
|
|
331
385
|
tokens = normalize_command(command).lower().split()
|
|
332
386
|
if not tokens:
|
|
@@ -688,7 +742,115 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
|
|
|
688
742
|
return synced
|
|
689
743
|
|
|
690
744
|
|
|
691
|
-
def
|
|
745
|
+
def store_captured_event(
|
|
746
|
+
home: Path,
|
|
747
|
+
config: dict[str, Any],
|
|
748
|
+
command: str,
|
|
749
|
+
stdout: str,
|
|
750
|
+
stderr: str,
|
|
751
|
+
exit_code: int | None,
|
|
752
|
+
started_at: str,
|
|
753
|
+
ended_at: str,
|
|
754
|
+
duration_ms: int,
|
|
755
|
+
cwd: Path,
|
|
756
|
+
source: str,
|
|
757
|
+
extra_metadata: dict[str, Any] | None = None,
|
|
758
|
+
) -> tuple[dict[str, Any], bool, Path]:
|
|
759
|
+
event = make_terminal_event(
|
|
760
|
+
command=command,
|
|
761
|
+
stdout=stdout,
|
|
762
|
+
stderr=stderr,
|
|
763
|
+
exit_code=exit_code,
|
|
764
|
+
started_at=started_at,
|
|
765
|
+
ended_at=ended_at,
|
|
766
|
+
duration_ms=duration_ms,
|
|
767
|
+
cwd=cwd,
|
|
768
|
+
config=config,
|
|
769
|
+
source=source,
|
|
770
|
+
)
|
|
771
|
+
if extra_metadata:
|
|
772
|
+
event["metadata"].update(extra_metadata)
|
|
773
|
+
created, event_path = store_event(home, event)
|
|
774
|
+
state = "stored" if created else "deduped"
|
|
775
|
+
append_history(home, event, state)
|
|
776
|
+
print(f"ai-memory: {state} terminal hash {event['event_hash'][:12]} at {event_path}")
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
sync_events(home, config, quiet=True)
|
|
780
|
+
except Exception as exc:
|
|
781
|
+
print(f"ai-memory: sync queued until network/API is available ({exc})", file=sys.stderr)
|
|
782
|
+
|
|
783
|
+
return event, created, event_path
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def capture_cd_command(
|
|
787
|
+
home: Path,
|
|
788
|
+
config: dict[str, Any],
|
|
789
|
+
command: str,
|
|
790
|
+
cwd: Path,
|
|
791
|
+
previous_cwd: Path | None,
|
|
792
|
+
source: str,
|
|
793
|
+
) -> tuple[int, Path, Path | None]:
|
|
794
|
+
require_verified_auth(home, config)
|
|
795
|
+
target = parse_cd_command(command)
|
|
796
|
+
if target is None:
|
|
797
|
+
return 1, cwd, previous_cwd
|
|
798
|
+
|
|
799
|
+
old_cwd = cwd.resolve()
|
|
800
|
+
started_at = utc_now()
|
|
801
|
+
started_monotonic = time.monotonic()
|
|
802
|
+
new_cwd, output, should_track = resolve_cd_target(target, old_cwd, previous_cwd)
|
|
803
|
+
ended_at = utc_now()
|
|
804
|
+
duration_ms = int((time.monotonic() - started_monotonic) * 1000)
|
|
805
|
+
|
|
806
|
+
if output:
|
|
807
|
+
if new_cwd is None:
|
|
808
|
+
print(output, end="", file=sys.stderr)
|
|
809
|
+
else:
|
|
810
|
+
print(output, end="")
|
|
811
|
+
|
|
812
|
+
if new_cwd is None:
|
|
813
|
+
print("ai-memory: skipped invalid cd; nothing was stored.")
|
|
814
|
+
return 1, old_cwd, previous_cwd
|
|
815
|
+
|
|
816
|
+
changed = new_cwd.resolve() != old_cwd
|
|
817
|
+
if should_track:
|
|
818
|
+
store_captured_event(
|
|
819
|
+
home=home,
|
|
820
|
+
config=config,
|
|
821
|
+
command=command,
|
|
822
|
+
stdout=output if new_cwd is not None else "",
|
|
823
|
+
stderr="",
|
|
824
|
+
exit_code=0,
|
|
825
|
+
started_at=started_at,
|
|
826
|
+
ended_at=ended_at,
|
|
827
|
+
duration_ms=duration_ms,
|
|
828
|
+
cwd=old_cwd,
|
|
829
|
+
source=source,
|
|
830
|
+
extra_metadata={
|
|
831
|
+
"builtin": "cd",
|
|
832
|
+
"cwd_changed": changed,
|
|
833
|
+
"new_cwd_hash": sha256_text(str(new_cwd.resolve())),
|
|
834
|
+
"new_cwd_tail": new_cwd.resolve().name,
|
|
835
|
+
},
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
return 0, new_cwd.resolve(), old_cwd if changed else previous_cwd
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def default_command_cwd(config: dict[str, Any]) -> Path:
|
|
842
|
+
workspace = Path(str(config.get("workspace_path") or ".")).expanduser()
|
|
843
|
+
return workspace.resolve() if workspace.exists() else Path.cwd().resolve()
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def capture_command(
|
|
847
|
+
home: Path,
|
|
848
|
+
config: dict[str, Any],
|
|
849
|
+
command: str,
|
|
850
|
+
include_excluded: bool,
|
|
851
|
+
source: str,
|
|
852
|
+
cwd: Path | None = None,
|
|
853
|
+
) -> int:
|
|
692
854
|
if is_clear_command(command):
|
|
693
855
|
clear_console()
|
|
694
856
|
return 0
|
|
@@ -698,16 +860,15 @@ def capture_command(home: Path, config: dict[str, Any], command: str, include_ex
|
|
|
698
860
|
return 0
|
|
699
861
|
|
|
700
862
|
require_verified_auth(home, config)
|
|
701
|
-
|
|
702
|
-
cwd = workspace if workspace.exists() else Path.cwd()
|
|
863
|
+
effective_cwd = cwd.resolve() if cwd else default_command_cwd(config)
|
|
703
864
|
|
|
704
865
|
if is_excluded(command, config) and not include_excluded:
|
|
705
866
|
print(f"ai-memory: running without capture because this command is excluded: {command}", file=sys.stderr)
|
|
706
|
-
return int(run_external_command(command,
|
|
867
|
+
return int(run_external_command(command, effective_cwd, capture=False))
|
|
707
868
|
|
|
708
869
|
started_at = utc_now()
|
|
709
870
|
started_monotonic = time.monotonic()
|
|
710
|
-
completed = run_external_command(command,
|
|
871
|
+
completed = run_external_command(command, effective_cwd, capture=True)
|
|
711
872
|
if isinstance(completed, int):
|
|
712
873
|
return completed
|
|
713
874
|
ended_at = utc_now()
|
|
@@ -722,7 +883,9 @@ def capture_command(home: Path, config: dict[str, Any], command: str, include_ex
|
|
|
722
883
|
print("ai-memory: skipped invalid command; nothing was stored.")
|
|
723
884
|
return completed.returncode
|
|
724
885
|
|
|
725
|
-
|
|
886
|
+
store_captured_event(
|
|
887
|
+
home=home,
|
|
888
|
+
config=config,
|
|
726
889
|
command=command,
|
|
727
890
|
stdout=completed.stdout or "",
|
|
728
891
|
stderr=completed.stderr or "",
|
|
@@ -730,20 +893,9 @@ def capture_command(home: Path, config: dict[str, Any], command: str, include_ex
|
|
|
730
893
|
started_at=started_at,
|
|
731
894
|
ended_at=ended_at,
|
|
732
895
|
duration_ms=duration_ms,
|
|
733
|
-
cwd=
|
|
734
|
-
config=config,
|
|
896
|
+
cwd=effective_cwd,
|
|
735
897
|
source=source,
|
|
736
898
|
)
|
|
737
|
-
created, event_path = store_event(home, event)
|
|
738
|
-
state = "stored" if created else "deduped"
|
|
739
|
-
append_history(home, event, state)
|
|
740
|
-
print(f"ai-memory: {state} terminal hash {event['event_hash'][:12]} at {event_path}")
|
|
741
|
-
|
|
742
|
-
try:
|
|
743
|
-
sync_events(home, config, quiet=True)
|
|
744
|
-
except Exception as exc:
|
|
745
|
-
print(f"ai-memory: sync queued until network/API is available ({exc})", file=sys.stderr)
|
|
746
|
-
|
|
747
899
|
return completed.returncode
|
|
748
900
|
|
|
749
901
|
|
|
@@ -909,10 +1061,12 @@ def command_watch(args: argparse.Namespace) -> int:
|
|
|
909
1061
|
prompt_for_auth(home, config)
|
|
910
1062
|
config = load_config(home)
|
|
911
1063
|
|
|
1064
|
+
cwd = default_command_cwd(config)
|
|
1065
|
+
previous_cwd: Path | None = None
|
|
912
1066
|
print("AI Memory watch mode. Type commands to run and capture. Type exit to stop.")
|
|
913
1067
|
while True:
|
|
914
1068
|
try:
|
|
915
|
-
command = input(
|
|
1069
|
+
command = input(watch_prompt(cwd)).strip()
|
|
916
1070
|
except (EOFError, KeyboardInterrupt):
|
|
917
1071
|
print()
|
|
918
1072
|
break
|
|
@@ -933,7 +1087,10 @@ def command_watch(args: argparse.Namespace) -> int:
|
|
|
933
1087
|
if is_interactive_shell_command(command):
|
|
934
1088
|
print("ai-memory: do not start a nested shell here. Type commands directly, for example: ls")
|
|
935
1089
|
continue
|
|
936
|
-
|
|
1090
|
+
if parse_cd_command(command) is not None:
|
|
1091
|
+
_, cwd, previous_cwd = capture_cd_command(home, config, command, cwd, previous_cwd, "watch")
|
|
1092
|
+
continue
|
|
1093
|
+
capture_command(home, config, command, args.include_excluded, "watch", cwd=cwd)
|
|
937
1094
|
return 0
|
|
938
1095
|
|
|
939
1096
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-memory-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Python CLI for AI Memory terminal capture and offline sync.
|
|
5
5
|
Author: AI Memory
|
|
6
6
|
License-Expression: MIT
|
|
@@ -70,6 +70,7 @@ On Windows, `python -m ai_memory_cli ...` is the safest form because it avoids P
|
|
|
70
70
|
Inside `watch`, type the real command you want to capture, for example `python --version`. Do not type `python -m ai_memory_cli run -- ...` inside `watch`, or you will capture the nested CLI command too.
|
|
71
71
|
Use `cls` on Windows or `clear` on Unix shells to clear the watch screen; those control commands are not stored or synced.
|
|
72
72
|
On Windows, commands run through PowerShell, so type `ls` directly. Do not type `powershell` or `cmd` inside `watch`; nested shell launchers are ignored and not stored.
|
|
73
|
+
The watch prompt shows the active folder, for example `ai-memory C:\work\repo>`. Use `cd`, `chdir`, `cd..`, `cd /d D:\path`, `cd ~`, or `cd -` normally; successful directory changes are tracked as hashed terminal events and become the working folder for the next command.
|
|
73
74
|
|
|
74
75
|
## Background agent
|
|
75
76
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_memory_cli-0.1.9 → ai_memory_cli-0.1.11}/src/ai_memory_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|