ai-memory-cli 0.1.5__tar.gz → 0.1.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-memory-cli
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Python CLI for AI Memory terminal capture and offline sync.
5
5
  Author: AI Memory
6
6
  License-Expression: MIT
@@ -53,6 +53,13 @@ python -m ai_memory_cli workspace connect --path . --repo owner/repo --editor vs
53
53
  watch
54
54
  ```
55
55
 
56
+ The `auth` command verifies the website-issued token with FastAPI before it is saved locally. After verification,
57
+ the CLI stores a SHA-512 user hash for this computer, binds the token to that hash on the server, and starts the
58
+ background sync agent once on Windows.
59
+
60
+ If you run `watch` before auth, it will prompt for the website CLI token and FastAPI URL, then continue into
61
+ terminal capture after verification.
62
+
56
63
  `watch` is a shortcut for `python -m ai_memory_cli watch`. If Windows Device Guard blocks the generated launcher, keep using `python -m ai_memory_cli watch`.
57
64
  On Windows the shortcut is installed as `watch.cmd`; the Python Scripts folder must be on `PATH` for bare `watch` to resolve.
58
65
 
@@ -64,12 +71,13 @@ Inside `watch`, type the real command you want to capture, for example `python -
64
71
 
65
72
  ## Background agent
66
73
 
67
- The background agent starts at Windows logon and keeps syncing queued terminal hashes whenever the API is reachable:
74
+ After `auth`, the background agent starts once and is installed in the Windows Startup folder so queued terminal
75
+ hashes keep syncing whenever the API is reachable. Use these commands when you need manual control:
68
76
 
69
77
  ```powershell
70
- python -m ai_memory_cli agent install
71
- python -m ai_memory_cli agent start
72
78
  python -m ai_memory_cli agent status
79
+ python -m ai_memory_cli agent stop
80
+ python -m ai_memory_cli agent start
73
81
  ```
74
82
 
75
83
  The agent does not secretly capture every terminal on the computer. Commands are captured when they run through:
@@ -31,6 +31,13 @@ python -m ai_memory_cli workspace connect --path . --repo owner/repo --editor vs
31
31
  watch
32
32
  ```
33
33
 
34
+ The `auth` command verifies the website-issued token with FastAPI before it is saved locally. After verification,
35
+ the CLI stores a SHA-512 user hash for this computer, binds the token to that hash on the server, and starts the
36
+ background sync agent once on Windows.
37
+
38
+ If you run `watch` before auth, it will prompt for the website CLI token and FastAPI URL, then continue into
39
+ terminal capture after verification.
40
+
34
41
  `watch` is a shortcut for `python -m ai_memory_cli watch`. If Windows Device Guard blocks the generated launcher, keep using `python -m ai_memory_cli watch`.
35
42
  On Windows the shortcut is installed as `watch.cmd`; the Python Scripts folder must be on `PATH` for bare `watch` to resolve.
36
43
 
@@ -42,12 +49,13 @@ Inside `watch`, type the real command you want to capture, for example `python -
42
49
 
43
50
  ## Background agent
44
51
 
45
- The background agent starts at Windows logon and keeps syncing queued terminal hashes whenever the API is reachable:
52
+ After `auth`, the background agent starts once and is installed in the Windows Startup folder so queued terminal
53
+ hashes keep syncing whenever the API is reachable. Use these commands when you need manual control:
46
54
 
47
55
  ```powershell
48
- python -m ai_memory_cli agent install
49
- python -m ai_memory_cli agent start
50
56
  python -m ai_memory_cli agent status
57
+ python -m ai_memory_cli agent stop
58
+ python -m ai_memory_cli agent start
51
59
  ```
52
60
 
53
61
  The agent does not secretly capture every terminal on the computer. Commands are captured when they run through:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-memory-cli"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "Python CLI for AI Memory terminal capture and offline sync."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """AI Memory terminal capture CLI."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.7"
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import getpass
4
5
  import json
5
6
  import os
6
7
  import platform
7
8
  import re
9
+ import secrets
8
10
  import shutil
9
11
  import subprocess
10
12
  import sys
@@ -52,6 +54,12 @@ def sha256_text(value: str) -> str:
52
54
  return hashlib.sha256(value.encode("utf-8", errors="replace")).hexdigest()
53
55
 
54
56
 
