sourcerykit 1.0.0b1__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.
Files changed (61) hide show
  1. sourcerykit/__init__.py +31 -0
  2. sourcerykit/bootstrap/__init__.py +3 -0
  3. sourcerykit/bootstrap/_cache.py +118 -0
  4. sourcerykit/bootstrap/bootstrap.py +46 -0
  5. sourcerykit/cli/__init__.py +0 -0
  6. sourcerykit/cli/config.py +136 -0
  7. sourcerykit/cli/doctor.py +184 -0
  8. sourcerykit/cli/endpoints.py +56 -0
  9. sourcerykit/cli/feedback.py +57 -0
  10. sourcerykit/cli/init.py +360 -0
  11. sourcerykit/cli/logo.py +29 -0
  12. sourcerykit/cli/main.py +50 -0
  13. sourcerykit/cli/trace.py +237 -0
  14. sourcerykit/cli/utils.py +160 -0
  15. sourcerykit/config.py +213 -0
  16. sourcerykit/db/__init__.py +1 -0
  17. sourcerykit/db/_engine.py +98 -0
  18. sourcerykit/db/_intercepts.py +75 -0
  19. sourcerykit/db/_schema.py +139 -0
  20. sourcerykit/db/_traces.py +140 -0
  21. sourcerykit/db/_trusted_endpoints.py +107 -0
  22. sourcerykit/errors/__init__.py +32 -0
  23. sourcerykit/evaluator/__init__.py +3 -0
  24. sourcerykit/evaluator/_eval_modes.py +151 -0
  25. sourcerykit/evaluator/evaluator.py +120 -0
  26. sourcerykit/handoff/__init__.py +5 -0
  27. sourcerykit/handoff/_guide.py +46 -0
  28. sourcerykit/handoff/_preprocess.py +21 -0
  29. sourcerykit/handoff/_query_records.py +43 -0
  30. sourcerykit/handoff/payload_builder.py +185 -0
  31. sourcerykit/intercept/__init__.py +8 -0
  32. sourcerykit/intercept/_aiohttp_hook.py +62 -0
  33. sourcerykit/intercept/_httpx_hook.py +86 -0
  34. sourcerykit/intercept/_loader.py +44 -0
  35. sourcerykit/intercept/_self_egress.py +26 -0
  36. sourcerykit/intercept/_storage.py +59 -0
  37. sourcerykit/intercept/interceptor.py +85 -0
  38. sourcerykit/intercept/requests_hook.py +97 -0
  39. sourcerykit/logger.py +24 -0
  40. sourcerykit/provably/__init__.py +3 -0
  41. sourcerykit/provably/_answer_model.py +79 -0
  42. sourcerykit/provably/_api.py +369 -0
  43. sourcerykit/provably/_auth_api.py +157 -0
  44. sourcerykit/provably/_errors.py +150 -0
  45. sourcerykit/provably/_http.py +170 -0
  46. sourcerykit/provably/auth_service.py +122 -0
  47. sourcerykit/provably/service.py +617 -0
  48. sourcerykit/schemas/__init__.py +6 -0
  49. sourcerykit/schemas/agent_response.py +28 -0
  50. sourcerykit/schemas/handoff.py +109 -0
  51. sourcerykit/schemas/outcome.py +9 -0
  52. sourcerykit/schemas/verification_mode.py +8 -0
  53. sourcerykit/trusted_endpoints/__init__.py +11 -0
  54. sourcerykit/trusted_endpoints/service.py +187 -0
  55. sourcerykit/utils/__init__.py +5 -0
  56. sourcerykit/utils/validation.py +20 -0
  57. sourcerykit-1.0.0b1.dist-info/METADATA +228 -0
  58. sourcerykit-1.0.0b1.dist-info/RECORD +61 -0
  59. sourcerykit-1.0.0b1.dist-info/WHEEL +4 -0
  60. sourcerykit-1.0.0b1.dist-info/entry_points.txt +2 -0
  61. sourcerykit-1.0.0b1.dist-info/licenses/LICENSE.md +101 -0
