keld 0.1.0__tar.gz

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-0.1.0/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
keld-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: keld
3
+ Version: 0.1.0
4
+ Summary: Keld CLI — set up local telemetry for AI coding tools
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: rich>=13
8
+ Requires-Dist: typer>=0.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # keld
12
+
13
+ The Keld CLI configures your local AI coding tools (Claude Code, Codex, Gemini
14
+ CLI) to send telemetry to Keld Atlas.
15
+
16
+ ## Install
17
+
18
+ Recommended (isolated, works on every platform):
19
+
20
+ ```bash
21
+ pipx install keld
22
+ ```
23
+
24
+ If you don't have pipx yet: `python -m pip install --user pipx && python -m pipx ensurepath`.
25
+
26
+ Alternatives: `uvx keld` / `uv tool install keld` (if you use uv), or
27
+ `pip install keld` **inside a virtual environment** (a bare `pip install` into
28
+ system Python fails with `externally-managed-environment` on modern distros).
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ keld login # authenticate (also happens automatically on first `signal setup`)
34
+
35
+ keld signal setup # detect tools, show changes, configure telemetry + install hook
36
+ keld signal status # see what's configured
37
+ keld signal doctor # diagnose problems
38
+ keld signal uninstall # cleanly remove everything Keld added
39
+ ```
40
+
41
+ Auth commands (`login`, `logout`, `whoami`) are top-level and shared across
42
+ Keld product groups. Telemetry onboarding lives under the `keld signal` group.
43
+
44
+ `keld signal setup` flags: `--tool claude_code,codex` (target specific tools),
45
+ `--dry-run` (show changes only), `--yes` (skip confirmation),
46
+ `--no-login` (fail instead of opening a browser, for CI).
47
+
48
+ `setup` is interactive. By default it prints a concise summary of the changes to
49
+ each config file; pass `--diff` to see the full unified diff. If a tool's config
50
+ already has settings Keld can't safely merge (e.g. Codex with its own `[otel]`
51
+ section), setup explains the conflict and lets you **[s]kip** that tool,
52
+ **[r]eplace** just the conflicting section with Keld's (the rest of your config
53
+ is preserved, and the diff is always shown for a replace), or **[a]bort**. Every
54
+ file Keld modifies is first copied to `~/.keld/backups/<tool>/`. Use `--dry-run`
55
+ to preview without writing and `--yes` to skip prompts (conflicts are
56
+ auto-skipped in that mode).
57
+
58
+ ### Local development
59
+
60
+ To point the CLI at a Keld server running locally, pass `--api-url` to `keld
61
+ login` (or `keld signal setup`):
62
+
63
+ ```bash
64
+ keld login --api-url http://localhost:8000 # auth against the local server
65
+ keld signal setup # remembered — uses the same server
66
+ ```
67
+
68
+ The chosen URL is stored with your credentials, so subsequent commands target it
69
+ automatically. `--api-url` overrides the `KELD_API_URL` environment variable,
70
+ which does the same thing if you prefer setting it in your shell.
71
+
72
+ ## Authentication
73
+
74
+ `keld` signs you in with a **browser-based device authorization** flow — you
75
+ approve the CLI from a normal signed-in Keld session, so your password is never
76
+ typed into (or seen by) the terminal.
77
+
78
+ ```mermaid
79
+ sequenceDiagram
80
+ actor You
81
+ participant CLI as keld CLI
82
+ participant Browser
83
+ participant Keld
84
+
85
+ You->>CLI: keld login (or any `keld signal` command)
86
+ CLI->>Keld: start device authorization
87
+ Keld-->>CLI: verification URL + short code
88
+ CLI->>Browser: open the verification URL
89
+ You->>Browser: sign in to Keld and confirm the code
90
+ Browser->>Keld: approve (from your authenticated session)
91
+ loop until you approve (or the request expires)
92
+ CLI->>Keld: check whether it's approved yet
93
+ Keld-->>CLI: not yet… then: an access token
94
+ end
95
+ CLI->>CLI: save the token locally (your user only)
96
+ Note over CLI,Keld: keld signal setup then uses that token to fetch your<br/>telemetry config and set up your local tools
97
+ ```
98
+
99
+ Key points:
100
+
101
+ - **Lazy by default** — you don't have to run `keld login` first. Any command
102
+ that needs auth (e.g. `keld signal setup`) starts this flow automatically; on a
103
+ CI box use `--no-login` to fail cleanly instead of opening a browser.
104
+ - **You approve, in the browser** — the short code shown in your terminal is only
105
+ meaningful to confirm inside an authenticated Keld session, and the approval is
106
+ attributed to the signed-in person. The request stops working shortly after it
107
+ is issued if left unapproved.
108
+ - **The token stays on your machine** — it's written under `~/.keld` with
109
+ user-only file permissions (override the location with `KELD_HOME`). `keld
110
+ whoami` shows who you're signed in as; `keld logout` removes it. Tokens are
111
+ revocable from Keld, so a lost laptop can be cut off without rotating anything
112
+ else.
113
+
114
+ ## Environment
115
+
116
+ - `KELD_HOME` — where credentials, the hook, and the manifest live (default `~/.keld`).
117
+ - `KELD_API_URL` — Atlas base URL (default `https://atlas.keld.co`).
keld-0.1.0/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # keld
2
+
3
+ The Keld CLI configures your local AI coding tools (Claude Code, Codex, Gemini
4
+ CLI) to send telemetry to Keld Atlas.
5
+
6
+ ## Install
7
+
8
+ Recommended (isolated, works on every platform):
9
+
10
+ ```bash
11
+ pipx install keld
12
+ ```
13
+
14
+ If you don't have pipx yet: `python -m pip install --user pipx && python -m pipx ensurepath`.
15
+
16
+ Alternatives: `uvx keld` / `uv tool install keld` (if you use uv), or
17
+ `pip install keld` **inside a virtual environment** (a bare `pip install` into
18
+ system Python fails with `externally-managed-environment` on modern distros).
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ keld login # authenticate (also happens automatically on first `signal setup`)
24
+
25
+ keld signal setup # detect tools, show changes, configure telemetry + install hook
26
+ keld signal status # see what's configured
27
+ keld signal doctor # diagnose problems
28
+ keld signal uninstall # cleanly remove everything Keld added
29
+ ```
30
+
31
+ Auth commands (`login`, `logout`, `whoami`) are top-level and shared across
32
+ Keld product groups. Telemetry onboarding lives under the `keld signal` group.
33
+
34
+ `keld signal setup` flags: `--tool claude_code,codex` (target specific tools),
35
+ `--dry-run` (show changes only), `--yes` (skip confirmation),
36
+ `--no-login` (fail instead of opening a browser, for CI).
37
+
38
+ `setup` is interactive. By default it prints a concise summary of the changes to
39
+ each config file; pass `--diff` to see the full unified diff. If a tool's config
40
+ already has settings Keld can't safely merge (e.g. Codex with its own `[otel]`
41
+ section), setup explains the conflict and lets you **[s]kip** that tool,
42
+ **[r]eplace** just the conflicting section with Keld's (the rest of your config
43
+ is preserved, and the diff is always shown for a replace), or **[a]bort**. Every
44
+ file Keld modifies is first copied to `~/.keld/backups/<tool>/`. Use `--dry-run`
45
+ to preview without writing and `--yes` to skip prompts (conflicts are
46
+ auto-skipped in that mode).
47
+
48
+ ### Local development
49
+
50
+ To point the CLI at a Keld server running locally, pass `--api-url` to `keld
51
+ login` (or `keld signal setup`):
52
+
53
+ ```bash
54
+ keld login --api-url http://localhost:8000 # auth against the local server
55
+ keld signal setup # remembered — uses the same server
56
+ ```
57
+
58
+ The chosen URL is stored with your credentials, so subsequent commands target it
59
+ automatically. `--api-url` overrides the `KELD_API_URL` environment variable,
60
+ which does the same thing if you prefer setting it in your shell.
61
+
62
+ ## Authentication
63
+
64
+ `keld` signs you in with a **browser-based device authorization** flow — you
65
+ approve the CLI from a normal signed-in Keld session, so your password is never
66
+ typed into (or seen by) the terminal.
67
+
68
+ ```mermaid
69
+ sequenceDiagram
70
+ actor You
71
+ participant CLI as keld CLI
72
+ participant Browser
73
+ participant Keld
74
+
75
+ You->>CLI: keld login (or any `keld signal` command)
76
+ CLI->>Keld: start device authorization
77
+ Keld-->>CLI: verification URL + short code
78
+ CLI->>Browser: open the verification URL
79
+ You->>Browser: sign in to Keld and confirm the code
80
+ Browser->>Keld: approve (from your authenticated session)
81
+ loop until you approve (or the request expires)
82
+ CLI->>Keld: check whether it's approved yet
83
+ Keld-->>CLI: not yet… then: an access token
84
+ end
85
+ CLI->>CLI: save the token locally (your user only)
86
+ Note over CLI,Keld: keld signal setup then uses that token to fetch your<br/>telemetry config and set up your local tools
87
+ ```
88
+
89
+ Key points:
90
+
91
+ - **Lazy by default** — you don't have to run `keld login` first. Any command
92
+ that needs auth (e.g. `keld signal setup`) starts this flow automatically; on a
93
+ CI box use `--no-login` to fail cleanly instead of opening a browser.
94
+ - **You approve, in the browser** — the short code shown in your terminal is only
95
+ meaningful to confirm inside an authenticated Keld session, and the approval is
96
+ attributed to the signed-in person. The request stops working shortly after it
97
+ is issued if left unapproved.
98
+ - **The token stays on your machine** — it's written under `~/.keld` with
99
+ user-only file permissions (override the location with `KELD_HOME`). `keld
100
+ whoami` shows who you're signed in as; `keld logout` removes it. Tokens are
101
+ revocable from Keld, so a lost laptop can be cut off without rotating anything
102
+ else.
103
+
104
+ ## Environment
105
+
106
+ - `KELD_HOME` — where credentials, the hook, and the manifest live (default `~/.keld`).
107
+ - `KELD_API_URL` — Atlas base URL (default `https://atlas.keld.co`).
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "keld"
3
+ version = "0.1.0"
4
+ description = "Keld CLI — set up local telemetry for AI coding tools"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = ["typer>=0.12", "httpx>=0.27", "rich>=13"]
8
+
9
+ [project.scripts]
10
+ keld = "keld.cli:main"
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/keld"]
18
+
19
+ # Keep the sdist to shippable source only — exclude internal dev artifacts
20
+ # (.superpowers/, docs/ plans, tests) that would otherwise be published.
21
+ [tool.hatch.build.targets.sdist]
22
+ include = ["src/keld", "README.md", "pyproject.toml"]
23
+
24
+ [dependency-groups]
25
+ dev = ["pytest>=8"]
26
+
27
+ [tool.pytest.ini_options]
28
+ addopts = "-q"
29
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -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
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)
@@ -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
@@ -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
@@ -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}")
@@ -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)