57
+ def sha512_text(value: str) -> str:
58
+ import hashlib
59
+
60
+ return hashlib.sha512(value.encode("utf-8", errors="replace")).hexdigest()
61
+
62
+
55
63
  def cli_home() -> Path:
56
64
  configured = os.getenv("AI_MEMORY_CLI_HOME")
57
65
  if configured:
@@ -126,6 +134,50 @@ def save_config(home: Path, config: dict[str, Any]) -> None:
126
134
  write_json(config_path(home), config)
127
135
 
128
136
 
137
+ def ensure_local_identity(home: Path, config: dict[str, Any]) -> str:
138
+ secret = str(config.get("local_identity_secret") or "").strip()
139
+ if not secret:
140
+ secret = secrets.token_urlsafe(96)
141
+ config["local_identity_secret"] = secret
142
+ config["local_identity_created_at"] = utc_now()
143
+
144
+ local_user_hash = sha512_text(
145
+ "\0".join(
146
+ [
147
+ secret,
148
+ str(home),
149
+ platform.node() or "unknown-host",
150
+ getpass.getuser() or "unknown-user",
151
+ platform.system(),
152
+ ]
153
+ )
154
+ )
155
+ config["local_user_hash"] = local_user_hash
156
+ return local_user_hash
157
+
158
+
159
+ def client_identity(home: Path, config: dict[str, Any]) -> dict[str, Any]:
160
+ local_user_hash = ensure_local_identity(home, config)
161
+ return {
162
+ "name": "ai-memory-cli",
163
+ "version": __version__,
164
+ "local_user_hash": local_user_hash,
165
+ "user_hash": config.get("user_hash") or "",
166
+ "github_user": config.get("github_user") or "",
167
+ "session_id": config.get("session_id") or "",
168
+ "storage_home_hash": sha256_text(str(home)),
169
+ "hostname_hash": sha256_text(platform.node() or "unknown"),
170
+ "username_hash": sha256_text(getpass.getuser() or "unknown"),
171
+ "platform": platform.system(),
172
+ "python": platform.python_version(),
173
+ "auth_verified_at": config.get("auth_verified_at") or "",
174
+ }
175
+
176
+
177
+ def has_verified_auth(config: dict[str, Any]) -> bool:
178
+ return bool(config.get("token") and config.get("auth_verified_at") and config.get("user_hash") and config.get("local_user_hash"))
179
+
180
+
129
181
  def agent_state_path(home: Path) -> Path:
130
182
  return home / "agent.json"
131
183
 
@@ -200,6 +252,63 @@ def start_detached_agent(interval: int, limit: int) -> int:
200
252
  return int(process.pid)
201
253
 
202
254
 
255
+ def is_process_running(pid: Any) -> bool:
256
+ try:
257
+ pid_int = int(pid)
258
+ except (TypeError, ValueError):
259
+ return False
260
+ if pid_int <= 0:
261
+ return False
262
+
263
+ if os.name == "nt":
264
+ result = subprocess.run(
265
+ ["tasklist", "/FI", f"PID eq {pid_int}", "/FO", "CSV", "/NH"],
266
+ capture_output=True,
267
+ text=True,
268
+ encoding="utf-8",
269
+ errors="replace",
270
+ )
271
+ return result.returncode == 0 and str(pid_int) in result.stdout
272
+
273
+ try:
274
+ os.kill(pid_int, 0)
275
+ return True
276
+ except OSError:
277
+ return False
278
+
279
+
280
+ def ensure_agent_started_once(home: Path, config: dict[str, Any]) -> None:
281
+ agent_config = config.get("agent") if isinstance(config.get("agent"), dict) else {}
282
+ interval = int(agent_config.get("interval_seconds") or DEFAULT_AGENT_INTERVAL_SECONDS)
283
+ limit = int(agent_config.get("limit") or 50)
284
+ state = read_json(agent_state_path(home), {})
285
+
286
+ if is_process_running(state.get("pid")):
287
+ print(f"AI Memory sync agent already running: pid={state.get('pid')}")
288
+ return
289
+
290
+ if os.name == "nt":
291
+ script_path = write_windows_startup_script(interval, limit)
292
+ pid = start_detached_agent(interval, limit)
293
+ config["agent"] = {
294
+ **agent_config,
295
+ "method": "startup",
296
+ "interval_seconds": interval,
297
+ "limit": limit,
298
+ "startup_script": str(script_path),
299
+ "last_started_pid": pid,
300
+ "last_started_at": utc_now(),
301
+ }
302
+ save_config(home, config)
303
+ append_log(home, f"auth started detached agent pid={pid}")
304
+ print(f"AI Memory sync agent started once: pid={pid}")
305
+ print(f"Startup sync installed at: {script_path}")
306
+ return
307
+
308
+ print("Automatic startup agent install is only implemented for Windows.")
309
+ print("Start sync manually with: python -m ai_memory_cli agent run")
310
+
311
+
203
312
  def normalize_command(command: str) -> str:
