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.
- {ai_memory_cli-0.1.5/src/ai_memory_cli.egg-info → ai_memory_cli-0.1.7}/PKG-INFO +12 -4
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/README.md +11 -3
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/pyproject.toml +1 -1
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/src/ai_memory_cli/__init__.py +1 -1
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/src/ai_memory_cli/cli.py +256 -20
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7/src/ai_memory_cli.egg-info}/PKG-INFO +12 -4
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/LICENSE +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/bin/watch.cmd +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/setup.cfg +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/src/ai_memory_cli/__main__.py +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/src/ai_memory_cli.egg-info/SOURCES.txt +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/src/ai_memory_cli.egg-info/dependency_links.txt +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/src/ai_memory_cli.egg-info/entry_points.txt +0 -0
- {ai_memory_cli-0.1.5 → ai_memory_cli-0.1.7}/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.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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
509
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|