@@ -0,0 +1,31 @@
1
+ from sourcerykit.bootstrap import bootstrap_system
2
+ from sourcerykit.errors import (
3
+ SourceryKitBootstrapError,
4
+ SourceryKitConfigError,
5
+ SourceryKitError,
6
+ SourceryKitStorageError,
7
+ SourceryKitTrustError,
8
+ )
9
+ from sourcerykit.evaluator import evaluate_handoff
10
+ from sourcerykit.handoff import build_handoff_payload
11
+ from sourcerykit.intercept import async_intercept_context, take_last_intercept_row_id
12
+ from sourcerykit.schemas import SourceryKitAgentResponse, VerificationMode
13
+ from sourcerykit.trusted_endpoints import insert_trusted_endpoint
14
+
15
+ __all__ = [
16
+ "bootstrap_system",
17
+ "evaluate_handoff",
18
+ "build_handoff_payload",
19
+ "async_intercept_context",
20
+ "take_last_intercept_row_id",
21
+ "insert_trusted_endpoint",
22
+ # Types
23
+ "VerificationMode",
24
+ "SourceryKitAgentResponse",
25
+ # Exceptions
26
+ "SourceryKitError",
27
+ "SourceryKitConfigError",
28
+ "SourceryKitBootstrapError",
29
+ "SourceryKitStorageError",
30
+ "SourceryKitTrustError",
31
+ ]
@@ -0,0 +1,3 @@
1
+ from sourcerykit.bootstrap.bootstrap import bootstrap_system
2
+
3
+ __all__ = ["bootstrap_system"]
@@ -0,0 +1,118 @@
1
+ """Provably bootstrap: middleware + database + collection + integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from uuid import UUID
7
+
8
+ from sourcerykit.config import Settings
9
+ from sourcerykit.db._engine import ConnectionInfo, get_connection_info
10
+ from sourcerykit.errors import SourceryKitBootstrapError, SourceryKitError
11
+ from sourcerykit.logger import get_logger
12
+ from sourcerykit.provably._errors import ProvablyError
13
+ from sourcerykit.provably.service import service
14
+
15
+ _log = get_logger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class ProvablyBootstrapCache:
20
+ """Holds the resolved Provably resource IDs cached for the process lifetime."""
21
+
22
+ middleware_id: UUID | None = field(default=None)
23
+ database_id: UUID | None = field(default=None)
24
+ schema_id: UUID | None = field(default=None)
25
+ table_id: UUID | None = field(default=None)
26
+ collection_id: UUID | None = field(default=None)
27
+ integration_key: str | None = field(default=None)
28
+ collection_name: str | None = field(default=None)
29
+
30
+ def load_from(self, settings: Settings) -> None:
31
+ """Populate cache from pre-resolved settings."""
32
+ self.middleware_id = settings.middleware_id
33
+ self.database_id = settings.database_id
34
+ self.schema_id = settings.schema_id
35
+ self.table_id = settings.table_id
36
+ self.collection_id = settings.collection_id
37
+ self.integration_key = settings.integration_key
38
+ self.collection_name = settings.project_name or None
39
+
40
+ async def run_handshake(self, project_name: str) -> None:
41
+ """Resolve or create all required Provably resources."""
42
+ try:
43
+ self.middleware_id = await self._resolve_middleware()
44
+
45
+ connection_info = get_connection_info()
46
+ self.database_id = await self._resolve_database(connection_info)
47
+
48
+ ids = await service.get_database_schema_id_and_table_id(self.middleware_id, connection_info)
49
+ self.schema_id = ids["schema_id"]
50
+ self.table_id = ids["table_id"]
51
+
52
+ self.collection_name = project_name
53
+ self.collection_id = await self._resolve_collection(project_name)
54
+ self.integration_key = await self._resolve_integration_key()
55
+ except SourceryKitError:
56
+ raise
57
+ except Exception as e:
58
+ _log.error("handshake_failed_unexpected", error=str(e))
59
+ raise SourceryKitBootstrapError("Unexpected error during Provably handshake") from e
60
+
61
+ async def _resolve_middleware(self) -> UUID:
62
+ try:
63
+ return await service.get_middleware_id()
64
+ except ProvablyError:
65
+ _log.info("middleware_not_found_creating_new")
66
+ try:
67
+ return await service.create_middleware()
68
+ except Exception as e:
69
+ _log.error("middleware_creation_failed", error=str(e))
70
+ raise
71
+
72
+ async def _resolve_database(self, database: ConnectionInfo) -> UUID:
73
+ if self.middleware_id is None:
74
+ raise SourceryKitBootstrapError("middleware_id is not set; run_handshake() must be called first")
75
+ try:
76
+ return await service.get_database_id(self.middleware_id, database)
77
+ except ProvablyError:
78
+ _log.info("database_not_found_creating_new")
79
+ try:
80
+ return await service.create_database(self.middleware_id, database)
81
+ except Exception as e:
82
+ _log.error("database_creation_failed", error=str(e))
83
+ raise
84
+
85
+ async def _resolve_collection(self, project_name: str) -> UUID:
86
+ if self.middleware_id is None or self.database_id is None or self.schema_id is None or self.table_id is None:
87
+ raise SourceryKitBootstrapError(
88
+ "middleware_id, database_id, schema_id, and table_id must all be resolved before creating a collection"
89
+ )
90
+ try:
91
+ return await service.get_collection_id(name=project_name)
92
+ except (ProvablyError, ValueError):
93
+ _log.info("collection_not_found_creating_new")
94
+ try:
95
+ columns = await service.get_columns_from_database(
96
+ self.middleware_id, self.database_id, self.schema_id, self.table_id
97
+ )
98
+ return await service.create_collection(
99
+ self.middleware_id,
100
+ self.database_id,
101
+ self.schema_id,
102
+ self.table_id,
103
+ columns,
104
+ name=project_name,
105
+ )
106
+ except Exception as e:
107
+ _log.error("collection_creation_failed", error=str(e))
108
+ raise
109
+
110
+ async def _resolve_integration_key(self) -> str:
111
+ if self.collection_id is None:
112
+ raise SourceryKitBootstrapError("collection_id is not set; _resolve_collection() must succeed first")
113
+ _, key = await service.create_integration(self.collection_id)
114
+ return key
115
+
116
+
117
+ # Module-level singleton.
118
+ _BOOTSTRAP_INSTANCE = ProvablyBootstrapCache()
@@ -0,0 +1,46 @@
1
+ from sourcerykit.bootstrap._cache import _BOOTSTRAP_INSTANCE, ProvablyBootstrapCache
2
+ from sourcerykit.config import get_settings
3
+ from sourcerykit.db._engine import get_engine
4
+ from sourcerykit.db._schema import ensure_schema
5
+ from sourcerykit.errors import (
6
+ SourceryKitConfigError,
7
+ SourceryKitStorageError,
8
+ )
9
+ from sourcerykit.intercept.interceptor import init_interceptor
10
+ from sourcerykit.logger import get_logger
11
+
12
+ _log = get_logger(__name__)
13
+
14
+
15
+ async def bootstrap_system() -> None:
16
+ """System entry point called exactly once during container/server startup."""
17
+ _log.info("system_bootstrap_started")
18
+
19
+ # Validate configuration
20
+ settings = get_settings()
21
+
22
+ if not settings.postgres_url:
23
+ raise SourceryKitConfigError("SOURCERYKIT_POSTGRES_URL is required. Run 'sourcerykit init' first.")
24
+
25
+ # Initialize database schemas
26
+ try:
27
+ await ensure_schema(get_engine())
28
+ except Exception as e:
29
+ _log.error("bootstrap_db_schema_failed", error=str(e))
30
+ raise SourceryKitStorageError("Failed to create database schema during bootstrap") from e
31
+
32
+ # Populate from cached settings or run handshake
33
+ if settings.has_bootstrap_ids:
34
+ _log.info("bootstrap_using_cached_ids")
35
+ _BOOTSTRAP_INSTANCE.load_from(settings)
36
+ else:
37
+ _log.info("bootstrap_running_handshake")
38
+ await _BOOTSTRAP_INSTANCE.run_handshake(project_name=settings.project_name)
39
+
40
+ init_interceptor()
41
+ _log.info("system_bootstrap_completed")
42
+
43
+
44
+ def get_bootstrap() -> ProvablyBootstrapCache:
45
+ """Synchronous gateway to access resolved IDs."""
46
+ return _BOOTSTRAP_INSTANCE
File without changes
@@ -0,0 +1,136 @@
1
+ import re
2
+
3
+ import questionary
4
+ import typer
5
+
6
+ from sourcerykit.cli.init import clear_auth_caches, create_db_tables, run_provably_handshake, save_bootstrap_ids
7
+ from sourcerykit.cli.utils import (
8
+ console,
9
+ mask_postgres_url,
10
+ mask_secret,
11
+ prompt_postgres_url_with_retry,
12
+ prompt_project_name,
13
+ require_settings,
14
+ )
15
+ from sourcerykit.config import get_settings, load_local_env, save_app_dir_config, save_local_env
16
+
17
+ config = typer.Typer(no_args_is_help=True)
18
+
19
+
20
+ @config.command()
21
+ def list(show_key: bool = typer.Option(False, "--show-key", help="show secrets in clear text")) -> None:
22
+ """Pretty print the active configuration (global + local)."""
23
+ settings = require_settings()
24
+
25
+ api_key_display = settings.api_key if show_key else mask_secret(settings.api_key)
26
+ console.print("\nšŸ“‹ [bold]Global Config[/bold] \n")
27
+ console.print(f"[cyan]PROVABLY_API_KEY[/cyan] = [yellow]'{api_key_display}'[/yellow]")
28
+
29
+ pg_display = settings.postgres_url if show_key else mask_postgres_url(settings.postgres_url)
30
+
31
+ console.print("\nšŸ“‹ [bold]Local Config[/bold] (.env)\n")
32
+ console.print(f"[cyan]SOURCERYKIT_POSTGRES_URL[/cyan] = [yellow]'{pg_display}'[/yellow]")
33
+ console.print(f"[cyan]SOURCERYKIT_PROJECT_NAME[/cyan] = [yellow]'{settings.project_name}'[/yellow]")
34
+
35
+ console.print()
36
+
37
+
38
+ @config.command()
39
+ def set() -> None:
40
+ """Interactively set or update configuration variables."""
41
+ console.print("\nāš™ļø [bold]SourceryKit Configuration Setup[/bold]\n")
42
+
43
+ choices = questionary.checkbox(
44
+ "Which configuration variables would you like to update?",
45
+ choices=[
46
+ questionary.Choice("PROVABLY_API_KEY (global)", checked=False),
47
+ questionary.Choice("SOURCERYKIT_POSTGRES_URL (local)", checked=False),
48
+ questionary.Choice("SOURCERYKIT_PROJECT_NAME (local)", checked=False),
49
+ ],
50
+ ).ask()
51
+
52
+ if not choices:
53
+ console.print("[yellow]No variables selected. Configuration unchanged.[/yellow]")
54
+ return
55
+
56
+ # --- Collect inputs ---
57
+
58
+ api_key = None
59
+ if "PROVABLY_API_KEY (global)" in choices:
60
+ api_key = questionary.password("Enter your PROVABLY_API_KEY:").ask()
61
+ if api_key is not None:
62
+ api_key = api_key.strip()
63
+ if not api_key:
64
+ console.print("[red]āŒ API key cannot be empty.[/red]")
65
+ api_key = None
66
+ elif not re.fullmatch(r"zk-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", api_key):
67
+ console.print("[red]āŒ API key must match format zk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx[/red]")
68
+ api_key = None
69
+
70
+ postgres_url = None
71
+ postgres_changed = False
72
+ if "SOURCERYKIT_POSTGRES_URL (local)" in choices:
73
+ local_env = load_local_env()
74
+ current_url = local_env.get("SOURCERYKIT_POSTGRES_URL", "")
75
+ result = prompt_postgres_url_with_retry()
76
+ if result and result != current_url:
77
+ postgres_url = result
78
+ postgres_changed = True
79
+ elif result == current_url:
80
+ console.print("[yellow]Same URL — no change.[/yellow]")
81
+
82
+ project_name = None
83
+ project_changed = False
84
+ if "SOURCERYKIT_PROJECT_NAME (local)" in choices:
85
+ local_env = load_local_env()
86
+ current_name = local_env.get("SOURCERYKIT_PROJECT_NAME", "")
87
+ result = prompt_project_name(current=current_name)
88
+ if result and result != current_name:
89
+ project_name = result
90
+ project_changed = True
91
+ elif result == current_name:
92
+ console.print("[yellow]Same name — no change.[/yellow]")
93
+
94
+ # --- Save config ---
95
+
96
+ if api_key:
97
+ save_app_dir_config(api_key=api_key)
98
+
99
+ local_updates: dict[str, str] = {}
100
+ if postgres_url:
101
+ local_updates["SOURCERYKIT_POSTGRES_URL"] = postgres_url
102
+ if project_name:
103
+ local_updates["SOURCERYKIT_PROJECT_NAME"] = project_name
104
+ if local_updates:
105
+ save_local_env(**local_updates)
106
+
107
+ # --- Re-bootstrap if needed ---
108
+
109
+ if postgres_changed or project_changed:
110
+ settings = get_settings()
111
+ name = settings.project_name
112
+
113
+ clear_auth_caches()
114
+
115
+ if postgres_changed:
116
+ console.print("\nšŸ”§ [bold]Re-bootstrapping (DB + Provably)...[/bold]")
117
+ try:
118
+ create_db_tables()
119
+ console.print(" āœ… Database tables created")
120
+ except Exception as e:
121
+ console.print(f" [red]āŒ DB tables failed: {e}[/red]")
122
+ return
123
+ else:
124
+ console.print("\nšŸ”§ [bold]Re-running Provably handshake (new collection name)...[/bold]")
125
+
126
+ try:
127
+ run_provably_handshake(name)
128
+ console.print(" āœ… Provably handshake completed")
129
+ except Exception as e:
130
+ console.print(f" [red]āŒ Handshake failed: {e}[/red]")
131
+ return
132
+
133
+ save_bootstrap_ids()
134
+ console.print(" āœ… Bootstrap IDs saved\n")
135
+
136
+ console.print("[bold green]✨ Configuration updated.[/bold green]")
@@ -0,0 +1,184 @@
1
+ """Doctor command — validate configuration and connectivity."""
2
+
3
+ import asyncio
4
+ import dataclasses
5
+ from collections.abc import Callable
6
+
7
+ from sourcerykit.cli.init import run_full_bootstrap
8
+ from sourcerykit.cli.utils import console, run_connectivity_check
9
+ from sourcerykit.config import Settings, get_settings
10
+ from sourcerykit.db._engine import get_connection_info
11
+ from sourcerykit.provably._errors import ProvablyConnectionError, ProvablyUnauthorizedError
12
+ from sourcerykit.provably.auth_service import auth_service
13
+ from sourcerykit.provably.service import service
14
+
15
+
16
+ def _check_api_key_and_org(settings: Settings) -> tuple[bool, str]:
17
+ """Validate API key and org_id in one call (list_organizations uses API key)."""
18
+ if not settings.api_key:
19
+ return False, "PROVABLY_API_KEY is missing — run 'sourcerykit init'"
20
+
21
+ try:
22
+ orgs = asyncio.run(auth_service.list_organizations())
23
+ except ProvablyUnauthorizedError:
24
+ return False, "API key is invalid or expired — run 'sourcerykit init'"
25
+ except ProvablyConnectionError:
26
+ return False, "Cannot reach Provably API (network error)"
27
+ except Exception as e:
28
+ return False, f"API key check failed: {e}"
29
+
30
+ org_ids = [str(o.get("id", "")) for o in orgs]
31
+ if str(settings.org_id) not in org_ids:
32
+ return False, f"Org ID {settings.org_id} not found — run 'sourcerykit init'"
33
+
34
+ return True, f"API key valid, org found ({len(orgs)} org(s))"
35
+
36
+
37
+ def _check_postgres(settings: Settings) -> tuple[bool, str]:
38
+ """Validate postgres_url connectivity."""
39
+ if not settings.postgres_url:
40
+ return False, "SOURCERYKIT_POSTGRES_URL is missing — run 'sourcerykit init'"
41
+
42
+ if run_connectivity_check(settings.postgres_url, quiet=True):
43
+ return True, "PostgreSQL connection successful"
44
+ return False, "PostgreSQL connection failed — check your SOURCERYKIT_POSTGRES_URL"
45
+
46
+
47
+ def _check_project_name(settings: Settings) -> tuple[bool, str]:
48
+ """Validate project_name is set."""
49
+ if settings.project_name:
50
+ return True, f"'{settings.project_name}'"
51
+ return False, "SOURCERYKIT_PROJECT_NAME is missing — run 'sourcerykit init'"
52
+
53
+
54
+ def _check_bootstrap_ids(settings: Settings) -> tuple[bool, str]:
55
+ """Validate all bootstrap resource IDs are present."""
56
+ if settings.has_bootstrap_ids:
57
+ return True, "All bootstrap IDs present"
58
+
59
+ missing = [
60
+ f.name
61
+ for f in dataclasses.fields(settings)
62
+ if (f.name.endswith("_id") or f.name == "integration_key") and not getattr(settings, f.name)
63
+ ]
64
+ return False, f"Missing: {', '.join(missing)} — run 'sourcerykit doctor --fix'"
65
+
66
+
67
+ # --- Deep checks — verify IDs exist in Provably backend ---
68
+ async def _deep_check_collection_and_ids(settings: Settings) -> tuple[bool, str]:
69
+ """Verify Provably resources match local config using the same calls as bootstrap."""
70
+ if not settings.has_bootstrap_ids or settings.middleware_id is None:
71
+ return False, "Bootstrap IDs missing — run 'sourcerykit doctor --fix'"
72
+
73
+ remote_mw = await service.get_middleware_id()
74
+ if remote_mw != settings.middleware_id:
75
+ return False, f"Middleware mismatch (local={settings.middleware_id}, remote={remote_mw})"
76
+
77
+ connection_info = get_connection_info()
78
+ remote_db = await service.get_database_id(settings.middleware_id, connection_info)
79
+ if remote_db != settings.database_id:
80
+ return False, f"Database mismatch (local={settings.database_id}, remote={remote_db})"
81
+
82
+ ids = await service.get_database_schema_id_and_table_id(settings.middleware_id, connection_info)
83
+ if ids["schema_id"] != settings.schema_id:
84
+ return False, f"Schema mismatch (local={settings.schema_id}, remote={ids['schema_id']})"
85
+ if ids["table_id"] != settings.table_id:
86
+ return False, f"Table mismatch (local={settings.table_id}, remote={ids['table_id']})"
87
+
88
+ remote_col = await service.get_collection_id(settings.project_name)
89
+ if remote_col != settings.collection_id:
90
+ return False, f"Collection mismatch (local={settings.collection_id}, remote={remote_col})"
91
+
92
+ return True, f"Collection '{settings.project_name}' verified (middleware, db, schema, table, collection)"
93
+
94
+
95
+ async def _deep_check_integration(settings: Settings) -> tuple[bool, str]:
96
+ """Verify integration_key has the expected format (API key, not UUID)."""
97
+ if not settings.integration_key:
98
+ return False, "SOURCERYKIT_INTEGRATION_KEY is missing — run 'sourcerykit doctor --fix'"
99
+
100
+ if not settings.integration_key.startswith("i-"):
101
+ return False, f"SOURCERYKIT_INTEGRATION_KEY has unexpected format: {settings.integration_key!r}"
102
+
103
+ return True, "Integration key format valid"
104
+
105
+
106
+ def _run_deep_check_collection_and_ids(settings: Settings) -> tuple[bool, str]:
107
+ try:
108
+ return asyncio.run(_deep_check_collection_and_ids(settings))
109
+ except ProvablyUnauthorizedError:
110
+ return False, "API key expired — run 'sourcerykit init'"
111
+ except ProvablyConnectionError:
112
+ return False, "Cannot reach Provably API (network error)"
113
+ except Exception as e:
114
+ return False, f"Collection check failed: {e}"
115
+
116
+
117
+ def _run_deep_check_integration(settings: Settings) -> tuple[bool, str]:
118
+ try:
119
+ return asyncio.run(_deep_check_integration(settings))
120
+ except ProvablyUnauthorizedError:
121
+ return False, "API key expired — run 'sourcerykit init'"
122
+ except ProvablyConnectionError:
123
+ return False, "Cannot reach Provably API (network error)"
124
+ except Exception as e:
125
+ return False, f"Integration check failed: {e}"
126
+
127
+
128
+ def run_doctor(fix: bool = False) -> None:
129
+ """Validate global config, local config, and connectivity."""
130
+ console.print("\n🩺 [bold]SourceryKit Doctor[/bold]\n")
131
+
132
+ try:
133
+ settings = get_settings()
134
+ except Exception as e:
135
+ console.print(f"[red]āŒ Cannot load settings: {e}[/red]")
136
+ console.print("\n[bold yellow]Run 'sourcerykit init' to configure[/bold yellow]\n")
137
+ return
138
+
139
+ checks: list[tuple[str, Callable[[], tuple[bool, str]]]] = [
140
+ ("API key + org", lambda: _check_api_key_and_org(settings)),
141
+ ("PostgreSQL", lambda: _check_postgres(settings)),
142
+ ("Project name", lambda: _check_project_name(settings)),
143
+ ("Bootstrap IDs", lambda: _check_bootstrap_ids(settings)),
144
+ ("Collection + IDs", lambda: _run_deep_check_collection_and_ids(settings)),
145
+ ("Integration", lambda: _run_deep_check_integration(settings)),
146
+ ]
147
+
148
+ passed = 0
149
+ total = len(checks)
150
+ failed_indices: list[int] = []
151
+
152
+ for i, (label, check_fn) in enumerate(checks):
153
+ ok, detail = check_fn()
154
+ icon = "[green]āœ…[/green]" if ok else "[red]āŒ[/red]"
155
+ console.print(f" {icon} {label}: {detail}")
156
+ if ok:
157
+ passed += 1
158
+ else:
159
+ failed_indices.append(i)
160
+
161
+ # --fix: attempt to create missing bootstrap IDs
162
+ if fix and failed_indices:
163
+ console.print("\n šŸ”§ [bold]Running bootstrap handshake...[/bold]", end=" ")
164
+ try:
165
+ if run_full_bootstrap(settings.project_name):
166
+ console.print("[green]DONE āœ…[/green]")
167
+ get_settings.cache_clear()
168
+ settings = get_settings()
169
+ for i in failed_indices:
170
+ label, check_fn = checks[i]
171
+ ok, detail = check_fn()
172
+ icon = "[green]āœ…[/green]" if ok else "[red]āŒ[/red]"
173
+ console.print(f" {icon} {label}: {detail}")
174
+ if ok:
175
+ passed += 1
176
+ else:
177
+ console.print("[red]FAILED āŒ[/red]")
178
+ except Exception as e:
179
+ console.print(f"[red]FAILED āŒ[/red]\n {e}")
180
+
181
+ if passed == total:
182
+ console.print(f"\n[bold green]All {total} checks passed![/bold green]\n")
183
+ else:
184
+ console.print(f"\n[bold yellow]{passed}/{total} checks passed — run 'sourcerykit init' to fix[/bold yellow]\n")
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+
3
+ import questionary
4
+ import typer
5
+ from rich.table import Table
6
+
7
+ from sourcerykit.cli.utils import console, require_settings
8
+ from sourcerykit.errors import SourceryKitTrustError
9
+ from sourcerykit.trusted_endpoints import service
10
+ from sourcerykit.trusted_endpoints.service import sanitize_and_extract_trusted_url
11
+
12
+ endpoints = typer.Typer(no_args_is_help=True)
13
+
14
+
15
+ @endpoints.command()
16
+ def add(url: str, label: str | None = typer.Option(None, "--label", "-l", help="optional display label")) -> None:
17
+ require_settings()
18
+ try:
19
+ sanitize_and_extract_trusted_url(url)
20
+ except SourceryKitTrustError as e:
21
+ console.print(f"[red]āŒ {e}[/red]")
22
+ raise typer.Exit(code=1)
23
+ asyncio.run(service.insert_trusted_endpoint(url=url, display_label=label))
24
+ console.print(f"[bold green]āœ… Endpoint added:[/bold green] {url}" + (f" ({label})" if label else ""))
25
+
26
+
27
+ @endpoints.command()
28
+ def list() -> None:
29
+ require_settings()
30
+ rows = asyncio.run(service.list_all_trusted_endpoints_detailed())
31
+
32
+ if not rows:
33
+ console.print("[yellow]No trusted endpoints found.[/yellow]")
34
+ return
35
+
36
+ table = Table(title="Trusted Endpoints")
37
+ table.add_column("URL", style="cyan")
38
+ table.add_column("Label", style="white")
39
+ table.add_column("Policy", style="dim")
40
+ table.add_column("Created by", style="dim")
41
+
42
+ for ep in rows:
43
+ table.add_row(ep["url"], ep["label"], ep["policy_version"], ep["created_by"])
44
+
45
+ console.print(table)
46
+
47
+
48
+ @endpoints.command()
49
+ def remove(url: str) -> None:
50
+ require_settings()
51
+ confirm = questionary.confirm(f"Remove endpoint '{url}'?", default=False).ask()
52
+ if not confirm:
53
+ console.print("[yellow]Cancelled.[/yellow]")
54
+ return
55
+ asyncio.run(service.remove_trusted_endpoint(url=url))
56
+ console.print(f"[bold green]āœ… Endpoint removed:[/bold green] {url}")
@@ -0,0 +1,57 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ import questionary
5
+ import typer
6
+
7
+ from sourcerykit.cli.utils import console, require_settings
8
+ from sourcerykit.provably.service import ProvablyService
9
+
10
+ service = ProvablyService()
11
+
12
+
13
+ def send_feedback() -> None:
14
+ """Submit interactive feedback or bug reports."""
15
+ require_settings()
16
+ console.print("\nšŸ“£ [bold]Send Feedback to SourceryKit[/bold]\n")
17
+
18
+ # read description
19
+ description = questionary.text("Please provide a description of your feedback/issue:", multiline=True).ask()
20
+
21
+ if not description or not description.strip():
22
+ console.print("[yellow]āš ļø Feedback description cannot be empty. Aborting.[/yellow]")
23
+ raise typer.Exit(code=1)
24
+
25
+ # read file
26
+ attach_file = questionary.confirm(
27
+ "Would you like to attach a file (e.g., a log or screenshot)?", default=False
28
+ ).ask()
29
+
30
+ file_bytes = b""
31
+ if attach_file:
32
+ file_path_str = questionary.text("Enter the path to the file you want to attach:").ask()
33
+
34
+ if file_path_str:
35
+ path = Path(file_path_str.strip()).expanduser().resolve()
36
+
37
+ if not path.exists() or not path.is_file():
38
+ console.print(f"[red]āŒ Error: File not found at '{path}'[/red]")
39
+ raise typer.Exit(code=1)
40
+
41
+ try:
42
+ # Read the file directly into raw bytes
43
+ file_bytes = path.read_bytes()
44
+ console.print(f"[green]āœ“ Attached '{path.name}' ({len(file_bytes)} bytes)[/green]")
45
+ except PermissionError:
46
+ console.print(f"[red]āŒ Permission denied when reading '{path}'[/red]")
47
+ raise typer.Exit(code=1)
48
+
49
+ console.print("\nšŸš€ Sending feedback...")
50
+
51
+ # send feedback
52
+ try:
53
+ asyncio.run(service.create_feedback(description.strip(), file_bytes))
54
+ console.print("\n✨ [bold green]Success![/bold green] Thank you for your feedback.")
55
+ except Exception as e:
56
+ console.print(f"\n[red]āŒ Failed to submit feedback: {e}[/red]")
57
+ raise typer.Exit(code=1)