204
313
  return " ".join(command.strip().split())
205
314
 
@@ -283,6 +392,105 @@ def http_json(
283
392
  return json.loads(response_body)
284
393
 
285
394
 
395
+ def describe_http_error(exc: urllib.error.HTTPError) -> str:
396
+ try:
397
+ body = exc.read().decode("utf-8", errors="replace")
398
+ payload = json.loads(body) if body else {}
399
+ detail = payload.get("detail") if isinstance(payload, dict) else None
400
+ if detail:
401
+ return str(detail)
402
+ if body:
403
+ return body[:500]
404
+ except Exception:
405
+ pass
406
+ return f"HTTP {exc.code} {exc.reason}"
407
+
408
+
409
+ def verify_cli_auth(home: Path, config: dict[str, Any], token: str) -> dict[str, Any]:
410
+ identity = client_identity(home, config)
411
+ response = http_json(
412
+ "POST",
413
+ f"{api_url(config)}/cli/auth/verify",
414
+ {"client": identity},
415
+ token,
416
+ )
417
+ if not response.get("verified"):
418
+ raise RuntimeError("CLI token was not verified by the backend.")
419
+
420
+ config["token"] = token
421
+ config["token_hash"] = sha256_text(token)
422
+ config["token_tail"] = response.get("token_tail") or ""
423
+ config["session_id"] = response.get("session_id") or ""
424
+ config["github_user"] = response.get("github_user") or response.get("github_account_name") or ""
425
+ config["user_hash"] = response.get("user_hash") or ""
426
+ config["bound_local_user_hash"] = response.get("bound_local_user_hash") or identity["local_user_hash"]
427
+ config["auth_verified_at"] = response.get("verified_at") or utc_now()
428
+ config["server_account_storage_dir"] = response.get("account_storage_dir") or ""
429
+ return response
430
+
431
+
432
+ def finish_auth(home: Path, config: dict[str, Any], token: str, start_agent: bool = True) -> dict[str, Any]:
433
+ config["pending_token_hash"] = sha256_text(token)
434
+ response = verify_cli_auth(home, config, token)
435
+ config["authed_at"] = utc_now()
436
+ save_config(home, config)
437
+
438
+ print(f"Saved CLI auth in {config_path(home)}")
439
+ print(f"GitHub account: {response.get('github_user') or config.get('github_user') or '-'}")
440
+ print(f"Local user hash: {str(config.get('user_hash') or '')[:24]}...")
441
+ if response.get("account_storage_dir"):
442
+ print(f"Server account storage: {response['account_storage_dir']}")
443
+
444
+ if start_agent:
445
+ ensure_agent_started_once(home, config)
446
+
447
+ try:
448
+ synced = sync_events(home, config, quiet=True)
449
+ if synced:
450
+ print(f"Synced {synced} queued terminal event(s).")
451
+ except Exception as exc:
452
+ print(f"Auth saved. Sync will retry later: {exc}", file=sys.stderr)
453
+
454
+ return response
455
+
456
+
457
+ def prompt_for_auth(home: Path, config: dict[str, Any]) -> dict[str, Any]:
458
+ print("AI Memory needs website auth before watch can capture and sync.")
459
+ print("Generate a CLI token from the website Integrations page, then paste it here.")
460
+ token = getpass.getpass("Website CLI token: ").strip()
461
+ if not token:
462
+ raise SystemExit("No token entered. Generate a CLI token from the website and run watch again.")
463
+
464
+ current_api_url = api_url(config)
465
+ entered_api_url = input(f"FastAPI URL [{current_api_url}]: ").strip()
466
+ if entered_api_url:
467
+ config["api_url"] = entered_api_url.rstrip("/")
468
+
469
+ try:
470
+ return finish_auth(home, config, token, start_agent=True)
471
+ except urllib.error.HTTPError as exc:
472
+ save_config(home, config)
473
+ raise SystemExit(f"CLI auth failed: {describe_http_error(exc)}") from exc
474
+ except Exception as exc:
475
+ save_config(home, config)
476
+ raise SystemExit(f"CLI auth failed. Keep the local FastAPI server running and generate a fresh website token: {exc}") from exc
477
+
478
+
479
+ def require_verified_auth(home: Path, config: dict[str, Any]) -> str:
480
+ token = require_token(config)
481
+ if has_verified_auth(config):
482
+ return token
483
+
484
+ try:
485
+ verify_cli_auth(home, config, token)
486
+ save_config(home, config)
487
+ return token
488
+ except urllib.error.HTTPError as exc:
489
+ raise SystemExit(f"CLI auth is not verified: {describe_http_error(exc)}") from exc
490
+ except Exception as exc:
491
+ raise SystemExit(f"CLI auth is not verified. Run website auth again when the local server is available: {exc}") from exc
492
+
493
+
286
494
  def make_terminal_event(
287
495
  command: str,
288
496
  stdout: str,
@@ -388,6 +596,14 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
388
596
  if not quiet:
389
597
  print("No CLI token saved. Events remain queued until python -m ai_memory_cli auth is configured.")
390
598
  return 0
599
+ if not has_verified_auth(config):
600
+ try:
601
+ verify_cli_auth(home, config, token)
602
+ save_config(home, config)
603
+ except Exception as exc:
604
+ if not quiet:
605
+ print(f"CLI auth is not verified yet. Events remain queued: {exc}")
606
+ return 0
391
607
  paths = sorted((home / "outbox").glob("*.json"))[:limit]
392
608
  if not paths:
393
609
  if not quiet:
@@ -401,12 +617,7 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
401
617
 
402
618
  payload = {
403
619
  "events": events,
404
- "client": {
405
- "name": "ai-memory-cli",
406
- "version": __version__,
407
- "storage_home_hash": sha256_text(str(home)),
408
- "hostname_hash": sha256_text(platform.node() or "unknown"),
409
- },
620
+ "client": client_identity(home, config),
410
621
  }
411
622
  response = http_json("POST", f"{api_url(config)}/cli/events/terminal", payload, token)
412
623
  accepted = {item.get("event_hash") for item in response.get("events", []) if item.get("event_hash")}
@@ -421,6 +632,7 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
421
632
 
422
633
 
423
634
  def capture_command(home: Path, config: dict[str, Any], command: str, include_excluded: bool, source: str) -> int:
635
+ require_verified_auth(home, config)
424
636
  workspace = Path(str(config.get("workspace_path") or ".")).expanduser()
425
637
  cwd = workspace if workspace.exists() else Path.cwd()
426
638
 
@@ -498,17 +710,16 @@ def command_auth(args: argparse.Namespace) -> int:
498
710
  config = load_config(home)
499
711
  if args.api_url:
500
712
  config["api_url"] = args.api_url.rstrip("/")
501
- config["token"] = args.token.strip()
502
- config["token_hash"] = sha256_text(args.token.strip())
503
- config["authed_at"] = utc_now()
504
- save_config(home, config)
505
- print(f"Saved CLI auth in {config_path(home)}")
506
713
 
714
+ token = args.token.strip()
507
715
  try:
508
- health = http_json("GET", f"{api_url(config)}/health", None, None)
509
- print(f"Connected to API: {health.get('service', api_url(config))}")
716
+ finish_auth(home, config, token, start_agent=not args.no_agent)
717
+ except urllib.error.HTTPError as exc:
718
+ save_config(home, config)
719
+ raise SystemExit(f"CLI auth failed: {describe_http_error(exc)}") from exc
510
720
  except Exception as exc:
511
- print(f"Auth saved. API check failed, sync will retry later: {exc}", file=sys.stderr)
721
+ save_config(home, config)
722
+ raise SystemExit(f"CLI auth failed. Keep the local FastAPI server running and generate a fresh website token: {exc}") from exc
512
723
  return 0
513
724
 
514
725
 
@@ -525,7 +736,7 @@ def command_init(args: argparse.Namespace) -> int:
525
736
  config["workspace_path"] = args.workspace
526
737
  save_config(home, config)
527
738
 
528
- token = require_token(config)
739
+ token = require_verified_auth(home, config)
529
740
  payload = {
530
741
  "project": config.get("project") or "memory-project",
531
742
  "repository": config.get("repository") or "",
@@ -538,7 +749,7 @@ def command_init(args: argparse.Namespace) -> int:
538
749
  print(f"Initialized project: {project.get('id', payload['project'])}")
539
750
  except Exception as exc:
540
751
  print(f"Project config saved locally. Server init will need retry: {exc}", file=sys.stderr)
541
- print("Start terminal capture with: python -m ai_memory_cli watch")
752
+ print("Start terminal capture with: watch")
542
753
  return 0
543
754
 
544
755
 
@@ -551,7 +762,8 @@ def command_workspace_connect(args: argparse.Namespace) -> int:
551
762
  config["repository"] = args.repo
552
763
  save_config(home, config)
553
764
 
554
- token = require_token(config)
765
+ token = require_verified_auth(home, config)
766
+ identity = client_identity(home, config)
555
767
  payload = {
556
768
  "payload": {
557
769
  "source": "ai-memory-cli",
@@ -561,6 +773,9 @@ def command_workspace_connect(args: argparse.Namespace) -> int:
561
773
  "editor": args.editor,
562
774
  "package_manager": args.package_manager,
563
775
  "cli_storage_home_hash": sha256_text(str(home)),
776
+ "local_user_hash": identity["local_user_hash"],
777
+ "user_hash": identity["user_hash"],
778
+ "github_user": identity["github_user"],
564
779
  }
565
780
  }
566
781
  response = http_json("POST", f"{api_url(config)}/workspace/connect", payload, token)
@@ -572,7 +787,8 @@ def command_workspace_connect(args: argparse.Namespace) -> int:
572
787
  def command_mcp_connect(args: argparse.Namespace) -> int:
573
788
  home = cli_home()
574
789
  config = load_config(home)
575
- token = require_token(config)
790
+ token = require_verified_auth(home, config)
791
+ identity = client_identity(home, config)
576
792
  config["mcp_server"] = args.server
577
793
  save_config(home, config)
578
794
  payload = {
@@ -582,6 +798,9 @@ def command_mcp_connect(args: argparse.Namespace) -> int:
582
798
  "project": config.get("project") or "",
583
799
  "repository": config.get("repository") or "",
584
800
  "cli_storage_home_hash": sha256_text(str(home)),
801
+ "local_user_hash": identity["local_user_hash"],
802
+ "user_hash": identity["user_hash"],
803
+ "github_user": identity["github_user"],
585
804
  }
586
805
  }
587
806
  response = http_json("POST", f"{api_url(config)}/mcp/connect", payload, token)
@@ -593,7 +812,8 @@ def command_mcp_connect(args: argparse.Namespace) -> int:
593
812
  def command_chat_connect(args: argparse.Namespace) -> int:
594
813
  home = cli_home()
595
814
  config = load_config(home)
596
- token = require_token(config)
815
+ token = require_verified_auth(home, config)
816
+ identity = client_identity(home, config)
597
817
  config["chat_provider"] = args.provider
598
818
  save_config(home, config)
599
819
  payload = {
@@ -603,6 +823,9 @@ def command_chat_connect(args: argparse.Namespace) -> int:
603
823
  "project": config.get("project") or "",
604
824
  "repository": config.get("repository") or "",
605
825
  "cli_storage_home_hash": sha256_text(str(home)),
826
+ "local_user_hash": identity["local_user_hash"],
827
+ "user_hash": identity["user_hash"],
828
+ "github_user": identity["github_user"],
606
829
  }
607
830
  }
608
831
  response = http_json("POST", f"{api_url(config)}/chat/connect", payload, token)
@@ -623,6 +846,10 @@ def command_run(args: argparse.Namespace) -> int:
623
846
  def command_watch(args: argparse.Namespace) -> int:
624
847
  home = cli_home()
625
848
  config = load_config(home)
849
+ if not has_verified_auth(config):
850
+ prompt_for_auth(home, config)
851
+ config = load_config(home)
852
+
626
853
  print("AI Memory watch mode. Type commands to run and capture. Type exit to stop.")
627
854
  while True:
628
855
  try:
@@ -648,6 +875,7 @@ def command_watch(args: argparse.Namespace) -> int:
648
875
  def command_history_import(args: argparse.Namespace) -> int:
649
876
  home = cli_home()
650
877
  config = load_config(home)
878
+ require_verified_auth(home, config)
651
879
  history_path = Path(args.path).expanduser() if args.path else detect_history_file()
652
880
  if not history_path or not history_path.exists():
653
881
  raise SystemExit("No shell history file found. Pass --path <history-file>.")
@@ -823,6 +1051,11 @@ def command_agent_start(args: argparse.Namespace) -> int:
823
1051
 
824
1052
  home = cli_home()
825
1053
  config = load_config(home)
1054
+ state = read_json(agent_state_path(home), {})
1055
+ if is_process_running(state.get("pid")):
1056
+ print(f"Agent already running: pid={state.get('pid')}")
1057
+ return 0
1058
+
826
1059
  result = subprocess.run(
827
1060
  ["schtasks", "/Run", "/TN", args.task_name],
828
1061
  capture_output=True,
@@ -925,7 +1158,9 @@ def command_status(_: argparse.Namespace) -> int:
925
1158
  print(f"Project: {config.get('project') or '-'}")
926
1159
  print(f"Repository: {config.get('repository') or '-'}")
927
1160
  print(f"Workspace: {config.get('workspace_path') or '.'}")
928
- print(f"Token: {'saved' if config.get('token') else 'missing'}")
1161
+ print(f"Token: {'verified' if has_verified_auth(config) else 'saved' if config.get('token') else 'missing'}")
1162
+ print(f"GitHub account: {config.get('github_user') or '-'}")
1163
+ print(f"User hash: {str(config.get('user_hash') or '-')[:24]}{'...' if config.get('user_hash') else ''}")
929
1164
  print(f"Events: {event_count} total, {outbox_count} queued, {sent_count} synced receipts")
930
1165
  return 0
931
1166
 
@@ -955,6 +1190,7 @@ def build_parser() -> argparse.ArgumentParser:
955
1190
  auth = subparsers.add_parser("auth", help="Save the app-issued CLI token.")
956
1191
  auth.add_argument("--token", required=True, help="Token generated by the website.")
957
1192
  auth.add_argument("--api-url", default=DEFAULT_API_URL, help="FastAPI base URL.")
1193
+ auth.add_argument("--no-agent", action="store_true", help="Do not auto-start the background sync agent after auth.")
958
1194
  auth.set_defaults(func=command_auth)
959
1195
 
960
1196
  init = subparsers.add_parser("init", help="Save project config and call /projects/init.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-memory-cli
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Python CLI for AI Memory terminal capture and offline sync.
5
5
  Author: AI Memory
6
6
  License-Expression: MIT
@@ -53,6 +53,13 @@ python -m ai_memory_cli workspace connect --path . --repo owner/repo --editor vs
53
53
  watch
54
54
  ```
55
55
 
56
+ The `auth` command verifies the website-issued token with FastAPI before it is saved locally. After verification,
57
+ the CLI stores a SHA-512 user hash for this computer, binds the token to that hash on the server, and starts the
58
+ background sync agent once on Windows.
59
+
60
+ If you run `watch` before auth, it will prompt for the website CLI token and FastAPI URL, then continue into
61
+ terminal capture after verification.
62
+
56
63
  `watch` is a shortcut for `python -m ai_memory_cli watch`. If Windows Device Guard blocks the generated launcher, keep using `python -m ai_memory_cli watch`.
57
64
  On Windows the shortcut is installed as `watch.cmd`; the Python Scripts folder must be on `PATH` for bare `watch` to resolve.
58
65
 
@@ -64,12 +71,13 @@ Inside `watch`, type the real command you want to capture, for example `python -
64
71
 
65
72
  ## Background agent
66
73
 
67
- The background agent starts at Windows logon and keeps syncing queued terminal hashes whenever the API is reachable:
74
+ After `auth`, the background agent starts once and is installed in the Windows Startup folder so queued terminal
75
+ hashes keep syncing whenever the API is reachable. Use these commands when you need manual control:
68
76
 
69
77
  ```powershell
70
- python -m ai_memory_cli agent install
71
- python -m ai_memory_cli agent start
72
78
  python -m ai_memory_cli agent status
79
+ python -m ai_memory_cli agent stop
80
+ python -m ai_memory_cli agent start
73
81
  ```
74
82
 
75
83
  The agent does not secretly capture every terminal on the computer. Commands are captured when they run through:
File without changes
File without changes