quickcall-integrations 0.1.8__py3-none-any.whl → 0.3.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.
- mcp_server/api_clients/github_client.py +366 -77
- mcp_server/auth/__init__.py +8 -0
- mcp_server/auth/credentials.py +350 -42
- mcp_server/tools/auth_tools.py +194 -27
- mcp_server/tools/git_tools.py +174 -0
- mcp_server/tools/github_tools.py +416 -45
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/METADATA +71 -61
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/RECORD +10 -10
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/entry_points.txt +0 -0
mcp_server/auth/credentials.py
CHANGED
|
@@ -3,13 +3,17 @@ Credential storage and management for QuickCall MCP.
|
|
|
3
3
|
|
|
4
4
|
Stores device tokens locally in ~/.quickcall/credentials.json
|
|
5
5
|
Fetches fresh API credentials from quickcall.dev on demand.
|
|
6
|
+
|
|
7
|
+
Also supports GitHub PAT fallback for users without GitHub App access:
|
|
8
|
+
- Environment variable: GITHUB_TOKEN or GITHUB_PAT
|
|
9
|
+
- Config file: .quickcall.env in project root or ~/.quickcall.env
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
import os
|
|
9
13
|
import json
|
|
10
14
|
import logging
|
|
11
15
|
from pathlib import Path
|
|
12
|
-
from typing import Optional, Dict, Any
|
|
16
|
+
from typing import Optional, Dict, Any, Tuple
|
|
13
17
|
from dataclasses import dataclass, asdict
|
|
14
18
|
from datetime import datetime
|
|
15
19
|
|
|
@@ -21,6 +25,9 @@ logger = logging.getLogger(__name__)
|
|
|
21
25
|
QUICKCALL_DIR = Path.home() / ".quickcall"
|
|
22
26
|
CREDENTIALS_FILE = QUICKCALL_DIR / "credentials.json"
|
|
23
27
|
|
|
28
|
+
# PAT config file names (searched in order)
|
|
29
|
+
PAT_CONFIG_FILENAMES = [".quickcall.env", "quickcall.env"]
|
|
30
|
+
|
|
24
31
|
# QuickCall API - configurable via environment for local testing
|
|
25
32
|
# Set QUICKCALL_API_URL=http://localhost:8000 for local development
|
|
26
33
|
QUICKCALL_API_URL = os.getenv("QUICKCALL_API_URL", "https://api.quickcall.dev")
|
|
@@ -50,6 +57,26 @@ class StoredCredentials:
|
|
|
50
57
|
)
|
|
51
58
|
|
|
52
59
|
|
|
60
|
+
@dataclass
|
|
61
|
+
class GitHubPATCredentials:
|
|
62
|
+
"""GitHub PAT credentials stored locally (independent of QuickCall)."""
|
|
63
|
+
|
|
64
|
+
token: str # ghp_xxx or github_pat_xxx
|
|
65
|
+
username: str
|
|
66
|
+
configured_at: str
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
return asdict(self)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Dict[str, Any]) -> "GitHubPATCredentials":
|
|
73
|
+
return cls(
|
|
74
|
+
token=data["token"],
|
|
75
|
+
username=data["username"],
|
|
76
|
+
configured_at=data["configured_at"],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
53
80
|
@dataclass
|
|
54
81
|
class APICredentials:
|
|
55
82
|
"""Fresh credentials fetched from QuickCall API."""
|
|
@@ -77,21 +104,26 @@ class CredentialStore:
|
|
|
77
104
|
"""
|
|
78
105
|
Manages credential storage and retrieval.
|
|
79
106
|
|
|
107
|
+
Supports two independent credential types:
|
|
108
|
+
1. QuickCall credentials (device token for API auth)
|
|
109
|
+
2. GitHub PAT credentials (for users without GitHub App access)
|
|
110
|
+
|
|
80
111
|
Usage:
|
|
81
112
|
store = CredentialStore()
|
|
82
113
|
|
|
83
|
-
#
|
|
114
|
+
# QuickCall auth
|
|
84
115
|
if store.is_authenticated():
|
|
85
116
|
creds = store.get_api_credentials()
|
|
86
|
-
if creds.github_connected:
|
|
87
|
-
# Use GitHub token
|
|
88
|
-
pass
|
|
89
117
|
|
|
90
|
-
#
|
|
91
|
-
store.
|
|
118
|
+
# GitHub PAT auth
|
|
119
|
+
pat_creds = store.get_github_pat_credentials()
|
|
120
|
+
if pat_creds:
|
|
121
|
+
# Use PAT
|
|
122
|
+
pass
|
|
92
123
|
|
|
93
|
-
#
|
|
94
|
-
store.
|
|
124
|
+
# Save credentials
|
|
125
|
+
store.save(StoredCredentials(...)) # QuickCall
|
|
126
|
+
store.save_github_pat(token="ghp_xxx", username="user") # PAT
|
|
95
127
|
"""
|
|
96
128
|
|
|
97
129
|
def __init__(self, api_url: Optional[str] = None):
|
|
@@ -103,6 +135,7 @@ class CredentialStore:
|
|
|
103
135
|
"""
|
|
104
136
|
self.api_url = api_url or QUICKCALL_API_URL
|
|
105
137
|
self._stored: Optional[StoredCredentials] = None
|
|
138
|
+
self._github_pat: Optional[GitHubPATCredentials] = None
|
|
106
139
|
self._api_creds: Optional[APICredentials] = None
|
|
107
140
|
self._load()
|
|
108
141
|
|
|
@@ -114,47 +147,127 @@ class CredentialStore:
|
|
|
114
147
|
try:
|
|
115
148
|
with open(CREDENTIALS_FILE) as f:
|
|
116
149
|
data = json.load(f)
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
|
|
151
|
+
# New format: separate keys for quickcall and github_pat
|
|
152
|
+
if "quickcall" in data:
|
|
153
|
+
self._stored = StoredCredentials.from_dict(data["quickcall"])
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"Loaded QuickCall credentials for user {self._stored.user_id}"
|
|
156
|
+
)
|
|
157
|
+
elif "device_token" in data:
|
|
158
|
+
# Legacy format: direct StoredCredentials
|
|
159
|
+
self._stored = StoredCredentials.from_dict(data)
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Loaded legacy credentials for user {self._stored.user_id}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Load GitHub PAT if present
|
|
165
|
+
if "github_pat" in data:
|
|
166
|
+
self._github_pat = GitHubPATCredentials.from_dict(
|
|
167
|
+
data["github_pat"]
|
|
168
|
+
)
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"Loaded GitHub PAT for user {self._github_pat.username}"
|
|
171
|
+
)
|
|
172
|
+
|
|
119
173
|
except Exception as e:
|
|
120
174
|
logger.warning(f"Failed to load credentials: {e}")
|
|
121
175
|
|
|
122
|
-
def
|
|
123
|
-
"""Save credentials to disk."""
|
|
176
|
+
def _save_to_file(self):
|
|
177
|
+
"""Save all credentials to disk."""
|
|
124
178
|
QUICKCALL_DIR.mkdir(parents=True, exist_ok=True)
|
|
125
179
|
|
|
180
|
+
data = {}
|
|
181
|
+
if self._stored:
|
|
182
|
+
data["quickcall"] = self._stored.to_dict()
|
|
183
|
+
if self._github_pat:
|
|
184
|
+
data["github_pat"] = self._github_pat.to_dict()
|
|
185
|
+
|
|
126
186
|
try:
|
|
127
187
|
with open(CREDENTIALS_FILE, "w") as f:
|
|
128
|
-
json.dump(
|
|
188
|
+
json.dump(data, f, indent=2)
|
|
129
189
|
CREDENTIALS_FILE.chmod(0o600) # Restrict permissions
|
|
130
|
-
|
|
131
|
-
self._api_creds = None # Clear cached API creds
|
|
132
|
-
logger.info(f"Saved credentials for user {credentials.user_id}")
|
|
190
|
+
logger.debug("Saved credentials to disk")
|
|
133
191
|
except Exception as e:
|
|
134
192
|
logger.error(f"Failed to save credentials: {e}")
|
|
135
193
|
raise
|
|
136
194
|
|
|
195
|
+
def save(self, credentials: StoredCredentials):
|
|
196
|
+
"""Save QuickCall credentials to disk."""
|
|
197
|
+
self._stored = credentials
|
|
198
|
+
self._api_creds = None # Clear cached API creds
|
|
199
|
+
self._save_to_file()
|
|
200
|
+
logger.info(f"Saved QuickCall credentials for user {credentials.user_id}")
|
|
201
|
+
|
|
137
202
|
def clear(self):
|
|
138
|
-
"""Clear stored credentials."""
|
|
203
|
+
"""Clear all stored credentials (QuickCall + PAT)."""
|
|
139
204
|
if CREDENTIALS_FILE.exists():
|
|
140
205
|
try:
|
|
141
206
|
CREDENTIALS_FILE.unlink()
|
|
142
|
-
logger.info("Cleared stored credentials")
|
|
207
|
+
logger.info("Cleared all stored credentials")
|
|
143
208
|
except Exception as e:
|
|
144
209
|
logger.error(f"Failed to clear credentials: {e}")
|
|
145
210
|
raise
|
|
146
211
|
|
|
147
212
|
self._stored = None
|
|
213
|
+
self._github_pat = None
|
|
148
214
|
self._api_creds = None
|
|
149
215
|
|
|
216
|
+
def clear_quickcall(self):
|
|
217
|
+
"""Clear only QuickCall credentials, keep PAT if configured."""
|
|
218
|
+
self._stored = None
|
|
219
|
+
self._api_creds = None
|
|
220
|
+
if self._github_pat:
|
|
221
|
+
self._save_to_file()
|
|
222
|
+
elif CREDENTIALS_FILE.exists():
|
|
223
|
+
CREDENTIALS_FILE.unlink()
|
|
224
|
+
logger.info("Cleared QuickCall credentials")
|
|
225
|
+
|
|
150
226
|
def is_authenticated(self) -> bool:
|
|
151
|
-
"""Check if we have
|
|
227
|
+
"""Check if we have QuickCall credentials."""
|
|
152
228
|
return self._stored is not None
|
|
153
229
|
|
|
154
230
|
def get_stored_credentials(self) -> Optional[StoredCredentials]:
|
|
155
|
-
"""Get locally stored credentials (device token, etc)."""
|
|
231
|
+
"""Get locally stored QuickCall credentials (device token, etc)."""
|
|
156
232
|
return self._stored
|
|
157
233
|
|
|
234
|
+
# ========================================================================
|
|
235
|
+
# GitHub PAT Methods
|
|
236
|
+
# ========================================================================
|
|
237
|
+
|
|
238
|
+
def save_github_pat(self, token: str, username: str):
|
|
239
|
+
"""
|
|
240
|
+
Save GitHub PAT credentials.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
token: GitHub Personal Access Token
|
|
244
|
+
username: GitHub username
|
|
245
|
+
"""
|
|
246
|
+
self._github_pat = GitHubPATCredentials(
|
|
247
|
+
token=token,
|
|
248
|
+
username=username,
|
|
249
|
+
configured_at=datetime.utcnow().isoformat() + "Z",
|
|
250
|
+
)
|
|
251
|
+
self._save_to_file()
|
|
252
|
+
logger.info(f"Saved GitHub PAT for user {username}")
|
|
253
|
+
|
|
254
|
+
def get_github_pat_credentials(self) -> Optional[GitHubPATCredentials]:
|
|
255
|
+
"""Get stored GitHub PAT credentials."""
|
|
256
|
+
return self._github_pat
|
|
257
|
+
|
|
258
|
+
def clear_github_pat(self):
|
|
259
|
+
"""Clear only GitHub PAT credentials, keep QuickCall if configured."""
|
|
260
|
+
self._github_pat = None
|
|
261
|
+
if self._stored:
|
|
262
|
+
self._save_to_file()
|
|
263
|
+
elif CREDENTIALS_FILE.exists():
|
|
264
|
+
CREDENTIALS_FILE.unlink()
|
|
265
|
+
logger.info("Cleared GitHub PAT credentials")
|
|
266
|
+
|
|
267
|
+
def has_github_pat(self) -> bool:
|
|
268
|
+
"""Check if GitHub PAT is configured."""
|
|
269
|
+
return self._github_pat is not None
|
|
270
|
+
|
|
158
271
|
def get_api_credentials(
|
|
159
272
|
self, force_refresh: bool = False
|
|
160
273
|
) -> Optional[APICredentials]:
|
|
@@ -223,32 +336,56 @@ class CredentialStore:
|
|
|
223
336
|
|
|
224
337
|
def get_status(self) -> Dict[str, Any]:
|
|
225
338
|
"""Get authentication status for diagnostics."""
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
"authenticated": False,
|
|
229
|
-
"credentials_file": str(CREDENTIALS_FILE),
|
|
230
|
-
"file_exists": CREDENTIALS_FILE.exists(),
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
# Always fetch fresh status (force refresh to get latest connection states)
|
|
234
|
-
api_creds = self.get_api_credentials(force_refresh=True)
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
"authenticated": True,
|
|
339
|
+
result = {
|
|
238
340
|
"credentials_file": str(CREDENTIALS_FILE),
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
341
|
+
"file_exists": CREDENTIALS_FILE.exists(),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# QuickCall status
|
|
345
|
+
if self._stored:
|
|
346
|
+
result["quickcall_authenticated"] = True
|
|
347
|
+
result["user_id"] = self._stored.user_id
|
|
348
|
+
result["email"] = self._stored.email
|
|
349
|
+
result["username"] = self._stored.username
|
|
350
|
+
result["authenticated_at"] = self._stored.authenticated_at
|
|
351
|
+
|
|
352
|
+
# Fetch fresh API credentials for integration status
|
|
353
|
+
api_creds = self.get_api_credentials(force_refresh=True)
|
|
354
|
+
result["github"] = {
|
|
244
355
|
"connected": api_creds.github_connected if api_creds else False,
|
|
356
|
+
"mode": "github_app"
|
|
357
|
+
if (api_creds and api_creds.github_connected)
|
|
358
|
+
else None,
|
|
245
359
|
"username": api_creds.github_username if api_creds else None,
|
|
246
|
-
}
|
|
247
|
-
"slack"
|
|
360
|
+
}
|
|
361
|
+
result["slack"] = {
|
|
248
362
|
"connected": api_creds.slack_connected if api_creds else False,
|
|
249
363
|
"team_name": api_creds.slack_team_name if api_creds else None,
|
|
250
|
-
}
|
|
251
|
-
|
|
364
|
+
}
|
|
365
|
+
else:
|
|
366
|
+
result["quickcall_authenticated"] = False
|
|
367
|
+
result["github"] = {"connected": False, "mode": None, "username": None}
|
|
368
|
+
result["slack"] = {"connected": False, "team_name": None}
|
|
369
|
+
|
|
370
|
+
# GitHub PAT status (independent of QuickCall)
|
|
371
|
+
if self._github_pat:
|
|
372
|
+
result["github_pat"] = {
|
|
373
|
+
"configured": True,
|
|
374
|
+
"username": self._github_pat.username,
|
|
375
|
+
"configured_at": self._github_pat.configured_at,
|
|
376
|
+
}
|
|
377
|
+
# If QuickCall GitHub not connected, PAT takes over
|
|
378
|
+
if not result["github"]["connected"]:
|
|
379
|
+
result["github"]["connected"] = True
|
|
380
|
+
result["github"]["mode"] = "pat"
|
|
381
|
+
result["github"]["username"] = self._github_pat.username
|
|
382
|
+
else:
|
|
383
|
+
result["github_pat"] = {"configured": False}
|
|
384
|
+
|
|
385
|
+
# Legacy compatibility
|
|
386
|
+
result["authenticated"] = result["quickcall_authenticated"]
|
|
387
|
+
|
|
388
|
+
return result
|
|
252
389
|
|
|
253
390
|
|
|
254
391
|
# Global credential store instance
|
|
@@ -276,3 +413,174 @@ def get_credentials() -> Optional[APICredentials]:
|
|
|
276
413
|
def clear_credentials():
|
|
277
414
|
"""Clear stored credentials (logout)."""
|
|
278
415
|
get_credential_store().clear()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ============================================================================
|
|
419
|
+
# GitHub PAT Fallback Support
|
|
420
|
+
# ============================================================================
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _parse_env_file(file_path: Path) -> Dict[str, str]:
|
|
424
|
+
"""
|
|
425
|
+
Parse a simple .env file into a dictionary.
|
|
426
|
+
|
|
427
|
+
Supports:
|
|
428
|
+
- KEY=value
|
|
429
|
+
- KEY="value"
|
|
430
|
+
- KEY='value'
|
|
431
|
+
- # comments
|
|
432
|
+
- Empty lines
|
|
433
|
+
"""
|
|
434
|
+
result = {}
|
|
435
|
+
if not file_path.exists():
|
|
436
|
+
return result
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
with open(file_path) as f:
|
|
440
|
+
for line in f:
|
|
441
|
+
line = line.strip()
|
|
442
|
+
# Skip empty lines and comments
|
|
443
|
+
if not line or line.startswith("#"):
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
# Parse KEY=value
|
|
447
|
+
if "=" in line:
|
|
448
|
+
key, _, value = line.partition("=")
|
|
449
|
+
key = key.strip()
|
|
450
|
+
value = value.strip()
|
|
451
|
+
|
|
452
|
+
# Remove surrounding quotes
|
|
453
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
454
|
+
value.startswith("'") and value.endswith("'")
|
|
455
|
+
):
|
|
456
|
+
value = value[1:-1]
|
|
457
|
+
|
|
458
|
+
result[key] = value
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logger.debug(f"Failed to parse {file_path}: {e}")
|
|
461
|
+
|
|
462
|
+
return result
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _find_project_root() -> Optional[Path]:
|
|
466
|
+
"""
|
|
467
|
+
Find the project root by looking for common markers.
|
|
468
|
+
|
|
469
|
+
Walks up from cwd looking for .git, pyproject.toml, package.json, etc.
|
|
470
|
+
Returns None if no project root is found.
|
|
471
|
+
"""
|
|
472
|
+
markers = [".git", "pyproject.toml", "package.json", "Cargo.toml", "go.mod"]
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
current = Path.cwd().resolve()
|
|
476
|
+
except Exception:
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
# Walk up the directory tree
|
|
480
|
+
while current != current.parent:
|
|
481
|
+
for marker in markers:
|
|
482
|
+
if (current / marker).exists():
|
|
483
|
+
return current
|
|
484
|
+
current = current.parent
|
|
485
|
+
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def get_github_pat() -> Tuple[Optional[str], Optional[str]]:
|
|
490
|
+
"""
|
|
491
|
+
Get GitHub Personal Access Token from various sources.
|
|
492
|
+
|
|
493
|
+
Search order (first found wins):
|
|
494
|
+
1. Stored credentials file (via connect_github_via_pat command)
|
|
495
|
+
2. Environment variable: GITHUB_TOKEN
|
|
496
|
+
3. Environment variable: GITHUB_PAT
|
|
497
|
+
4. Project root: .quickcall.env or quickcall.env
|
|
498
|
+
5. Home directory: ~/.quickcall.env or ~/quickcall.env
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Tuple of (token, source) where source describes where the token was found.
|
|
502
|
+
Returns (None, None) if no PAT is configured.
|
|
503
|
+
"""
|
|
504
|
+
# 1. Check stored credentials (from connect_github_via_pat command)
|
|
505
|
+
store = get_credential_store()
|
|
506
|
+
pat_creds = store.get_github_pat_credentials()
|
|
507
|
+
if pat_creds:
|
|
508
|
+
logger.debug("Found GitHub PAT in stored credentials")
|
|
509
|
+
return (pat_creds.token, "credentials file")
|
|
510
|
+
|
|
511
|
+
# 2. Check environment variables
|
|
512
|
+
for env_var in ["GITHUB_TOKEN", "GITHUB_PAT"]:
|
|
513
|
+
token = os.environ.get(env_var)
|
|
514
|
+
if token:
|
|
515
|
+
logger.debug(f"Found GitHub PAT in environment variable {env_var}")
|
|
516
|
+
return (token, f"environment variable {env_var}")
|
|
517
|
+
|
|
518
|
+
# 3. Check project root config files
|
|
519
|
+
project_root = _find_project_root()
|
|
520
|
+
if project_root:
|
|
521
|
+
for filename in PAT_CONFIG_FILENAMES:
|
|
522
|
+
config_path = project_root / filename
|
|
523
|
+
if config_path.exists():
|
|
524
|
+
env_vars = _parse_env_file(config_path)
|
|
525
|
+
for key in ["GITHUB_TOKEN", "GITHUB_PAT"]:
|
|
526
|
+
if key in env_vars:
|
|
527
|
+
logger.debug(f"Found GitHub PAT in {config_path}")
|
|
528
|
+
return (env_vars[key], f"{config_path}")
|
|
529
|
+
|
|
530
|
+
# 4. Check home directory config files
|
|
531
|
+
home = Path.home()
|
|
532
|
+
for filename in PAT_CONFIG_FILENAMES:
|
|
533
|
+
config_path = home / filename
|
|
534
|
+
if config_path.exists():
|
|
535
|
+
env_vars = _parse_env_file(config_path)
|
|
536
|
+
for key in ["GITHUB_TOKEN", "GITHUB_PAT"]:
|
|
537
|
+
if key in env_vars:
|
|
538
|
+
logger.debug(f"Found GitHub PAT in {config_path}")
|
|
539
|
+
return (env_vars[key], f"{config_path}")
|
|
540
|
+
|
|
541
|
+
return (None, None)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def get_github_pat_username() -> Optional[str]:
|
|
545
|
+
"""
|
|
546
|
+
Get the GitHub username for PAT authentication.
|
|
547
|
+
|
|
548
|
+
Checks:
|
|
549
|
+
1. Stored credentials (from connect_github_via_pat command)
|
|
550
|
+
2. Environment variable: GITHUB_USERNAME
|
|
551
|
+
3. Config files (same search order as get_github_pat)
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
GitHub username or None if not configured.
|
|
555
|
+
"""
|
|
556
|
+
# Check stored credentials first
|
|
557
|
+
store = get_credential_store()
|
|
558
|
+
pat_creds = store.get_github_pat_credentials()
|
|
559
|
+
if pat_creds:
|
|
560
|
+
return pat_creds.username
|
|
561
|
+
|
|
562
|
+
# Check environment variable
|
|
563
|
+
username = os.environ.get("GITHUB_USERNAME")
|
|
564
|
+
if username:
|
|
565
|
+
return username
|
|
566
|
+
|
|
567
|
+
# Check project root config files
|
|
568
|
+
project_root = _find_project_root()
|
|
569
|
+
if project_root:
|
|
570
|
+
for filename in PAT_CONFIG_FILENAMES:
|
|
571
|
+
config_path = project_root / filename
|
|
572
|
+
if config_path.exists():
|
|
573
|
+
env_vars = _parse_env_file(config_path)
|
|
574
|
+
if "GITHUB_USERNAME" in env_vars:
|
|
575
|
+
return env_vars["GITHUB_USERNAME"]
|
|
576
|
+
|
|
577
|
+
# Check home directory config files
|
|
578
|
+
home = Path.home()
|
|
579
|
+
for filename in PAT_CONFIG_FILENAMES:
|
|
580
|
+
config_path = home / filename
|
|
581
|
+
if config_path.exists():
|
|
582
|
+
env_vars = _parse_env_file(config_path)
|
|
583
|
+
if "GITHUB_USERNAME" in env_vars:
|
|
584
|
+
return env_vars["GITHUB_USERNAME"]
|
|
585
|
+
|
|
586
|
+
return None
|