cartograph-cli 0.1.0__py3-none-any.whl
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.
- cartograph/__init__.py +11 -0
- cartograph/__main__.py +5 -0
- cartograph/auth.py +196 -0
- cartograph/checkin.py +521 -0
- cartograph/cli.py +1359 -0
- cartograph/cloud.py +385 -0
- cartograph/dashboard.py +474 -0
- cartograph/dashboard_ui.py +1666 -0
- cartograph/engine.py +544 -0
- cartograph/inspector.py +125 -0
- cartograph/installer.py +210 -0
- cartograph/languages/__init__.py +2 -0
- cartograph/languages/base.py +168 -0
- cartograph/languages/javascript.py +248 -0
- cartograph/languages/nim.py +282 -0
- cartograph/languages/python.py +142 -0
- cartograph/languages/registry.py +41 -0
- cartograph/library_config.json +24 -0
- cartograph/scaffolding/__init__.py +119 -0
- cartograph/scaffolding/templates.py +186 -0
- cartograph/search/__init__.py +31 -0
- cartograph/search/base.py +20 -0
- cartograph/search/bm25.py +94 -0
- cartograph/search/filters.py +85 -0
- cartograph/search/hybrid.py +95 -0
- cartograph/search/ngram.py +83 -0
- cartograph/search/synonyms.json +48 -0
- cartograph/trust.py +54 -0
- cartograph/validation_stamp.py +115 -0
- cartograph/validator.py +265 -0
- cartograph_cli-0.1.0.dist-info/METADATA +125 -0
- cartograph_cli-0.1.0.dist-info/RECORD +35 -0
- cartograph_cli-0.1.0.dist-info/WHEEL +4 -0
- cartograph_cli-0.1.0.dist-info/entry_points.txt +2 -0
- cartograph_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
cartograph/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Cartograph - widget library manager for AI agents."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as _pkg_version
|
|
4
|
+
|
|
5
|
+
from .engine import Cartograph, LIBRARY_PATH
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = _pkg_version("cartograph-cli")
|
|
9
|
+
except Exception:
|
|
10
|
+
__version__ = "0.0.0"
|
|
11
|
+
__all__ = ["Cartograph", "LIBRARY_PATH", "__version__"]
|
cartograph/__main__.py
ADDED
cartograph/auth.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth credential storage for the Cartograph cloud registry.
|
|
3
|
+
|
|
4
|
+
Credentials are stored in the platform user-config directory with 0o600
|
|
5
|
+
permissions so only the current user can read them. Nothing in this module
|
|
6
|
+
makes network calls except the token refresh path.
|
|
7
|
+
|
|
8
|
+
Stored format:
|
|
9
|
+
{"id_token": "...", "refresh_token": "...", "signing_key": "..."}
|
|
10
|
+
|
|
11
|
+
Environment overrides (useful for CI/scripts):
|
|
12
|
+
CARTOGRAPH_TOKEN — skip file storage entirely (raw ID token)
|
|
13
|
+
CARTOGRAPH_REGISTRY_URL — override the default registry endpoint
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import stat
|
|
21
|
+
import time
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.parse
|
|
24
|
+
import urllib.request
|
|
25
|
+
|
|
26
|
+
from platformdirs import user_config_dir
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger("cartograph")
|
|
29
|
+
|
|
30
|
+
_DEFAULT_REGISTRY_URL = "https://cartograph-api-562154372671.us-central1.run.app"
|
|
31
|
+
_CREDENTIALS_FILE = os.path.join(user_config_dir("cartograph"), "credentials.json")
|
|
32
|
+
_GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
33
|
+
def _google_client_id() -> str:
|
|
34
|
+
env = os.environ.get("GOOGLE_CLIENT_ID", "")
|
|
35
|
+
if env:
|
|
36
|
+
return env
|
|
37
|
+
return _read_credentials().get("client_id", "")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _google_client_secret() -> str:
|
|
41
|
+
env = os.environ.get("GOOGLE_CLIENT_SECRET", "")
|
|
42
|
+
if env:
|
|
43
|
+
return env
|
|
44
|
+
return _read_credentials().get("client_secret", "")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_registry_url() -> str:
|
|
48
|
+
return os.environ.get("CARTOGRAPH_REGISTRY_URL", _DEFAULT_REGISTRY_URL)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _read_credentials() -> dict:
|
|
52
|
+
"""Read the credentials file, or return empty dict."""
|
|
53
|
+
try:
|
|
54
|
+
with open(_CREDENTIALS_FILE) as f:
|
|
55
|
+
return json.load(f)
|
|
56
|
+
except Exception:
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _decode_jwt_payload(token: str) -> dict:
|
|
61
|
+
"""Decode the payload of a JWT without verification (client-side only)."""
|
|
62
|
+
try:
|
|
63
|
+
parts = token.split(".")
|
|
64
|
+
if len(parts) != 3:
|
|
65
|
+
return {}
|
|
66
|
+
# Add padding
|
|
67
|
+
payload = parts[1]
|
|
68
|
+
payload += "=" * (4 - len(payload) % 4)
|
|
69
|
+
return json.loads(base64.urlsafe_b64decode(payload))
|
|
70
|
+
except Exception:
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_token_expired(token: str) -> bool:
|
|
75
|
+
"""Check if a JWT ID token is expired (with 5min buffer)."""
|
|
76
|
+
payload = _decode_jwt_payload(token)
|
|
77
|
+
exp = payload.get("exp")
|
|
78
|
+
if not exp:
|
|
79
|
+
return True
|
|
80
|
+
return time.time() > (exp - 300) # 5 minute buffer
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _refresh_id_token(refresh_token: str) -> str | None:
|
|
84
|
+
"""Use a refresh token to get a new ID token from Google.
|
|
85
|
+
|
|
86
|
+
Returns the new id_token, or None on failure.
|
|
87
|
+
"""
|
|
88
|
+
if not refresh_token:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Need client_id and client_secret for refresh
|
|
92
|
+
client_id = _google_client_id()
|
|
93
|
+
client_secret = _google_client_secret()
|
|
94
|
+
if not client_id or not client_secret:
|
|
95
|
+
log.debug("Cannot refresh token: GOOGLE_CLIENT_ID/SECRET not set")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
data = urllib.parse.urlencode({
|
|
99
|
+
"grant_type": "refresh_token",
|
|
100
|
+
"refresh_token": refresh_token,
|
|
101
|
+
"client_id": client_id,
|
|
102
|
+
"client_secret": client_secret,
|
|
103
|
+
}).encode()
|
|
104
|
+
|
|
105
|
+
req = urllib.request.Request(_GOOGLE_TOKEN_URL, data=data, method="POST")
|
|
106
|
+
try:
|
|
107
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
108
|
+
result = json.loads(resp.read())
|
|
109
|
+
new_id_token = result.get("id_token")
|
|
110
|
+
if new_id_token:
|
|
111
|
+
# Update stored credentials with new id_token
|
|
112
|
+
creds = _read_credentials()
|
|
113
|
+
creds["id_token"] = new_id_token
|
|
114
|
+
_write_credentials(creds)
|
|
115
|
+
log.debug("ID token refreshed successfully")
|
|
116
|
+
return new_id_token
|
|
117
|
+
except Exception as e:
|
|
118
|
+
log.debug("Token refresh failed: %s", e)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_token() -> str | None:
|
|
124
|
+
"""Return a valid ID token, or None if not authenticated.
|
|
125
|
+
|
|
126
|
+
Checks expiration and auto-refreshes if needed.
|
|
127
|
+
"""
|
|
128
|
+
env = os.environ.get("CARTOGRAPH_TOKEN")
|
|
129
|
+
if env:
|
|
130
|
+
return env
|
|
131
|
+
|
|
132
|
+
creds = _read_credentials()
|
|
133
|
+
id_token = creds.get("id_token")
|
|
134
|
+
if not id_token:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Check if expired and try to refresh
|
|
138
|
+
if _is_token_expired(id_token):
|
|
139
|
+
refresh_token = creds.get("refresh_token", "")
|
|
140
|
+
refreshed = _refresh_id_token(refresh_token)
|
|
141
|
+
if refreshed:
|
|
142
|
+
return refreshed
|
|
143
|
+
# Refresh failed — token is expired
|
|
144
|
+
log.debug("ID token expired and refresh failed")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
return id_token
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_signing_key() -> str | None:
|
|
151
|
+
"""Return the user's signing key from stored credentials."""
|
|
152
|
+
env = os.environ.get("CARTOGRAPH_SIGNING_KEY")
|
|
153
|
+
if env:
|
|
154
|
+
return env
|
|
155
|
+
creds = _read_credentials()
|
|
156
|
+
return creds.get("signing_key") or None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_authenticated() -> bool:
|
|
160
|
+
return get_token() is not None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _write_credentials(creds: dict) -> None:
|
|
164
|
+
"""Write credentials dict to disk with restricted permissions."""
|
|
165
|
+
os.makedirs(os.path.dirname(_CREDENTIALS_FILE), exist_ok=True)
|
|
166
|
+
with open(_CREDENTIALS_FILE, "w") as f:
|
|
167
|
+
json.dump(creds, f)
|
|
168
|
+
try:
|
|
169
|
+
os.chmod(_CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0o600
|
|
170
|
+
except OSError as e:
|
|
171
|
+
log.debug("Could not set credentials file permissions: %s", e)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def save_credentials(id_token: str, refresh_token: str, signing_key: str,
|
|
175
|
+
client_id: str = "", client_secret: str = "") -> None:
|
|
176
|
+
"""Persist OAuth credentials to disk."""
|
|
177
|
+
creds = {
|
|
178
|
+
"id_token": id_token,
|
|
179
|
+
"refresh_token": refresh_token,
|
|
180
|
+
"signing_key": signing_key,
|
|
181
|
+
}
|
|
182
|
+
if client_id:
|
|
183
|
+
creds["client_id"] = client_id
|
|
184
|
+
if client_secret:
|
|
185
|
+
creds["client_secret"] = client_secret
|
|
186
|
+
_write_credentials(creds)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def clear_token() -> None:
|
|
190
|
+
"""Remove stored credentials."""
|
|
191
|
+
try:
|
|
192
|
+
os.remove(_CREDENTIALS_FILE)
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
pass
|
|
195
|
+
except OSError as e:
|
|
196
|
+
log.debug("Could not remove credentials file: %s", e)
|