keld 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.
keld/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
keld/api/__init__.py ADDED
File without changes
keld/api/client.py ADDED
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import httpx
6
+
7
+ from ..errors import KeldError
8
+
9
+
10
+ @dataclass
11
+ class DeviceStart:
12
+ device_code: str
13
+ user_code: str
14
+ verification_url: str
15
+ interval: int
16
+ expires_in: int
17
+
18
+
19
+ @dataclass
20
+ class Onboarding:
21
+ endpoint: str
22
+ ingest_token: str
23
+ actor: str
24
+
25
+
26
+ class AtlasClient:
27
+ def __init__(self, base_url: str, token: str | None = None, transport=None) -> None:
28
+ self.base_url = base_url.rstrip("/")
29
+ self.token = token
30
+ self._transport = transport
31
+ self._http = httpx.Client(base_url=self.base_url, timeout=30.0, transport=transport)
32
+
33
+ def _post(self, path: str, **kwargs) -> httpx.Response:
34
+ try:
35
+ return self._http.post(path, **kwargs)
36
+ except httpx.HTTPError as exc:
37
+ raise KeldError(f"network error contacting Atlas: {exc}") from exc
38
+
39
+ def _check(self, resp: httpx.Response) -> None:
40
+ if resp.status_code >= 400:
41
+ raise KeldError(f"Atlas returned {resp.status_code}: {resp.text[:200]}")
42
+
43
+ def device_start(self) -> DeviceStart:
44
+ resp = self._post("/v1/cli/device/start")
45
+ self._check(resp)
46
+ return DeviceStart(**resp.json())
47
+
48
+ def device_poll(self, device_code: str) -> dict | None:
49
+ resp = self._post("/v1/cli/device/poll", json={"device_code": device_code})
50
+ if resp.status_code == 202:
51
+ return None
52
+ self._check(resp)
53
+ return resp.json()
54
+
55
+ def onboarding(self) -> Onboarding:
56
+ if not self.token:
57
+ raise KeldError("onboarding requires authentication")
58
+ try:
59
+ resp = self._http.get("/v1/cli/onboarding",
60
+ headers={"Authorization": f"Bearer {self.token}"})
61
+ except httpx.HTTPError as exc:
62
+ raise KeldError(f"network error contacting Atlas: {exc}") from exc
63
+ self._check(resp)
64
+ return Onboarding(**resp.json())
65
+
66
+ def fetch_hook(self, endpoint: str, ingest_token: str) -> bytes:
67
+ url = f"{endpoint.rstrip('/')}/v1/tool-context/hook.py"
68
+ try:
69
+ with httpx.Client(timeout=30.0, transport=self._transport) as client:
70
+ resp = client.get(url, params={"token": ingest_token})
71
+ except httpx.HTTPError as exc:
72
+ raise KeldError(f"network error downloading hook: {exc}") from exc
73
+ self._check(resp)
74
+ return resp.content
keld/auth/__init__.py ADDED
File without changes
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import webbrowser
5
+
6
+ from ..api.client import AtlasClient
7
+ from ..console import console
8
+ from ..errors import KeldError
9
+ from ..paths import api_base
10
+ from .store import AuthData, load_auth, save_auth
11
+
12
+
13
+ def login(client: AtlasClient, *, open_browser: bool = True,
14
+ sleep=time.sleep, opener=webbrowser.open) -> AuthData:
15
+ ds = client.device_start()
16
+ console.print(
17
+ f"To authorize this device, open:\n [bold]{ds.verification_url}[/]\n"
18
+ f"The code [bold]{ds.user_code}[/] is already filled in — "
19
+ f"confirm it matches, then approve."
20
+ )
21
+ if open_browser:
22
+ console.print("(Opening your browser…)")
23
+ opener(ds.verification_url)
24
+
25
+ waited = 0
26
+ while waited <= ds.expires_in:
27
+ result = client.device_poll(ds.device_code)
28
+ if result is not None:
29
+ auth = AuthData(access_token=result["access_token"],
30
+ principal=result["principal"], org=result["org"],
31
+ api_url=client.base_url)
32
+ save_auth(auth)
33
+ console.print(f"Logged in as [bold]{auth.principal}[/] (org: {auth.org})")
34
+ return auth
35
+ sleep(ds.interval)
36
+ waited += max(ds.interval, 1)
37
+ raise KeldError("login timed out; please run `keld login` again")
38
+
39
+
40
+ def require_auth(*, no_login: bool, open_browser: bool = True) -> AuthData:
41
+ existing = load_auth()
42
+ if existing is not None:
43
+ return existing
44
+ if no_login:
45
+ raise KeldError("not logged in (run `keld login`; --no-login was set)")
46
+ return login(AtlasClient(api_base()), open_browser=open_browser)
keld/auth/store.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import asdict, dataclass
6
+
7
+ from ..paths import auth_path, keld_home
8
+
9
+
10
+ @dataclass
11
+ class AuthData:
12
+ access_token: str
13
+ principal: str
14
+ org: str
15
+ api_url: str
16
+
17
+
18
+ def save_auth(auth: AuthData) -> None:
19
+ keld_home().mkdir(parents=True, exist_ok=True)
20
+ path = auth_path()
21
+ path.write_text(json.dumps(asdict(auth), indent=2) + "\n")
22
+ os.chmod(path, 0o600)
23
+
24
+
25
+ def load_auth() -> AuthData | None:
26
+ path = auth_path()
27
+ if not path.exists():
28
+ return None
29
+ return AuthData(**json.loads(path.read_text()))
30
+
31
+
32
+ def clear_auth() -> bool:
33
+ path = auth_path()
34
+ if path.exists():
35
+ path.unlink()
36
+ return True
37
+ return False
keld/cli.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from .commands import login as login_cmd
6
+ from .commands import setup as setup_cmd
7
+ from .commands import status as status_cmd
8
+ from .commands import uninstall as uninstall_cmd
9
+ from .console import console_err
10
+ from .errors import KeldError
11
+
12
+ app = typer.Typer(no_args_is_help=True, add_completion=False, help="Keld CLI")
13
+
14
+ # Top-level auth commands — shared across all product groups.
15
+ app.command()(login_cmd.login)
16
+ app.command()(login_cmd.logout)
17
+ app.command()(login_cmd.whoami)
18
+
19
+ # `keld signal <cmd>` — Keld Signal telemetry onboarding for local tools.
20
+ signal_app = typer.Typer(
21
+ no_args_is_help=True,
22
+ help="Set up Keld Signal telemetry for your local AI coding tools.",
23
+ )
24
+ signal_app.command()(setup_cmd.setup)
25
+ signal_app.command()(status_cmd.status)
26
+ signal_app.command("doctor")(status_cmd.doctor)
27
+ signal_app.command()(uninstall_cmd.uninstall)
28
+ app.add_typer(signal_app, name="signal")
29
+
30
+
31
+ def main() -> None:
32
+ try:
33
+ app()
34
+ except KeldError as exc:
35
+ console_err.print(f"[bold red]Error:[/] {exc}")
36
+ raise SystemExit(1)
File without changes
keld/commands/login.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from ..auth.device_flow import require_auth
6
+ from ..auth.store import clear_auth, load_auth
7
+ from ..config.manifest import Manifest
8
+ from ..console import console, fail
9
+ from ..paths import set_api_base_override
10
+
11
+
12
+ def login(
13
+ no_login: bool = typer.Option(False, "--no-login",
14
+ help="Fail instead of opening a browser."),
15
+ api_url: str = typer.Option(None, "--api-url", metavar="URL",
16
+ help="Target a different Keld API base URL "
17
+ "(e.g. http://localhost:8000) for local dev."),
18
+ ) -> None:
19
+ """Authenticate to Keld."""
20
+ if api_url:
21
+ set_api_base_override(api_url)
22
+ auth = require_auth(no_login=no_login)
23
+ console.print(f"Logged in as [bold]{auth.principal}[/] (org: {auth.org})")
24
+
25
+
26
+ def logout() -> None:
27
+ """Remove stored credentials."""
28
+ if clear_auth():
29
+ console.print("Logged out.")
30
+ else:
31
+ console.print("Not logged in.")
32
+
33
+
34
+ def whoami() -> None:
35
+ """Show the logged-in principal."""
36
+ auth = load_auth()
37
+ if auth is None:
38
+ fail("not logged in (run `keld login`)")
39
+ endpoint = Manifest.load().endpoint
40
+ suffix = f" · endpoint {endpoint}" if endpoint else ""
41
+ console.print(f"[bold]{auth.principal}[/] · org {auth.org} · {auth.api_url}{suffix}")
keld/commands/setup.py ADDED
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from .. import diffview
6
+ from ..api.client import AtlasClient, Onboarding
7
+ from ..auth.device_flow import require_auth
8
+ from ..config.manifest import Manifest, ToolManifest
9
+ from ..config.writer import backup_config, write_atomic
10
+ from ..console import console
11
+ from ..errors import KeldError
12
+ from ..hook import install_hook
13
+ from ..paths import api_base_override, set_api_base_override
14
+ from ..tools.base import Plan, SetupParams
15
+ from ..tools.registry import select_adapters
16
+
17
+
18
+ def _default_resolve_conflict(adapter, plan) -> str:
19
+ """Prompt for a conflicted tool. Returns "skip", "replace", or "abort"."""
20
+ choice = typer.prompt(
21
+ f"{adapter.display_name}: [s]kip this tool, [r]eplace the conflicting "
22
+ f"section, or [a]bort everything?",
23
+ default="s",
24
+ ).strip().lower()[:1]
25
+ return {"s": "skip", "r": "replace", "a": "abort"}.get(choice, "skip")
26
+
27
+
28
+ def _run_setup(adapters, params: SetupParams, client: AtlasClient, ob: Onboarding,
29
+ *, dry_run: bool, yes: bool, show_diff: bool = False,
30
+ confirm=typer.confirm, resolve_conflict=None) -> Manifest:
31
+ if resolve_conflict is None:
32
+ resolve_conflict = _default_resolve_conflict
33
+
34
+ approved = [] # list[tuple[adapter, Plan]]
35
+ for adapter in adapters:
36
+ path = adapter.config_path()
37
+ before = path.read_text() if path.exists() else None
38
+ console.rule(f"[bold]{adapter.display_name}[/] · {path}")
39
+ try:
40
+ plan = adapter.apply(before, params)
41
+ except KeldError as exc:
42
+ plan = Plan(name=adapter.name, config_path=path, after_text=before or "",
43
+ managed={}, summary=[], changed=False, conflict=str(exc))
44
+
45
+ if plan.conflict:
46
+ console.print(f" [yellow]conflict:[/] {plan.conflict}")
47
+ if dry_run:
48
+ console.print(" [dim](dry-run: would be skipped)[/]")
49
+ continue
50
+ if yes:
51
+ console.print(" [yellow]skipped[/] (--yes)")
52
+ continue
53
+ choice = resolve_conflict(adapter, plan)
54
+ if choice == "abort":
55
+ console.print("Aborted.")
56
+ raise typer.Exit(code=1)
57
+ if choice == "replace":
58
+ plan = adapter.apply(before, params, replace=True)
59
+ if plan.conflict:
60
+ console.print(f" [yellow]can't replace:[/] {plan.conflict}")
61
+ console.print(" [yellow]skipped[/]")
62
+ continue
63
+ diffview.render(before, plan.after_text, plan.config_path) # replace: always
64
+ for line in plan.summary:
65
+ console.print(f" [dim]{line}[/]")
66
+ approved.append((adapter, plan))
67
+ continue
68
+ console.print(" [yellow]skipped[/]")
69
+ continue
70
+
71
+ if not plan.changed:
72
+ console.print(" already configured — no changes")
73
+ continue
74
+
75
+ if show_diff:
76
+ diffview.render(before, plan.after_text, plan.config_path)
77
+ for line in plan.summary:
78
+ console.print(f" [dim]{line}[/]")
79
+ approved.append((adapter, plan))
80
+
81
+ console.print("\n[bold]Hook[/] · keld-context.py → ~/.keld")
82
+
83
+ if dry_run:
84
+ console.print("\n[dim]--dry-run: no changes written.[/]")
85
+ return Manifest.load()
86
+ if not approved:
87
+ console.print("\nNothing to apply.")
88
+ return Manifest.load()
89
+ if not yes and not confirm(f"Apply {len(approved)} change(s)?"):
90
+ console.print("Aborted.")
91
+ return Manifest.load()
92
+
93
+ manifest = Manifest(endpoint=ob.endpoint, actor=ob.actor)
94
+ manifest.hook = install_hook(client, ob)
95
+ for adapter, plan in approved:
96
+ backup = backup_config(plan.config_path, adapter.name)
97
+ if backup:
98
+ console.print(f" [dim]backed up {plan.config_path} → {backup}[/]")
99
+ write_atomic(plan.config_path, plan.after_text, backup=False)
100
+ manifest.tools[adapter.name] = ToolManifest(
101
+ name=adapter.name, config_path=str(plan.config_path),
102
+ managed=plan.managed, backup_path=str(backup) if backup else None)
103
+ console.print(f" [green]✓[/] {adapter.display_name}")
104
+
105
+ manifest.save()
106
+ console.print("\nSetup complete. Restart any running sessions to pick up the new config.")
107
+ return manifest
108
+
109
+
110
+ def setup(
111
+ tool: str = typer.Option("", "--tool", help="Comma-separated tools to target."),
112
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show changes without writing."),
113
+ diff: bool = typer.Option(False, "--diff", help="Show full unified diffs (default: summary only)."),
114
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
115
+ no_login: bool = typer.Option(False, "--no-login", help="Fail instead of opening a browser."),
116
+ api_url: str = typer.Option(None, "--api-url", metavar="URL",
117
+ help="Target a different Keld API base URL "
118
+ "(e.g. http://localhost:8000) for local dev."),
119
+ ) -> None:
120
+ """Configure detected tools for Keld telemetry."""
121
+ if api_url:
122
+ set_api_base_override(api_url)
123
+ auth = require_auth(no_login=no_login)
124
+ client = AtlasClient(api_base_override() or auth.api_url, token=auth.access_token)
125
+ ob = client.onboarding()
126
+ names = [t.strip() for t in tool.split(",") if t.strip()] or None
127
+ adapters = select_adapters(names)
128
+ if not adapters:
129
+ console.print("No supported tools detected. Use --tool to target one explicitly.")
130
+ raise typer.Exit(code=0)
131
+ params = SetupParams(endpoint=ob.endpoint, ingest_token=ob.ingest_token, actor=ob.actor)
132
+ _run_setup(adapters, params, client, ob, dry_run=dry_run, yes=yes, show_diff=diff)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from ..auth.store import load_auth
8
+ from ..config.manifest import Manifest
9
+ from ..console import console
10
+ from ..tools.base import ToolStatus
11
+ from ..tools.registry import ALL_ADAPTERS
12
+
13
+
14
+ def _read(path: Path) -> str | None:
15
+ return path.read_text() if path.exists() else None
16
+
17
+
18
+ def _collect_status(manifest: Manifest) -> list[tuple[str, ToolStatus]]:
19
+ rows = []
20
+ for adapter in ALL_ADAPTERS:
21
+ tm = manifest.tools.get(adapter.name)
22
+ managed = tm.managed if tm else None
23
+ st = adapter.status(_read(adapter.config_path()), managed)
24
+ rows.append((adapter.display_name, st))
25
+ return rows
26
+
27
+
28
+ def status() -> None:
29
+ """Show local Keld configuration state."""
30
+ auth = load_auth()
31
+ if auth is None:
32
+ console.print("[yellow]Not logged in[/] (run `keld login`)")
33
+ else:
34
+ console.print(f"Logged in: [bold]{auth.principal}[/] · org {auth.org} · {auth.api_url}")
35
+
36
+ manifest = Manifest.load()
37
+ for display_name, st in _collect_status(manifest):
38
+ state = "[green]configured[/]" if st.configured else (
39
+ "not configured" if st.installed else "[dim]not installed[/]")
40
+ console.print(f" {display_name:14} {state}")
41
+ if manifest.hook:
42
+ console.print(f" hook v{manifest.hook.version}")
43
+
44
+
45
+ def doctor() -> None:
46
+ """Diagnose Keld configuration problems."""
47
+ problems: list[str] = []
48
+ manifest = Manifest.load()
49
+
50
+ for name, tm in manifest.tools.items():
51
+ adapter = next((a for a in ALL_ADAPTERS if a.name == name), None)
52
+ if adapter is None:
53
+ continue
54
+ st = adapter.status(_read(Path(tm.config_path)), tm.managed)
55
+ if not st.configured:
56
+ problems.append(f"{adapter.display_name}: manifest records setup but config is not "
57
+ f"configured (drift). Re-run `keld setup`.")
58
+
59
+ if manifest.hook and not Path(manifest.hook.path).exists():
60
+ problems.append("hook script is missing. Re-run `keld setup`.")
61
+
62
+ if problems:
63
+ for p in problems:
64
+ console.print(f" [red]✗[/] {p}")
65
+ raise typer.Exit(code=1)
66
+ console.print("[green]No problems found.[/]")
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from ..config.manifest import Manifest
8
+ from ..config.writer import delete_if_empty, write_atomic
9
+ from ..console import console
10
+ from ..paths import hook_path
11
+ from ..tools.registry import get_adapter
12
+
13
+
14
+ def _run_uninstall(manifest: Manifest, names: list[str] | None,
15
+ *, yes: bool, confirm=typer.confirm) -> None:
16
+ targets = [n for n in manifest.tools if (names is None or n in names)]
17
+ if not targets:
18
+ console.print("Nothing to uninstall.")
19
+ return
20
+ if not yes and not confirm(f"Remove Keld config from {', '.join(targets)}?"):
21
+ console.print("Aborted.")
22
+ return
23
+
24
+ for name in targets:
25
+ tm = manifest.tools[name]
26
+ adapter = get_adapter(name)
27
+ path = Path(tm.config_path)
28
+ current = path.read_text() if path.exists() else None
29
+ plan = adapter.remove(current, tm.managed)
30
+ if tm.managed.get("created") and delete_if_empty(path, plan.after_text):
31
+ pass
32
+ else:
33
+ write_atomic(path, plan.after_text, backup=False)
34
+ bak = path.with_name(path.name + ".keld.bak")
35
+ if bak.exists():
36
+ bak.unlink()
37
+ del manifest.tools[name]
38
+ console.print(f" [green]✓[/] {adapter.display_name}")
39
+
40
+ if not manifest.tools:
41
+ if hook_path().exists():
42
+ hook_path().unlink()
43
+ if manifest.hook:
44
+ manifest.hook = None
45
+ manifest.endpoint = None
46
+ manifest.actor = None
47
+ manifest.save()
48
+ console.print("Done.")
49
+
50
+
51
+ def uninstall(
52
+ tool: str = typer.Option("", "--tool", help="Comma-separated tools to target."),
53
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
54
+ ) -> None:
55
+ """Remove Keld telemetry config and hook."""
56
+ names = [t.strip() for t in tool.split(",") if t.strip()] or None
57
+ _run_uninstall(Manifest.load(), names, yes=yes)
File without changes
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+
6
+ from ..paths import keld_home, manifest_path
7
+
8
+
9
+ @dataclass
10
+ class HookRecord:
11
+ path: str
12
+ version: str
13
+ sha256: str
14
+
15
+
16
+ @dataclass
17
+ class ToolManifest:
18
+ name: str
19
+ config_path: str
20
+ managed: dict = field(default_factory=dict)
21
+ backup_path: str | None = None
22
+
23
+
24
+ @dataclass
25
+ class Manifest:
26
+ endpoint: str | None = None
27
+ actor: str | None = None
28
+ tools: dict[str, ToolManifest] = field(default_factory=dict)
29
+ hook: HookRecord | None = None
30
+
31
+ @classmethod
32
+ def load(cls) -> "Manifest":
33
+ path = manifest_path()
34
+ if not path.exists():
35
+ return cls()
36
+ return cls.from_dict(json.loads(path.read_text()))
37
+
38
+ def save(self) -> None:
39
+ keld_home().mkdir(parents=True, exist_ok=True)
40
+ manifest_path().write_text(json.dumps(self.to_dict(), indent=2) + "\n")
41
+
42
+ def to_dict(self) -> dict:
43
+ return {
44
+ "endpoint": self.endpoint,
45
+ "actor": self.actor,
46
+ "tools": {k: vars(v) for k, v in self.tools.items()},
47
+ "hook": vars(self.hook) if self.hook else None,
48
+ }
49
+
50
+ @classmethod
51
+ def from_dict(cls, d: dict) -> "Manifest":
52
+ tools = {k: ToolManifest(**v) for k, v in (d.get("tools") or {}).items()}
53
+ hook = HookRecord(**d["hook"]) if d.get("hook") else None
54
+ return cls(endpoint=d.get("endpoint"), actor=d.get("actor"), tools=tools, hook=hook)
keld/config/merge.py ADDED
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tomllib
5
+
6
+ from ..errors import KeldError
7
+
8
+
9
+ def load_json(text: str | None) -> dict:
10
+ if not text or not text.strip():
11
+ return {}
12
+ try:
13
+ return json.loads(text)
14
+ except json.JSONDecodeError as exc:
15
+ raise KeldError(f"existing config is not valid JSON: {exc}") from exc
16
+
17
+
18
+ def dump_json(obj: dict) -> str:
19
+ return json.dumps(obj, indent=2) + "\n"
20
+
21
+
22
+ def merge_env(obj: dict, env: dict[str, str]) -> list[str]:
23
+ section = obj.setdefault("env", {})
24
+ section.update(env)
25
+ return list(env.keys())
26
+
27
+
28
+ def remove_section_keys(obj: dict, section: str, keys: list[str]) -> None:
29
+ sec = obj.get(section)
30
+ if not isinstance(sec, dict):
31
+ return
32
+ for key in keys:
33
+ sec.pop(key, None)
34
+ if not sec:
35
+ obj.pop(section, None)
36
+
37
+
38
+ def add_claude_hook(obj: dict, event: str, matcher: str | None, command: str) -> None:
39
+ entry: dict = {"hooks": [{"type": "command", "command": command}]}
40
+ if matcher is not None:
41
+ entry = {"matcher": matcher, **entry}
42
+ arr = obj.setdefault("hooks", {}).setdefault(event, [])
43
+ if entry not in arr:
44
+ arr.append(entry)
45
+
46
+
47
+ def has_hook_with_command(obj: dict, substr: str) -> bool:
48
+ hooks = obj.get("hooks")
49
+ if not isinstance(hooks, dict):
50
+ return False
51
+ return substr in json.dumps(hooks)
52
+
53
+
54
+ def remove_hooks_by_command(obj: dict, substr: str) -> None:
55
+ hooks = obj.get("hooks")
56
+ if not isinstance(hooks, dict):
57
+ return
58
+ for event in list(hooks):
59
+ arr = hooks[event]
60
+ arr[:] = [e for e in arr if substr not in json.dumps(e)]
61
+ if not arr:
62
+ del hooks[event]
63
+ if not hooks:
64
+ obj.pop("hooks", None)
65
+
66
+
67
+ KELD_TOML_START = "# >>> keld (managed by keld CLI — do not edit between markers)"
68
+ KELD_TOML_END = "# <<< keld"
69
+
70
+
71
+ def has_keld_block(text: str | None) -> bool:
72
+ return bool(text) and KELD_TOML_START in text
73
+
74
+
75
+ def strip_keld_block(text: str | None) -> str:
76
+ if not text or KELD_TOML_START not in text:
77
+ return text or ""
78
+ lines = text.splitlines()
79
+ out: list[str] = []
80
+ inside = False
81
+ for line in lines:
82
+ if line.strip() == KELD_TOML_START:
83
+ inside = True
84
+ continue
85
+ if inside and line.strip() == KELD_TOML_END:
86
+ inside = False
87
+ continue
88
+ if not inside:
89
+ out.append(line)
90
+ result = "\n".join(out).rstrip("\n")
91
+ return result + "\n" if result else ""
92
+
93
+
94
+ def upsert_keld_block(text: str | None, body: str) -> str:
95
+ base = strip_keld_block(text)
96
+ body = body if body.endswith("\n") else body + "\n"
97
+ block = f"{KELD_TOML_START}\n{body}{KELD_TOML_END}\n"
98
+ if not base.strip():
99
+ return block
100
+ return base + "\n" + block
101
+
102
+
103
+ def validate_toml(text: str) -> None:
104
+ try:
105
+ tomllib.loads(text)
106
+ except tomllib.TOMLDecodeError as exc:
107
+ raise KeldError(f"resulting TOML is invalid: {exc}") from exc
108
+
109
+
110
+ def strip_toml_table(text: str, table: str) -> str:
111
+ """Remove a top-level [table] (and its [table.sub] subtables) from raw TOML
112
+ text, preserving all other content. No-op if the table is absent.
113
+
114
+ Walks lines tracking the current top-level table (first dotted segment of
115
+ the most recent [header]/[[header]]); drops lines while it equals `table`.
116
+ """
117
+ out: list[str] = []
118
+ dropping = False
119
+ for line in text.splitlines(keepends=True):
120
+ stripped = line.strip()
121
+ if stripped.startswith("["):
122
+ header = stripped.strip("[]").strip()
123
+ top = header.split(".", 1)[0].strip().strip('"').strip("'")
124
+ dropping = top == table
125
+ if not dropping:
126
+ out.append(line)
127
+ return "".join(out)