-
-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathinit.py
More file actions
327 lines (277 loc) · 12 KB
/
Copy pathinit.py
File metadata and controls
327 lines (277 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""engraphis-init - one command from `pip install` to a configured, agent-connected setup.
Closes the biggest first-run gap: with no configuration, an installed build puts its
database in the platform user-data directory, where most people never think to look.
This command writes the process-selected trusted config file with an explicit absolute
DB path (and optional API token), then prints exact MCP snippets to paste into Claude
Code / Cursor / Cline / Zed.
engraphis-init # write ~/.engraphis/config.env
engraphis-init --db ~/mem.db # choose the database location
engraphis-init --token # also generate a bearer token for the HTTP APIs
engraphis-init --encrypted # require SQLCipher and provision a private DB key file
engraphis-init --force # overwrite the trusted config file
engraphis-init --check # doctor: verify install, extras, DB writability
Non-interactive by design (no prompts): safe in scripts, CI, and agent shells.
"""
from __future__ import annotations
import argparse
import json
import secrets
import sqlite3
import sys
from pathlib import Path
from typing import Any, Optional
from engraphis.backends.encrypted_db import connector_from_env
from engraphis.private_state import (
atomic_private_text,
ensure_owner_private_dir,
read_private_text,
)
_HEX64 = set("0123456789abcdef")
def _ok(label: str, detail: str = "") -> None:
print(f" [ok] {label}" + (f" - {detail}" if detail else ""))
def _miss(label: str, detail: str = "") -> None:
print(f" [--] {label}" + (f" - {detail}" if detail else ""))
def _fail(label: str, detail: str = "") -> None:
print(f" [FAIL] {label}" + (f" - {detail}" if detail else ""))
def _try_import(name: str):
try:
return __import__(name)
except Exception:
return None
def cmd_check() -> int:
"""Doctor: report what's installed and whether the configured DB is usable."""
failures = 0
print(f"engraphis doctor - python {sys.version.split()[0]}")
if _try_import("numpy") is None:
_fail("numpy (required core)", "pip install numpy")
failures += 1
else:
_ok("numpy (required core)")
for mod, label, hint in [
("mcp", "MCP server extra", 'pip install "engraphis[mcp]"'),
("fastapi", "REST/Inspector extra", 'pip install "engraphis[server]"'),
("sentence_transformers", "real embeddings",
"optional - deterministic offline embedder is the fallback"),
("tree_sitter", "AST code indexing",
"optional - regex code indexer is the fallback"),
]:
available = _try_import(mod) is not None
(_ok if available else _miss)(label, "" if available else hint)
from engraphis.config import settings
db = Path(settings.db_path).expanduser()
try:
db.parent.mkdir(parents=True, exist_ok=True)
connector = connector_from_env()
conn: Any = (
connector(str(db))
if connector is not None
else sqlite3.connect(str(db))
)
conn.execute("PRAGMA user_version")
conn.close()
_ok("database writable", str(db))
except Exception as exc:
_fail("database writable", f"{db}: {exc}")
failures += 1
_ok("local core", "single-user features available without a hosted subscription")
try:
from engraphis.cloud_session import configured
if configured(require_compute=False):
_ok("Engraphis Cloud", "installation connected")
else:
_miss("Engraphis Cloud", "not connected (optional for the local core)")
except Exception:
_miss("Engraphis Cloud", "saved session unavailable; reconnect if needed")
print("all good" if failures == 0 else f"{failures} problem(s) found")
return 0 if failures == 0 else 1
def _env_content(db_path: Path, token: str, key_path: Optional[Path] = None) -> str:
lines = [
"# Engraphis - generated by engraphis-init. Full reference: .env.example",
f"ENGRAPHIS_DB_PATH={db_path}",
]
if token:
lines += [
"# Bearer token required by the REST server & Inspector APIs:",
f"ENGRAPHIS_API_TOKEN={token}",
]
if key_path is not None:
lines += [
"# SQLCipher database key file, generated with owner-only permissions:",
f"ENGRAPHIS_DB_KEY_FILE={key_path}",
]
lines += [
"# Pro and Team are hosted. Connect through the Engraphis Cloud account portal;",
"# never paste access or refresh credentials into this configuration file.",
"# ENGRAPHIS_CLOUD_CONTROL_URL=https://control.example.com",
"# ENGRAPHIS_CLOUD_COMPUTE_URL=https://compute.example.com",
]
return "\n".join(lines) + "\n"
def _write_env(
path: Path,
content: str,
*,
owner_private_parent: bool = False,
) -> None:
"""Atomically replace one private configuration or key file."""
if owner_private_parent:
ensure_owner_private_dir(path.parent)
atomic_private_text(path, content)
def _key_path_for(db_path: Path) -> Path:
"""Return the private sidecar key location for a newly encrypted database."""
return db_path.with_name(f".{db_path.name}.key")
def _private_file_content(path: Path) -> str:
"""Read an existing generated key without printing its contents."""
try:
value = (read_private_text(path, max_bytes=128) or "").strip()
except OSError as exc:
raise RuntimeError(f"could not read database key file {path}: {exc}") from exc
if len(value) != 64 or any(character not in _HEX64 for character in value.casefold()):
raise RuntimeError(
f"database key file {path} must contain exactly 32 random bytes encoded as hex"
)
return value
def _provision_db_key(db_path: Path) -> Path:
"""Create or validate a sidecar SQLCipher key outside the trusted config.
An existing database without this key is intentionally rejected. Silently attaching a
fresh key would make an existing plaintext database inaccessible and could tempt a user
to overwrite it. SQLCipher conversion is a separate, deliberate migration operation.
"""
key_path = _key_path_for(db_path)
if key_path.exists():
_private_file_content(key_path)
return key_path
if db_path.exists():
raise RuntimeError(
"refusing to enable encryption for an existing database without its key file; "
"migrate the database to SQLCipher first or choose a new --db path"
)
_write_env(key_path, secrets.token_hex(32) + "\n")
return key_path
def _read_existing_env(env_file: Path) -> str:
"""Read one bounded, owner-only trusted config snapshot."""
return read_private_text(
env_file,
max_bytes=1024 * 1024,
owner_only=True,
) or ""
def _existing_env_value(content: str, name: str) -> str:
"""Read one simple assignment from trusted config content."""
for line in content.splitlines():
key, separator, value = line.partition("=")
if separator and key.strip() == name:
return value.strip().strip("\"'")
return ""
def _existing_db_path(env_file: Path, content: str, fallback: Path) -> Path:
"""Read the simple ENGRAPHIS_DB_PATH assignment emitted by this command."""
raw = _existing_env_value(content, "ENGRAPHIS_DB_PATH")
if raw:
configured = Path(raw).expanduser()
return (
configured
if configured.is_absolute()
else (env_file.parent / configured).resolve()
)
return fallback
def _trusted_env_file() -> Path:
"""Return the process-fixed private configuration path."""
from engraphis.config import trusted_env_path
return trusted_env_path()
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="engraphis-init", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--db", default="engraphis.db",
help="database file (default: ./engraphis.db)")
ap.add_argument("--token", action="store_true",
help="generate an ENGRAPHIS_API_TOKEN for the HTTP APIs")
encryption = ap.add_mutually_exclusive_group()
encryption.add_argument(
"--encrypted", action="store_true",
help="require SQLCipher and generate a private 32-byte database key file",
)
encryption.add_argument(
"--no-encryption", action="store_true",
help="do not enable SQLCipher even when its driver is installed",
)
ap.add_argument(
"--force",
action="store_true",
help="overwrite the existing trusted config file",
)
ap.add_argument("--check", action="store_true",
help="doctor mode: verify the installation without writing config")
args = ap.parse_args(argv)
if args.check:
return cmd_check()
db_path = Path(args.db).expanduser().resolve()
try:
env_file = _trusted_env_file()
except (OSError, RuntimeError, ValueError) as exc:
_fail("trusted configuration", str(exc))
return 1
token = secrets.token_urlsafe(24) if args.token else ""
sqlcipher_available = _try_import("sqlcipher3") is not None
if args.encrypted and not sqlcipher_available:
_fail("SQLCipher encryption", 'install it with: pip install "engraphis[encryption]"')
return 1
use_encryption = (args.encrypted or sqlcipher_available) and not args.no_encryption
key_path: Optional[Path] = None
if env_file.exists() and not args.force:
try:
existing_env = _read_existing_env(env_file)
except OSError as exc:
_fail("trusted configuration", str(exc))
return 1
print(
f"trusted config already exists at {env_file} - kept "
"(use --force to overwrite)."
)
db_path = _existing_db_path(env_file, existing_env, db_path)
existing_key = _existing_env_value(existing_env, "ENGRAPHIS_DB_KEY_FILE")
if existing_key:
key_path = Path(existing_key).expanduser()
else:
if use_encryption:
try:
key_path = _provision_db_key(db_path)
except RuntimeError as exc:
_fail("SQLCipher encryption", str(exc))
return 1
try:
_write_env(
env_file,
_env_content(db_path, token, key_path),
owner_private_parent=True,
)
except OSError as exc:
_fail("trusted configuration", str(exc))
return 1
print(f"wrote {env_file}")
print(f" database -> {db_path}")
if key_path is not None:
print(f" encryption -> SQLCipher key file {key_path}")
elif not args.no_encryption:
_miss("SQLCipher encryption", 'not installed; use --encrypted after pip install "engraphis[encryption]"')
if token:
print(" api token -> generated (in trusted config; send as 'Authorization: Bearer ...')")
mcp_env = {"ENGRAPHIS_DB_PATH": str(db_path)}
if key_path is not None:
mcp_env["ENGRAPHIS_DB_KEY_FILE"] = str(key_path)
snippet = {"mcpServers": {"engraphis": {
"command": "engraphis-mcp",
"env": mcp_env,
}}}
print("\nConnect your agent - Claude Code:")
command = f' claude mcp add engraphis --env ENGRAPHIS_DB_PATH="{db_path}"'
if key_path is not None:
command += f' --env ENGRAPHIS_DB_KEY_FILE="{key_path}"'
print(command + " -- engraphis-mcp")
print("\nCursor / Cline / Zed / Windsurf (mcp config):")
print(json.dumps(snippet, indent=2))
print("\nNext steps:")
print(" engraphis-dashboard # product UI on http://127.0.0.1:8700")
print(" engraphis-init --check # verify the install")
print(" Free forever at the core - start the 3-day Pro trial or subscribe at "
"https://api.engraphis.com/account?plan=pro&interval=monthly#billing")
return 0
if __name__ == "__main__":
raise SystemExit(main())