ai-memory-cli 0.1.5__tar.gz → 0.1.6__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.5/src/ai_memory_cli.egg-info → ai_memory_cli-0.1.6}/PKG-INFO +9 -4
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/README.md +8 -3
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/pyproject.toml +1 -1
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/src/ai_memory_cli/__init__.py +1 -1
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/src/ai_memory_cli/cli.py +215 -17
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6/src/ai_memory_cli.egg-info}/PKG-INFO +9 -4
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/LICENSE +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/bin/watch.cmd +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/setup.cfg +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/src/ai_memory_cli/__main__.py +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/src/ai_memory_cli.egg-info/SOURCES.txt +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/src/ai_memory_cli.egg-info/dependency_links.txt +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/src/ai_memory_cli.egg-info/entry_points.txt +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.6}/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.6
|
|
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,10 @@ 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
|
+
|
|
56
60
|
`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
61
|
On Windows the shortcut is installed as `watch.cmd`; the Python Scripts folder must be on `PATH` for bare `watch` to resolve.
|
|
58
62
|
|
|
@@ -64,12 +68,13 @@ Inside `watch`, type the real command you want to capture, for example `python -
|
|
|
64
68
|
|
|
65
69
|
## Background agent
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
After `auth`, the background agent starts once and is installed in the Windows Startup folder so queued terminal
|
|
72
|
+
hashes keep syncing whenever the API is reachable. Use these commands when you need manual control:
|
|
68
73
|
|
|
69
74
|
```powershell
|
|
70
|
-
python -m ai_memory_cli agent install
|
|
71
|
-
python -m ai_memory_cli agent start
|
|
72
75
|
python -m ai_memory_cli agent status
|
|
76
|
+
python -m ai_memory_cli agent stop
|
|
77
|
+
python -m ai_memory_cli agent start
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
The agent does not secretly capture every terminal on the computer. Commands are captured when they run through:
|
|
@@ -31,6 +31,10 @@ 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
|
+
|
|
34
38
|
`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
39
|
On Windows the shortcut is installed as `watch.cmd`; the Python Scripts folder must be on `PATH` for bare `watch` to resolve.
|
|
36
40
|
|
|
@@ -42,12 +46,13 @@ Inside `watch`, type the real command you want to capture, for example `python -
|
|
|
42
46
|
|
|
43
47
|
## Background agent
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
After `auth`, the background agent starts once and is installed in the Windows Startup folder so queued terminal
|
|
50
|
+
hashes keep syncing whenever the API is reachable. Use these commands when you need manual control:
|
|
46
51
|
|
|
47
52
|
```powershell
|
|
48
|
-
python -m ai_memory_cli agent install
|
|
49
|
-
python -m ai_memory_cli agent start
|
|
50
53
|
python -m ai_memory_cli agent status
|
|
54
|
+
python -m ai_memory_cli agent stop
|
|
55
|
+
python -m ai_memory_cli agent start
|
|
51
56
|
```
|
|
52
57
|
|
|
53
58
|
The agent does not secretly capture every terminal on the computer. Commands are captured when they run through:
|
|
@@ -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,46 @@ 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
|
+
|
|
129
177
|
def agent_state_path(home: Path) -> Path:
|
|
130
178
|
return home / "agent.json"
|
|
131
179
|
|
|
@@ -200,6 +248,63 @@ def start_detached_agent(interval: int, limit: int) -> int:
|
|
|
200
248
|
return int(process.pid)
|
|
201
249
|
|
|
202
250
|
|
|
251
|
+
def is_process_running(pid: Any) -> bool:
|
|
252
|
+
try:
|
|
253
|
+
pid_int = int(pid)
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
return False
|
|
256
|
+
if pid_int <= 0:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
if os.name == "nt":
|
|
260
|
+
result = subprocess.run(
|
|
261
|
+
["tasklist", "/FI", f"PID eq {pid_int}", "/FO", "CSV", "/NH"],
|
|
262
|
+
capture_output=True,
|
|
263
|
+
text=True,
|
|
264
|
+
encoding="utf-8",
|
|
265
|
+
errors="replace",
|
|
266
|
+
)
|
|
267
|
+
return result.returncode == 0 and str(pid_int) in result.stdout
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
os.kill(pid_int, 0)
|
|
271
|
+
return True
|
|
272
|
+
except OSError:
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def ensure_agent_started_once(home: Path, config: dict[str, Any]) -> None:
|
|
277
|
+
agent_config = config.get("agent") if isinstance(config.get("agent"), dict) else {}
|
|
278
|
+
interval = int(agent_config.get("interval_seconds") or DEFAULT_AGENT_INTERVAL_SECONDS)
|
|
279
|
+
limit = int(agent_config.get("limit") or 50)
|
|
280
|
+
state = read_json(agent_state_path(home), {})
|
|
281
|
+
|
|
282
|
+
if is_process_running(state.get("pid")):
|
|
283
|
+
print(f"AI Memory sync agent already running: pid={state.get('pid')}")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
if os.name == "nt":
|
|
287
|
+
script_path = write_windows_startup_script(interval, limit)
|
|
288
|
+
pid = start_detached_agent(interval, limit)
|
|
289
|
+
config["agent"] = {
|
|
290
|
+
**agent_config,
|
|
291
|
+
"method": "startup",
|
|
292
|
+
"interval_seconds": interval,
|
|
293
|
+
"limit": limit,
|
|
294
|
+
"startup_script": str(script_path),
|
|
295
|
+
"last_started_pid": pid,
|
|
296
|
+
"last_started_at": utc_now(),
|
|
297
|
+
}
|
|
298
|
+
save_config(home, config)
|
|
299
|
+
append_log(home, f"auth started detached agent pid={pid}")
|
|
300
|
+
print(f"AI Memory sync agent started once: pid={pid}")
|
|
301
|
+
print(f"Startup sync installed at: {script_path}")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
print("Automatic startup agent install is only implemented for Windows.")
|
|
305
|
+
print("Start sync manually with: python -m ai_memory_cli agent run")
|
|
306
|
+
|
|
307
|
+
|
|
203
308
|
def normalize_command(command: str) -> str:
|
|
204
309
|
return " ".join(command.strip().split())
|
|
205
310
|
|
|
@@ -283,6 +388,58 @@ def http_json(
|
|
|
283
388
|
return json.loads(response_body)
|
|
284
389
|
|
|
285
390
|
|
|
391
|
+
def describe_http_error(exc: urllib.error.HTTPError) -> str:
|
|
392
|
+
try:
|
|
393
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
394
|
+
payload = json.loads(body) if body else {}
|
|
395
|
+
detail = payload.get("detail") if isinstance(payload, dict) else None
|
|
396
|
+
if detail:
|
|
397
|
+
return str(detail)
|
|
398
|
+
if body:
|
|
399
|
+
return body[:500]
|
|
400
|
+
except Exception:
|
|
401
|
+
pass
|
|
402
|
+
return f"HTTP {exc.code} {exc.reason}"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def verify_cli_auth(home: Path, config: dict[str, Any], token: str) -> dict[str, Any]:
|
|
406
|
+
identity = client_identity(home, config)
|
|
407
|
+
response = http_json(
|
|
408
|
+
"POST",
|
|
409
|
+
f"{api_url(config)}/cli/auth/verify",
|
|
410
|
+
{"client": identity},
|
|
411
|
+
token,
|
|
412
|
+
)
|
|
413
|
+
if not response.get("verified"):
|
|
414
|
+
raise RuntimeError("CLI token was not verified by the backend.")
|
|
415
|
+
|
|
416
|
+
config["token"] = token
|
|
417
|
+
config["token_hash"] = sha256_text(token)
|
|
418
|
+
config["token_tail"] = response.get("token_tail") or ""
|
|
419
|
+
config["session_id"] = response.get("session_id") or ""
|
|
420
|
+
config["github_user"] = response.get("github_user") or response.get("github_account_name") or ""
|
|
421
|
+
config["user_hash"] = response.get("user_hash") or ""
|
|
422
|
+
config["bound_local_user_hash"] = response.get("bound_local_user_hash") or identity["local_user_hash"]
|
|
423
|
+
config["auth_verified_at"] = response.get("verified_at") or utc_now()
|
|
424
|
+
config["server_account_storage_dir"] = response.get("account_storage_dir") or ""
|
|
425
|
+
return response
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def require_verified_auth(home: Path, config: dict[str, Any]) -> str:
|
|
429
|
+
token = require_token(config)
|
|
430
|
+
if config.get("auth_verified_at") and config.get("user_hash") and config.get("local_user_hash"):
|
|
431
|
+
return token
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
verify_cli_auth(home, config, token)
|
|
435
|
+
save_config(home, config)
|
|
436
|
+
return token
|
|
437
|
+
except urllib.error.HTTPError as exc:
|
|
438
|
+
raise SystemExit(f"CLI auth is not verified: {describe_http_error(exc)}") from exc
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
raise SystemExit(f"CLI auth is not verified. Run website auth again when the local server is available: {exc}") from exc
|
|
441
|
+
|
|
442
|
+
|
|
286
443
|
def make_terminal_event(
|
|
287
444
|
command: str,
|
|
288
445
|
stdout: str,
|
|
@@ -388,6 +545,14 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
|
|
|
388
545
|
if not quiet:
|
|
389
546
|
print("No CLI token saved. Events remain queued until python -m ai_memory_cli auth is configured.")
|
|
390
547
|
return 0
|
|
548
|
+
if not (config.get("auth_verified_at") and config.get("user_hash")):
|
|
549
|
+
try:
|
|
550
|
+
verify_cli_auth(home, config, token)
|
|
551
|
+
save_config(home, config)
|
|
552
|
+
except Exception as exc:
|
|
553
|
+
if not quiet:
|
|
554
|
+
print(f"CLI auth is not verified yet. Events remain queued: {exc}")
|
|
555
|
+
return 0
|
|
391
556
|
paths = sorted((home / "outbox").glob("*.json"))[:limit]
|
|
392
557
|
if not paths:
|
|
393
558
|
if not quiet:
|
|
@@ -401,12 +566,7 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
|
|
|
401
566
|
|
|
402
567
|
payload = {
|
|
403
568
|
"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
|
-
},
|
|
569
|
+
"client": client_identity(home, config),
|
|
410
570
|
}
|
|
411
571
|
response = http_json("POST", f"{api_url(config)}/cli/events/terminal", payload, token)
|
|
412
572
|
accepted = {item.get("event_hash") for item in response.get("events", []) if item.get("event_hash")}
|
|
@@ -421,6 +581,7 @@ def sync_events(home: Path, config: dict[str, Any], limit: int = 50, quiet: bool
|
|
|
421
581
|
|
|
422
582
|
|
|
423
583
|
def capture_command(home: Path, config: dict[str, Any], command: str, include_excluded: bool, source: str) -> int:
|
|
584
|
+
require_verified_auth(home, config)
|
|
424
585
|
workspace = Path(str(config.get("workspace_path") or ".")).expanduser()
|
|
425
586
|
cwd = workspace if workspace.exists() else Path.cwd()
|
|
426
587
|
|
|
@@ -498,17 +659,33 @@ def command_auth(args: argparse.Namespace) -> int:
|
|
|
498
659
|
config = load_config(home)
|
|
499
660
|
if args.api_url:
|
|
500
661
|
config["api_url"] = args.api_url.rstrip("/")
|
|
501
|
-
|
|
502
|
-
|
|
662
|
+
|
|
663
|
+
token = args.token.strip()
|
|
664
|
+
config["pending_token_hash"] = sha256_text(token)
|
|
665
|
+
try:
|
|
666
|
+
response = verify_cli_auth(home, config, token)
|
|
667
|
+
except urllib.error.HTTPError as exc:
|
|
668
|
+
save_config(home, config)
|
|
669
|
+
raise SystemExit(f"CLI auth failed: {describe_http_error(exc)}") from exc
|
|
670
|
+
except Exception as exc:
|
|
671
|
+
save_config(home, config)
|
|
672
|
+
raise SystemExit(f"CLI auth failed. Keep the local FastAPI server running and generate a fresh website token: {exc}") from exc
|
|
673
|
+
|
|
503
674
|
config["authed_at"] = utc_now()
|
|
504
675
|
save_config(home, config)
|
|
505
676
|
print(f"Saved CLI auth in {config_path(home)}")
|
|
677
|
+
print(f"GitHub account: {response.get('github_user') or config.get('github_user') or '-'}")
|
|
678
|
+
print(f"Local user hash: {str(config.get('user_hash') or '')[:24]}...")
|
|
679
|
+
if response.get("account_storage_dir"):
|
|
680
|
+
print(f"Server account storage: {response['account_storage_dir']}")
|
|
681
|
+
|
|
682
|
+
if not args.no_agent:
|
|
683
|
+
ensure_agent_started_once(home, config)
|
|
506
684
|
|
|
507
685
|
try:
|
|
508
|
-
|
|
509
|
-
print(f"Connected to API: {health.get('service', api_url(config))}")
|
|
686
|
+
sync_events(home, config, quiet=True)
|
|
510
687
|
except Exception as exc:
|
|
511
|
-
print(f"Auth saved.
|
|
688
|
+
print(f"Auth saved. Sync will retry later: {exc}", file=sys.stderr)
|
|
512
689
|
return 0
|
|
513
690
|
|
|
514
691
|
|
|
@@ -525,7 +702,7 @@ def command_init(args: argparse.Namespace) -> int:
|
|
|
525
702
|
config["workspace_path"] = args.workspace
|
|
526
703
|
save_config(home, config)
|
|
527
704
|
|
|
528
|
-
token =
|
|
705
|
+
token = require_verified_auth(home, config)
|
|
529
706
|
payload = {
|
|
530
707
|
"project": config.get("project") or "memory-project",
|
|
531
708
|
"repository": config.get("repository") or "",
|
|
@@ -538,7 +715,7 @@ def command_init(args: argparse.Namespace) -> int:
|
|
|
538
715
|
print(f"Initialized project: {project.get('id', payload['project'])}")
|
|
539
716
|
except Exception as exc:
|
|
540
717
|
print(f"Project config saved locally. Server init will need retry: {exc}", file=sys.stderr)
|
|
541
|
-
print("Start terminal capture with:
|
|
718
|
+
print("Start terminal capture with: watch")
|
|
542
719
|
return 0
|
|
543
720
|
|
|
544
721
|
|
|
@@ -551,7 +728,8 @@ def command_workspace_connect(args: argparse.Namespace) -> int:
|
|
|
551
728
|
config["repository"] = args.repo
|
|
552
729
|
save_config(home, config)
|
|
553
730
|
|
|
554
|
-
token =
|
|
731
|
+
token = require_verified_auth(home, config)
|
|
732
|
+
identity = client_identity(home, config)
|
|
555
733
|
payload = {
|
|
556
734
|
"payload": {
|
|
557
735
|
"source": "ai-memory-cli",
|
|
@@ -561,6 +739,9 @@ def command_workspace_connect(args: argparse.Namespace) -> int:
|
|
|
561
739
|
"editor": args.editor,
|
|
562
740
|
"package_manager": args.package_manager,
|
|
563
741
|
"cli_storage_home_hash": sha256_text(str(home)),
|
|
742
|
+
"local_user_hash": identity["local_user_hash"],
|
|
743
|
+
"user_hash": identity["user_hash"],
|
|
744
|
+
"github_user": identity["github_user"],
|
|
564
745
|
}
|
|
565
746
|
}
|
|
566
747
|
response = http_json("POST", f"{api_url(config)}/workspace/connect", payload, token)
|
|
@@ -572,7 +753,8 @@ def command_workspace_connect(args: argparse.Namespace) -> int:
|
|
|
572
753
|
def command_mcp_connect(args: argparse.Namespace) -> int:
|
|
573
754
|
home = cli_home()
|
|
574
755
|
config = load_config(home)
|
|
575
|
-
token =
|
|
756
|
+
token = require_verified_auth(home, config)
|
|
757
|
+
identity = client_identity(home, config)
|
|
576
758
|
config["mcp_server"] = args.server
|
|
577
759
|
save_config(home, config)
|
|
578
760
|
payload = {
|
|
@@ -582,6 +764,9 @@ def command_mcp_connect(args: argparse.Namespace) -> int:
|
|
|
582
764
|
"project": config.get("project") or "",
|
|
583
765
|
"repository": config.get("repository") or "",
|
|
584
766
|
"cli_storage_home_hash": sha256_text(str(home)),
|
|
767
|
+
"local_user_hash": identity["local_user_hash"],
|
|
768
|
+
"user_hash": identity["user_hash"],
|
|
769
|
+
"github_user": identity["github_user"],
|
|
585
770
|
}
|
|
586
771
|
}
|
|
587
772
|
response = http_json("POST", f"{api_url(config)}/mcp/connect", payload, token)
|
|
@@ -593,7 +778,8 @@ def command_mcp_connect(args: argparse.Namespace) -> int:
|
|
|
593
778
|
def command_chat_connect(args: argparse.Namespace) -> int:
|
|
594
779
|
home = cli_home()
|
|
595
780
|
config = load_config(home)
|
|
596
|
-
token =
|
|
781
|
+
token = require_verified_auth(home, config)
|
|
782
|
+
identity = client_identity(home, config)
|
|
597
783
|
config["chat_provider"] = args.provider
|
|
598
784
|
save_config(home, config)
|
|
599
785
|
payload = {
|
|
@@ -603,6 +789,9 @@ def command_chat_connect(args: argparse.Namespace) -> int:
|
|
|
603
789
|
"project": config.get("project") or "",
|
|
604
790
|
"repository": config.get("repository") or "",
|
|
605
791
|
"cli_storage_home_hash": sha256_text(str(home)),
|
|
792
|
+
"local_user_hash": identity["local_user_hash"],
|
|
793
|
+
"user_hash": identity["user_hash"],
|
|
794
|
+
"github_user": identity["github_user"],
|
|
606
795
|
}
|
|
607
796
|
}
|
|
608
797
|
response = http_json("POST", f"{api_url(config)}/chat/connect", payload, token)
|
|
@@ -648,6 +837,7 @@ def command_watch(args: argparse.Namespace) -> int:
|
|
|
648
837
|
def command_history_import(args: argparse.Namespace) -> int:
|
|
649
838
|
home = cli_home()
|
|
650
839
|
config = load_config(home)
|
|
840
|
+
require_verified_auth(home, config)
|
|
651
841
|
history_path = Path(args.path).expanduser() if args.path else detect_history_file()
|
|
652
842
|
if not history_path or not history_path.exists():
|
|
653
843
|
raise SystemExit("No shell history file found. Pass --path <history-file>.")
|
|
@@ -823,6 +1013,11 @@ def command_agent_start(args: argparse.Namespace) -> int:
|
|
|
823
1013
|
|
|
824
1014
|
home = cli_home()
|
|
825
1015
|
config = load_config(home)
|
|
1016
|
+
state = read_json(agent_state_path(home), {})
|
|
1017
|
+
if is_process_running(state.get("pid")):
|
|
1018
|
+
print(f"Agent already running: pid={state.get('pid')}")
|
|
1019
|
+
return 0
|
|
1020
|
+
|
|
826
1021
|
result = subprocess.run(
|
|
827
1022
|
["schtasks", "/Run", "/TN", args.task_name],
|
|
828
1023
|
capture_output=True,
|
|
@@ -925,7 +1120,9 @@ def command_status(_: argparse.Namespace) -> int:
|
|
|
925
1120
|
print(f"Project: {config.get('project') or '-'}")
|
|
926
1121
|
print(f"Repository: {config.get('repository') or '-'}")
|
|
927
1122
|
print(f"Workspace: {config.get('workspace_path') or '.'}")
|
|
928
|
-
print(f"Token: {'saved' if config.get('token') else 'missing'}")
|
|
1123
|
+
print(f"Token: {'verified' if config.get('auth_verified_at') and config.get('user_hash') else 'saved' if config.get('token') else 'missing'}")
|
|
1124
|
+
print(f"GitHub account: {config.get('github_user') or '-'}")
|
|
1125
|
+
print(f"User hash: {str(config.get('user_hash') or '-')[:24]}{'...' if config.get('user_hash') else ''}")
|
|
929
1126
|
print(f"Events: {event_count} total, {outbox_count} queued, {sent_count} synced receipts")
|
|
930
1127
|
return 0
|
|
931
1128
|
|
|
@@ -955,6 +1152,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
955
1152
|
auth = subparsers.add_parser("auth", help="Save the app-issued CLI token.")
|
|
956
1153
|
auth.add_argument("--token", required=True, help="Token generated by the website.")
|
|
957
1154
|
auth.add_argument("--api-url", default=DEFAULT_API_URL, help="FastAPI base URL.")
|
|
1155
|
+
auth.add_argument("--no-agent", action="store_true", help="Do not auto-start the background sync agent after auth.")
|
|
958
1156
|
auth.set_defaults(func=command_auth)
|
|
959
1157
|
|
|
960
1158
|
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.
|
|
3
|
+
Version: 0.1.6
|
|
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,10 @@ 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
|
+
|
|
56
60
|
`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
61
|
On Windows the shortcut is installed as `watch.cmd`; the Python Scripts folder must be on `PATH` for bare `watch` to resolve.
|
|
58
62
|
|
|
@@ -64,12 +68,13 @@ Inside `watch`, type the real command you want to capture, for example `python -
|
|
|
64
68
|
|
|
65
69
|
## Background agent
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
After `auth`, the background agent starts once and is installed in the Windows Startup folder so queued terminal
|
|
72
|
+
hashes keep syncing whenever the API is reachable. Use these commands when you need manual control:
|
|
68
73
|
|
|
69
74
|
```powershell
|
|
70
|
-
python -m ai_memory_cli agent install
|
|
71
|
-
python -m ai_memory_cli agent start
|
|
72
75
|
python -m ai_memory_cli agent status
|
|
76
|
+
python -m ai_memory_cli agent stop
|
|
77
|
+
python -m ai_memory_cli agent start
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
The agent does not secretly capture every terminal on the computer. Commands are captured when they run through:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|