swisper-prism-cli 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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ dist/
8
+ build/
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-04-27
4
+
5
+ First public release on PyPI.
6
+
7
+ - Code-intelligence CLI for AI coding agents.
8
+ - Subcommands: `search`, `find-refs`, `def`, `body`, `outline`, `module-map`, `deps`, `prepare-edit`, `edit-anchors`, `check`, `ping`, `list-repos`, `find`.
9
+ - `--auto-overlay` flag (default ON for editing-relevant subcommands) for unpushed-file freshness via overlay γ (FEAT_013).
10
+ - Auth via `PRISM_TOKEN` env or piggyback on `~/.claude.json:mcpServers.prism-hosted`.
11
+ - Base URL via `PRISM_BASE_URL` env or default `https://prism-gateway.swisper.app`.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: swisper-prism-cli
3
+ Version: 0.1.0
4
+ Summary: Prism — code-intelligence CLI for AI coding agents
5
+ Project-URL: Homepage, https://github.com/Fintama/swisper_prism
6
+ Project-URL: Repository, https://github.com/Fintama/swisper_prism
7
+ Project-URL: Documentation, https://github.com/Fintama/swisper_prism/tree/main/apps/prism-cli
8
+ Author: Fintama Prism Team
9
+ License: Proprietary - Fintama Internal Use Only
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: click>=8.1
12
+ Requires-Dist: httpx>=0.27
13
+ Description-Content-Type: text/markdown
14
+
15
+ # swisper-prism-cli
16
+
17
+ `prism` — code-intelligence CLI for AI coding agents. Search, navigate, and reason about your codebase from the shell, with overlay-γ awareness for unpushed local edits.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install --user swisper-prism-cli
23
+ ```
24
+
25
+ Or with [pipx](https://pipx.pypa.io/) (recommended on systems where `pip --user` is restricted):
26
+
27
+ ```bash
28
+ pipx install swisper-prism-cli
29
+ ```
30
+
31
+ Requires Python ≥ 3.11.
32
+
33
+ ## Auth
34
+
35
+ Generate a developer token at <https://prism-console-swisper.web.app/settings>, then either:
36
+
37
+ ```bash
38
+ export PRISM_TOKEN="prism_<your-token>"
39
+ ```
40
+
41
+ …or rely on `~/.claude.json:mcpServers.prism-hosted.headers.Authorization` if you already have the Prism MCP server configured (the CLI piggybacks on that config).
42
+
43
+ ## Verify
44
+
45
+ ```bash
46
+ prism ping
47
+ ```
48
+
49
+ Expected: `{"status": "ok", ...}` plus a freshness timestamp.
50
+
51
+ ## Common commands
52
+
53
+ ```bash
54
+ prism search "rate limit retry"
55
+ prism find-refs "User.save" --pretty
56
+ prism def "MyClass.handler"
57
+ prism prepare-edit "MyClass.handler"
58
+ prism module-map
59
+ prism --help
60
+ ```
61
+
62
+ The `--auto-overlay` flag (default ON for editing-relevant subcommands) handles freshness for unpushed local edits — your dirty-file content flows into the query without a reindex.
63
+
64
+ ## Surfaces
65
+
66
+ | Tool | When |
67
+ |---|---|
68
+ | `prism` CLI (this package) | Coding agents — anything with shell access (Cursor, Claude Code, Windsurf, Codex, Devin, Antigravity) |
69
+ | MCP server (`prism-hosted` in `~/.claude.json`) | Non-editing agents (chatbots like Claude Desktop) |
70
+
71
+ Both surfaces talk to the same gateway and use the same token.
72
+
73
+ ## Contributing (developers)
74
+
75
+ ```bash
76
+ git clone https://github.com/Fintama/swisper_prism
77
+ cd swisper_prism/apps/prism-cli
78
+ uv pip install -e .
79
+ uv run python -m pytest tests/ -v
80
+ ```
81
+
82
+ See [`CHANGELOG.md`](./CHANGELOG.md) for release history.
@@ -0,0 +1,68 @@
1
+ # swisper-prism-cli
2
+
3
+ `prism` — code-intelligence CLI for AI coding agents. Search, navigate, and reason about your codebase from the shell, with overlay-γ awareness for unpushed local edits.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install --user swisper-prism-cli
9
+ ```
10
+
11
+ Or with [pipx](https://pipx.pypa.io/) (recommended on systems where `pip --user` is restricted):
12
+
13
+ ```bash
14
+ pipx install swisper-prism-cli
15
+ ```
16
+
17
+ Requires Python ≥ 3.11.
18
+
19
+ ## Auth
20
+
21
+ Generate a developer token at <https://prism-console-swisper.web.app/settings>, then either:
22
+
23
+ ```bash
24
+ export PRISM_TOKEN="prism_<your-token>"
25
+ ```
26
+
27
+ …or rely on `~/.claude.json:mcpServers.prism-hosted.headers.Authorization` if you already have the Prism MCP server configured (the CLI piggybacks on that config).
28
+
29
+ ## Verify
30
+
31
+ ```bash
32
+ prism ping
33
+ ```
34
+
35
+ Expected: `{"status": "ok", ...}` plus a freshness timestamp.
36
+
37
+ ## Common commands
38
+
39
+ ```bash
40
+ prism search "rate limit retry"
41
+ prism find-refs "User.save" --pretty
42
+ prism def "MyClass.handler"
43
+ prism prepare-edit "MyClass.handler"
44
+ prism module-map
45
+ prism --help
46
+ ```
47
+
48
+ The `--auto-overlay` flag (default ON for editing-relevant subcommands) handles freshness for unpushed local edits — your dirty-file content flows into the query without a reindex.
49
+
50
+ ## Surfaces
51
+
52
+ | Tool | When |
53
+ |---|---|
54
+ | `prism` CLI (this package) | Coding agents — anything with shell access (Cursor, Claude Code, Windsurf, Codex, Devin, Antigravity) |
55
+ | MCP server (`prism-hosted` in `~/.claude.json`) | Non-editing agents (chatbots like Claude Desktop) |
56
+
57
+ Both surfaces talk to the same gateway and use the same token.
58
+
59
+ ## Contributing (developers)
60
+
61
+ ```bash
62
+ git clone https://github.com/Fintama/swisper_prism
63
+ cd swisper_prism/apps/prism-cli
64
+ uv pip install -e .
65
+ uv run python -m pytest tests/ -v
66
+ ```
67
+
68
+ See [`CHANGELOG.md`](./CHANGELOG.md) for release history.
File without changes
@@ -0,0 +1,3 @@
1
+ from prism_cli.cli import main
2
+
3
+ main()
@@ -0,0 +1,47 @@
1
+ """Thin HTTP client for the prism gateway /api/v1 surface."""
2
+ from __future__ import annotations
3
+
4
+ import json as _json
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .auth import resolve_base_url, resolve_token
10
+
11
+
12
+ class PrismAPIError(RuntimeError):
13
+ pass
14
+
15
+
16
+ class PrismClient:
17
+ def __init__(
18
+ self, base_url: str | None = None, token: str | None = None,
19
+ timeout: float = 30.0,
20
+ ) -> None:
21
+ self.base_url = (base_url or resolve_base_url()).rstrip("/")
22
+ self.token = token or resolve_token()
23
+ self._http = httpx.Client(timeout=timeout)
24
+
25
+ def call(self, subcommand: str, payload: dict[str, Any]) -> dict[str, Any]:
26
+ url = f"{self.base_url}/api/v1/{subcommand}"
27
+ headers = {
28
+ "Authorization": f"Bearer {self.token}",
29
+ "Content-Type": "application/json",
30
+ }
31
+ try:
32
+ resp = self._http.post(url, json=payload, headers=headers)
33
+ except httpx.HTTPError as e:
34
+ raise PrismAPIError(f"network: {e}") from e
35
+ if resp.status_code == 413:
36
+ raise PrismAPIError(f"overlay rejected (413): {resp.text}")
37
+ if resp.status_code >= 500:
38
+ raise PrismAPIError(f"server error {resp.status_code}: {resp.text}")
39
+ if resp.status_code >= 400:
40
+ raise PrismAPIError(f"client error {resp.status_code}: {resp.text}")
41
+ try:
42
+ return resp.json()
43
+ except _json.JSONDecodeError as e:
44
+ raise PrismAPIError(f"non-JSON response: {resp.text[:200]}") from e
45
+
46
+ def close(self) -> None:
47
+ self._http.close()
@@ -0,0 +1,57 @@
1
+ """Resolve the Prism dev token from env var or ~/.claude.json."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ class AuthError(RuntimeError):
10
+ """Raised when no Prism token is available."""
11
+
12
+
13
+ def resolve_token() -> str:
14
+ env_token = os.environ.get("PRISM_TOKEN")
15
+ if env_token:
16
+ return env_token
17
+
18
+ claude_json = Path.home() / ".claude.json"
19
+ if claude_json.exists():
20
+ try:
21
+ data = json.loads(claude_json.read_text())
22
+ except json.JSONDecodeError:
23
+ data = {}
24
+ servers = data.get("mcpServers", {})
25
+ prism = servers.get("prism-hosted") or servers.get("prism")
26
+ if prism:
27
+ auth = (prism.get("headers") or {}).get("Authorization", "")
28
+ if auth.startswith("Bearer "):
29
+ return auth.removeprefix("Bearer ")
30
+
31
+ raise AuthError(
32
+ "No Prism token found. Set PRISM_TOKEN env var or configure "
33
+ "prism-hosted in ~/.claude.json."
34
+ )
35
+
36
+
37
+ def resolve_base_url() -> str:
38
+ env_url = os.environ.get("PRISM_BASE_URL")
39
+ if env_url:
40
+ return env_url
41
+ claude_json = Path.home() / ".claude.json"
42
+ if claude_json.exists():
43
+ try:
44
+ data = json.loads(claude_json.read_text())
45
+ except json.JSONDecodeError:
46
+ data = {}
47
+ servers = data.get("mcpServers", {})
48
+ prism = servers.get("prism-hosted") or servers.get("prism")
49
+ if prism and "url" in prism:
50
+ # Strip trailing /mcp or /sse to get gateway root
51
+ url: str = prism["url"]
52
+ for suffix in ("/mcp", "/sse", "/"):
53
+ if url.endswith(suffix):
54
+ url = url[: -len(suffix)]
55
+ break
56
+ return url
57
+ return "https://prism-gateway.swisper.app" # default
@@ -0,0 +1,93 @@
1
+ """Automate the §6.6 recipe: collect git state, build overlay payload."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+
8
+ _MAX_OVERLAY_BYTES = 1024 * 1024 # 1 MB
9
+
10
+
11
+ def collect_git_state() -> dict:
12
+ """Run git commands; return local_state-shaped dict.
13
+
14
+ Empty dict on failure (caller should not pass overlay in that case).
15
+ """
16
+ try:
17
+ head = subprocess.check_output(
18
+ ["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL,
19
+ ).strip()
20
+ except (subprocess.CalledProcessError, FileNotFoundError):
21
+ return {}
22
+ try:
23
+ branch = subprocess.check_output(
24
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
25
+ text=True, stderr=subprocess.DEVNULL,
26
+ ).strip()
27
+ except subprocess.CalledProcessError:
28
+ branch = ""
29
+
30
+ dirty_paths: list[str] = []
31
+ try:
32
+ porcelain = subprocess.check_output(
33
+ ["git", "status", "--porcelain", "-uall"],
34
+ text=True, stderr=subprocess.DEVNULL,
35
+ )
36
+ except subprocess.CalledProcessError:
37
+ return {}
38
+ for line in porcelain.splitlines():
39
+ if not line.strip():
40
+ continue
41
+ # Two-char status code + space + path. Untracked is "??".
42
+ path = line[3:].strip()
43
+ if path:
44
+ dirty_paths.append(path)
45
+
46
+ unpushed: list[str] = []
47
+ try:
48
+ log = subprocess.check_output(
49
+ ["git", "log", "@{upstream}..HEAD", "--format=%H"],
50
+ text=True, stderr=subprocess.DEVNULL,
51
+ )
52
+ unpushed = [s.strip() for s in log.splitlines() if s.strip()]
53
+ except subprocess.CalledProcessError:
54
+ pass # no upstream; not unusual on local-only branches
55
+
56
+ return {
57
+ "head_sha": head,
58
+ "branch": branch,
59
+ "dirty_paths": dirty_paths,
60
+ "unpushed_commits": unpushed,
61
+ }
62
+
63
+
64
+ def build_overlay(
65
+ state: dict, root: Path | None = None, max_bytes: int = _MAX_OVERLAY_BYTES
66
+ ) -> dict:
67
+ """Build {local_state, working_tree_overlay} from a state dict.
68
+
69
+ Reads dirty_paths from disk; truncates if total exceeds max_bytes.
70
+ """
71
+ if not state:
72
+ return {}
73
+ base = Path(root) if root else Path.cwd()
74
+ files: dict[str, str] = {}
75
+ total = 0
76
+ for path in state.get("dirty_paths", []):
77
+ full = base / path
78
+ if not full.is_file():
79
+ continue
80
+ try:
81
+ content = full.read_text(encoding="utf-8", errors="replace")
82
+ except OSError:
83
+ continue
84
+ size = len(content.encode("utf-8"))
85
+ if total + size > max_bytes:
86
+ break # cap; remaining files dropped (caller decides if to retry)
87
+ files[path] = content
88
+ total += size
89
+
90
+ return {
91
+ "local_state": state,
92
+ "working_tree_overlay": files,
93
+ }
@@ -0,0 +1,265 @@
1
+ """prism CLI entrypoint."""
2
+ from __future__ import annotations
3
+
4
+ import json as _json
5
+ import sys
6
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from .api_client import PrismClient, PrismAPIError
12
+ from .auto_overlay import build_overlay, collect_git_state
13
+ from .auth import AuthError
14
+
15
+
16
+ def _resolve_version() -> str:
17
+ try:
18
+ return _pkg_version("swisper-prism-cli")
19
+ except PackageNotFoundError:
20
+ return "0.0.0+local"
21
+
22
+
23
+ # Subcommands that benefit from --auto-overlay by default.
24
+ _OVERLAY_DEFAULT_ON = {
25
+ "search", "grep", "find-refs", "refs", "def", "outline",
26
+ "module-map", "map", "body", "prepare-edit", "edit-anchors",
27
+ }
28
+
29
+
30
+ def _format(result: dict[str, Any], pretty: bool) -> str:
31
+ if pretty:
32
+ return _json.dumps(result, indent=2)
33
+ return _json.dumps(result)
34
+
35
+
36
+ def _maybe_attach_overlay(payload: dict[str, Any], auto_overlay: bool) -> dict[str, Any]:
37
+ if not auto_overlay:
38
+ return payload
39
+ state = collect_git_state()
40
+ if not state:
41
+ return payload
42
+ overlay = build_overlay(state)
43
+ return {**payload, **overlay}
44
+
45
+
46
+ def _retry_with_recommended(
47
+ client: PrismClient, subcommand: str, payload: dict[str, Any], result: dict[str, Any],
48
+ ) -> dict[str, Any]:
49
+ """If response asks for overlay on additional files, retry once.
50
+
51
+ overlay_recommended_for is stamped at the top level of the response
52
+ dict (not nested under 'freshness').
53
+ """
54
+ rec = result.get("overlay_recommended_for") or []
55
+ if not rec:
56
+ return result
57
+ state = collect_git_state()
58
+ overlay = build_overlay({**state, "dirty_paths": rec})
59
+ new_payload = {**payload, **overlay}
60
+ return client.call(subcommand, new_payload)
61
+
62
+
63
+ def _run(
64
+ subcommand: str, payload: dict[str, Any], *, auto_overlay: bool, pretty: bool
65
+ ) -> int:
66
+ try:
67
+ client = PrismClient()
68
+ except AuthError as e:
69
+ click.echo(f"auth: {e}", err=True)
70
+ return 1
71
+ payload = _maybe_attach_overlay(payload, auto_overlay)
72
+ try:
73
+ result = client.call(subcommand, payload)
74
+ if auto_overlay:
75
+ result = _retry_with_recommended(client, subcommand, payload, result)
76
+ except PrismAPIError as e:
77
+ click.echo(str(e), err=True)
78
+ return 2
79
+ finally:
80
+ try:
81
+ client.close()
82
+ except Exception:
83
+ pass
84
+ click.echo(_format(result, pretty))
85
+ return 0
86
+
87
+
88
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
89
+ @click.version_option(_resolve_version(), prog_name="prism")
90
+ @click.option("--pretty", is_flag=True, help="Indent JSON output.")
91
+ @click.pass_context
92
+ def cli(ctx: click.Context, pretty: bool) -> None:
93
+ """prism — code-intelligence CLI for AI coding agents."""
94
+ ctx.ensure_object(dict)
95
+ ctx.obj["pretty"] = pretty
96
+
97
+
98
+ def _add_overlay_flag(default_on: bool) -> Any:
99
+ return click.option(
100
+ "--auto-overlay/--no-overlay",
101
+ default=default_on,
102
+ help="Run git status + read dirty files into overlay payload.",
103
+ )
104
+
105
+
106
+ @cli.command()
107
+ @click.argument("query")
108
+ @click.option("--limit", type=int, default=20)
109
+ @click.option("--repo")
110
+ @_add_overlay_flag(True)
111
+ @click.pass_context
112
+ def search(ctx: click.Context, query: str, limit: int, repo: str | None, auto_overlay: bool) -> None:
113
+ payload: dict[str, Any] = {"query": query, "limit": limit}
114
+ if repo:
115
+ payload["repo"] = repo
116
+ sys.exit(_run("search", payload, auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
117
+
118
+
119
+ @cli.command(name="grep")
120
+ @click.argument("query")
121
+ @click.option("--limit", type=int, default=20)
122
+ @click.option("--repo")
123
+ @_add_overlay_flag(True)
124
+ @click.pass_context
125
+ def grep(ctx: click.Context, query: str, limit: int, repo: str | None, auto_overlay: bool) -> None:
126
+ """Alias for search."""
127
+ payload: dict[str, Any] = {"query": query, "limit": limit}
128
+ if repo:
129
+ payload["repo"] = repo
130
+ sys.exit(_run("search", payload, auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
131
+
132
+
133
+ @cli.command(name="find-refs")
134
+ @click.argument("symbol")
135
+ @click.option(
136
+ "--kind",
137
+ type=click.Choice(["all", "callers", "imports", "definitions"]),
138
+ default="all",
139
+ )
140
+ @click.option("--repo")
141
+ @_add_overlay_flag(True)
142
+ @click.pass_context
143
+ def find_refs(ctx: click.Context, symbol: str, kind: str, repo: str | None, auto_overlay: bool) -> None:
144
+ payload: dict[str, Any] = {"symbol": symbol, "reference_type": kind}
145
+ if repo:
146
+ payload["repo"] = repo
147
+ sys.exit(_run("find-refs", payload, auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
148
+
149
+
150
+ @cli.command(name="refs")
151
+ @click.argument("symbol")
152
+ @_add_overlay_flag(True)
153
+ @click.pass_context
154
+ def refs(ctx: click.Context, symbol: str, auto_overlay: bool) -> None:
155
+ """Alias for find-refs."""
156
+ sys.exit(_run("find-refs", {"symbol": symbol},
157
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
158
+
159
+
160
+ @cli.command(name="def")
161
+ @click.argument("symbol")
162
+ @_add_overlay_flag(True)
163
+ @click.pass_context
164
+ def definition(ctx: click.Context, symbol: str, auto_overlay: bool) -> None:
165
+ sys.exit(_run("def", {"symbol": symbol},
166
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
167
+
168
+
169
+ @cli.command()
170
+ @click.argument("path")
171
+ @_add_overlay_flag(True)
172
+ @click.pass_context
173
+ def outline(ctx: click.Context, path: str, auto_overlay: bool) -> None:
174
+ sys.exit(_run("outline", {"file_path": path},
175
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
176
+
177
+
178
+ @cli.command(name="module-map")
179
+ @click.argument("path", required=False, default=".")
180
+ @_add_overlay_flag(True)
181
+ @click.pass_context
182
+ def module_map(ctx: click.Context, path: str, auto_overlay: bool) -> None:
183
+ sys.exit(_run("module-map", {"path": path},
184
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
185
+
186
+
187
+ @cli.command(name="map")
188
+ @click.argument("path", required=False, default=".")
189
+ @_add_overlay_flag(True)
190
+ @click.pass_context
191
+ def map_alias(ctx: click.Context, path: str, auto_overlay: bool) -> None:
192
+ """Alias for module-map."""
193
+ sys.exit(_run("module-map", {"path": path},
194
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
195
+
196
+
197
+ @cli.command()
198
+ @click.argument("symbol")
199
+ @_add_overlay_flag(True)
200
+ @click.pass_context
201
+ def body(ctx: click.Context, symbol: str, auto_overlay: bool) -> None:
202
+ sys.exit(_run("body", {"symbol": symbol},
203
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
204
+
205
+
206
+ @cli.command()
207
+ @click.argument("path")
208
+ @click.pass_context
209
+ def deps(ctx: click.Context, path: str) -> None:
210
+ sys.exit(_run("deps", {"path": path},
211
+ auto_overlay=False, pretty=ctx.obj["pretty"]))
212
+
213
+
214
+ @cli.command(name="prepare-edit")
215
+ @click.argument("symbol")
216
+ @_add_overlay_flag(True)
217
+ @click.pass_context
218
+ def prepare_edit(ctx: click.Context, symbol: str, auto_overlay: bool) -> None:
219
+ sys.exit(_run("prepare-edit", {"symbol": symbol},
220
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
221
+
222
+
223
+ @cli.command(name="edit-anchors")
224
+ @click.argument("symbol")
225
+ @_add_overlay_flag(True)
226
+ @click.pass_context
227
+ def edit_anchors(ctx: click.Context, symbol: str, auto_overlay: bool) -> None:
228
+ sys.exit(_run("edit-anchors", {"symbol": symbol},
229
+ auto_overlay=auto_overlay, pretty=ctx.obj["pretty"]))
230
+
231
+
232
+ @cli.command()
233
+ @click.argument("intent")
234
+ @click.pass_context
235
+ def check(ctx: click.Context, intent: str) -> None:
236
+ sys.exit(_run("check", {"intent": intent},
237
+ auto_overlay=False, pretty=ctx.obj["pretty"]))
238
+
239
+
240
+ @cli.command(name="find")
241
+ @click.argument("pattern")
242
+ @click.pass_context
243
+ def find_files(ctx: click.Context, pattern: str) -> None:
244
+ sys.exit(_run("find", {"pattern": pattern},
245
+ auto_overlay=False, pretty=ctx.obj["pretty"]))
246
+
247
+
248
+ @cli.command()
249
+ @click.pass_context
250
+ def ping(ctx: click.Context) -> None:
251
+ sys.exit(_run("ping", {}, auto_overlay=False, pretty=ctx.obj["pretty"]))
252
+
253
+
254
+ @cli.command(name="list-repos")
255
+ @click.pass_context
256
+ def list_repos(ctx: click.Context) -> None:
257
+ sys.exit(_run("list-repos", {}, auto_overlay=False, pretty=ctx.obj["pretty"]))
258
+
259
+
260
+ def main() -> None:
261
+ cli(obj={})
262
+
263
+
264
+ if __name__ == "__main__":
265
+ main()
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "swisper-prism-cli"
3
+ version = "0.1.0"
4
+ description = "Prism — code-intelligence CLI for AI coding agents"
5
+ authors = [{name = "Fintama Prism Team"}]
6
+ license = {text = "Proprietary - Fintama Internal Use Only"}
7
+ readme = "README.md"
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "click>=8.1",
11
+ "httpx>=0.27",
12
+ ]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/Fintama/swisper_prism"
16
+ Repository = "https://github.com/Fintama/swisper_prism"
17
+ Documentation = "https://github.com/Fintama/swisper_prism/tree/main/apps/prism-cli"
18
+
19
+ [project.scripts]
20
+ prism = "prism_cli.cli:main"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["prism_cli"]
28
+
29
+ [tool.pytest.ini_options]
30
+ markers = [
31
+ "unit: fast unit tests (no I/O)",
32
+ "integration: integration tests",
33
+ "e2e: end-to-end tests",
34
+ "ci_critical: golden-path tests that must complete in <2s each",
35
+ ]
File without changes
@@ -0,0 +1,75 @@
1
+ """Tests for the prism CLI auth resolution."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+
11
+ @pytest.mark.unit
12
+ def test_token_from_env_var(monkeypatch):
13
+ monkeypatch.setenv("PRISM_TOKEN", "from-env")
14
+ from prism_cli.auth import resolve_token
15
+ assert resolve_token() == "from-env"
16
+
17
+
18
+ @pytest.mark.unit
19
+ def test_token_from_claude_json(monkeypatch, tmp_path):
20
+ monkeypatch.delenv("PRISM_TOKEN", raising=False)
21
+ home = tmp_path
22
+ (home / ".claude.json").write_text(json.dumps({
23
+ "mcpServers": {
24
+ "prism-hosted": {
25
+ "type": "streamable-http",
26
+ "url": "https://gw.example/mcp",
27
+ "headers": {"Authorization": "Bearer from-claude"},
28
+ }
29
+ }
30
+ }))
31
+ monkeypatch.setattr(Path, "home", lambda: home)
32
+ from prism_cli.auth import resolve_token
33
+ assert resolve_token() == "from-claude"
34
+
35
+
36
+ @pytest.mark.unit
37
+ def test_token_missing_raises(monkeypatch, tmp_path):
38
+ monkeypatch.delenv("PRISM_TOKEN", raising=False)
39
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
40
+ from prism_cli.auth import resolve_token, AuthError
41
+ with pytest.raises(AuthError):
42
+ resolve_token()
43
+
44
+
45
+ @pytest.mark.unit
46
+ def test_base_url_from_env(monkeypatch):
47
+ monkeypatch.setenv("PRISM_BASE_URL", "http://custom:8080")
48
+ from prism_cli.auth import resolve_base_url
49
+ assert resolve_base_url() == "http://custom:8080"
50
+
51
+
52
+ @pytest.mark.unit
53
+ def test_base_url_strips_mcp_suffix(monkeypatch, tmp_path):
54
+ monkeypatch.delenv("PRISM_BASE_URL", raising=False)
55
+ home = tmp_path
56
+ (home / ".claude.json").write_text(json.dumps({
57
+ "mcpServers": {
58
+ "prism-hosted": {
59
+ "type": "streamable-http",
60
+ "url": "https://gw.example/mcp",
61
+ "headers": {"Authorization": "Bearer tok"},
62
+ }
63
+ }
64
+ }))
65
+ monkeypatch.setattr(Path, "home", lambda: home)
66
+ from prism_cli.auth import resolve_base_url
67
+ assert resolve_base_url() == "https://gw.example"
68
+
69
+
70
+ @pytest.mark.unit
71
+ def test_base_url_default(monkeypatch, tmp_path):
72
+ monkeypatch.delenv("PRISM_BASE_URL", raising=False)
73
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
74
+ from prism_cli.auth import resolve_base_url
75
+ assert resolve_base_url() == "https://prism-gateway.swisper.app"
@@ -0,0 +1,79 @@
1
+ """Tests for git-state collection and overlay assembly."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+
10
+ @pytest.mark.unit
11
+ def test_collect_git_state_returns_head_and_dirty(tmp_path, monkeypatch):
12
+ monkeypatch.chdir(tmp_path)
13
+ # Build a minimal repo
14
+ subprocess.run(["git", "init", "-q", "."], check=True)
15
+ subprocess.run(["git", "config", "user.email", "t@t"], check=True)
16
+ subprocess.run(["git", "config", "user.name", "t"], check=True)
17
+ (tmp_path / "x.py").write_text("a")
18
+ subprocess.run(["git", "add", "x.py"], check=True)
19
+ subprocess.run(["git", "commit", "-q", "-m", "init"], check=True)
20
+ (tmp_path / "x.py").write_text("changed") # dirty
21
+
22
+ from prism_cli.auto_overlay import collect_git_state
23
+ state = collect_git_state()
24
+ assert len(state["head_sha"]) >= 7
25
+ assert "x.py" in state["dirty_paths"]
26
+
27
+
28
+ @pytest.mark.unit
29
+ def test_build_overlay_reads_dirty_files_within_cap(tmp_path):
30
+ (tmp_path / "x.py").write_text("hello")
31
+ state = {
32
+ "head_sha": "abc", "branch": "main",
33
+ "dirty_paths": ["x.py"], "unpushed_commits": [],
34
+ }
35
+ from prism_cli.auto_overlay import build_overlay
36
+ payload = build_overlay(state, root=tmp_path, max_bytes=1024 * 1024)
37
+ assert payload["working_tree_overlay"]["x.py"] == "hello"
38
+
39
+
40
+ @pytest.mark.unit
41
+ def test_collect_git_state_returns_empty_outside_repo(tmp_path, monkeypatch):
42
+ monkeypatch.chdir(tmp_path)
43
+ from prism_cli.auto_overlay import collect_git_state
44
+ # tmp_path is not a git repo — git rev-parse HEAD should fail
45
+ state = collect_git_state()
46
+ assert state == {}
47
+
48
+
49
+ @pytest.mark.unit
50
+ def test_build_overlay_empty_state_returns_empty():
51
+ from prism_cli.auto_overlay import build_overlay
52
+ assert build_overlay({}) == {}
53
+
54
+
55
+ @pytest.mark.unit
56
+ def test_build_overlay_respects_byte_cap(tmp_path):
57
+ """Files that would exceed the cap are not included."""
58
+ (tmp_path / "big.py").write_text("x" * 100)
59
+ (tmp_path / "small.py").write_text("y" * 10)
60
+ state = {
61
+ "head_sha": "abc", "branch": "main",
62
+ "dirty_paths": ["big.py", "small.py"],
63
+ "unpushed_commits": [],
64
+ }
65
+ from prism_cli.auto_overlay import build_overlay
66
+ # cap at 50 bytes — big.py (100 bytes) must be dropped
67
+ payload = build_overlay(state, root=tmp_path, max_bytes=50)
68
+ assert "big.py" not in payload["working_tree_overlay"]
69
+
70
+
71
+ @pytest.mark.unit
72
+ def test_build_overlay_skips_missing_files(tmp_path):
73
+ state = {
74
+ "head_sha": "abc", "branch": "main",
75
+ "dirty_paths": ["nonexistent.py"], "unpushed_commits": [],
76
+ }
77
+ from prism_cli.auto_overlay import build_overlay
78
+ payload = build_overlay(state, root=tmp_path)
79
+ assert payload["working_tree_overlay"] == {}
@@ -0,0 +1,184 @@
1
+ """End-to-end CLI invocation tests using a fake API client."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+
11
+ @pytest.mark.unit
12
+ def test_cli_help():
13
+ from prism_cli.cli import cli
14
+ runner = CliRunner()
15
+ result = runner.invoke(cli, ["--help"])
16
+ assert result.exit_code == 0
17
+ assert "search" in result.output
18
+ assert "find-refs" in result.output
19
+ assert "grep" in result.output # alias visible
20
+
21
+
22
+ @pytest.mark.unit
23
+ def test_cli_ping_invokes_client():
24
+ from prism_cli import cli as cli_mod
25
+ runner = CliRunner()
26
+
27
+ class FakeClient:
28
+ def __init__(self): pass
29
+ def call(self, sub, payload):
30
+ return {"ok": True, "subcommand": sub, "payload": payload}
31
+ def close(self): pass
32
+
33
+ with patch.object(cli_mod, "PrismClient", FakeClient):
34
+ result = runner.invoke(cli_mod.cli, ["ping"])
35
+ assert result.exit_code == 0
36
+ body = json.loads(result.output)
37
+ assert body["subcommand"] == "ping"
38
+
39
+
40
+ @pytest.mark.unit
41
+ def test_grep_routes_to_search_subcommand():
42
+ from prism_cli import cli as cli_mod
43
+ runner = CliRunner()
44
+
45
+ class FakeClient:
46
+ def __init__(self): self.last = None
47
+ def call(self, sub, payload):
48
+ self.last = (sub, payload)
49
+ return {"ok": True}
50
+ def close(self): pass
51
+
52
+ fake = FakeClient()
53
+ with patch.object(cli_mod, "PrismClient", lambda: fake):
54
+ result = runner.invoke(cli_mod.cli, ["grep", "needle", "--no-overlay"])
55
+ assert result.exit_code == 0
56
+ sub, payload = fake.last
57
+ assert sub == "search"
58
+ assert payload["query"] == "needle"
59
+
60
+
61
+ @pytest.mark.unit
62
+ def test_map_alias_routes_to_module_map():
63
+ from prism_cli import cli as cli_mod
64
+ runner = CliRunner()
65
+
66
+ class FakeClient:
67
+ def __init__(self): self.last = None
68
+ def call(self, sub, payload):
69
+ self.last = (sub, payload)
70
+ return {"ok": True}
71
+ def close(self): pass
72
+
73
+ fake = FakeClient()
74
+ with patch.object(cli_mod, "PrismClient", lambda: fake):
75
+ result = runner.invoke(cli_mod.cli, ["map", ".", "--no-overlay"])
76
+ assert result.exit_code == 0
77
+ sub, payload = fake.last
78
+ assert sub == "module-map"
79
+
80
+
81
+ @pytest.mark.unit
82
+ def test_refs_alias_routes_to_find_refs():
83
+ from prism_cli import cli as cli_mod
84
+ runner = CliRunner()
85
+
86
+ class FakeClient:
87
+ def __init__(self): self.last = None
88
+ def call(self, sub, payload):
89
+ self.last = (sub, payload)
90
+ return {"ok": True}
91
+ def close(self): pass
92
+
93
+ fake = FakeClient()
94
+ with patch.object(cli_mod, "PrismClient", lambda: fake):
95
+ result = runner.invoke(cli_mod.cli, ["refs", "MyClass", "--no-overlay"])
96
+ assert result.exit_code == 0
97
+ sub, payload = fake.last
98
+ assert sub == "find-refs"
99
+ assert payload["symbol"] == "MyClass"
100
+
101
+
102
+ @pytest.mark.unit
103
+ def test_auth_error_exits_1(monkeypatch):
104
+ from prism_cli import cli as cli_mod
105
+ from prism_cli.auth import AuthError
106
+ runner = CliRunner()
107
+
108
+ def raise_auth(*args, **kwargs):
109
+ raise AuthError("no token")
110
+
111
+ with patch.object(cli_mod, "PrismClient", raise_auth):
112
+ result = runner.invoke(cli_mod.cli, ["ping"])
113
+ assert result.exit_code == 1
114
+ assert "auth" in result.output
115
+
116
+
117
+ @pytest.mark.unit
118
+ def test_api_error_exits_2():
119
+ from prism_cli import cli as cli_mod
120
+ from prism_cli.api_client import PrismAPIError
121
+ runner = CliRunner()
122
+
123
+ class FailClient:
124
+ def __init__(self): pass
125
+ def call(self, sub, payload):
126
+ raise PrismAPIError("server exploded")
127
+ def close(self): pass
128
+
129
+ with patch.object(cli_mod, "PrismClient", FailClient):
130
+ result = runner.invoke(cli_mod.cli, ["ping"])
131
+ assert result.exit_code == 2
132
+
133
+
134
+ @pytest.mark.unit
135
+ def test_pretty_flag_indents_output():
136
+ from prism_cli import cli as cli_mod
137
+ runner = CliRunner()
138
+
139
+ class FakeClient:
140
+ def __init__(self): pass
141
+ def call(self, sub, payload):
142
+ return {"key": "value"}
143
+ def close(self): pass
144
+
145
+ with patch.object(cli_mod, "PrismClient", FakeClient):
146
+ result = runner.invoke(cli_mod.cli, ["--pretty", "ping"])
147
+ assert result.exit_code == 0
148
+ # Pretty output has indentation
149
+ assert "\n" in result.output
150
+ body = json.loads(result.output)
151
+ assert body["key"] == "value"
152
+
153
+
154
+ @pytest.mark.unit
155
+ def test_retry_on_overlay_recommended_for():
156
+ """CLI retries once when response contains overlay_recommended_for."""
157
+ from prism_cli import cli as cli_mod
158
+ runner = CliRunner()
159
+
160
+ call_log = []
161
+
162
+ class FakeClient:
163
+ def __init__(self): pass
164
+ def call(self, sub, payload):
165
+ call_log.append((sub, payload.copy()))
166
+ if len(call_log) == 1:
167
+ return {"results": [], "overlay_recommended_for": ["src/foo.py"]}
168
+ return {"results": ["retried"], "overlay_recommended_for": []}
169
+ def close(self): pass
170
+
171
+ # Mock collect_git_state + build_overlay so they return something usable
172
+ with patch.object(cli_mod, "PrismClient", FakeClient), \
173
+ patch.object(cli_mod, "collect_git_state", return_value={
174
+ "head_sha": "abc", "branch": "main",
175
+ "dirty_paths": [], "unpushed_commits": [],
176
+ }), \
177
+ patch.object(cli_mod, "build_overlay", return_value={
178
+ "local_state": {"head_sha": "abc"},
179
+ "working_tree_overlay": {},
180
+ }):
181
+ result = runner.invoke(cli_mod.cli, ["search", "something", "--auto-overlay"])
182
+ assert result.exit_code == 0
183
+ # Should have called the client twice (initial + retry)
184
+ assert len(call_log) == 2
@@ -0,0 +1,56 @@
1
+ """Packaging smoke test — gates the PyPI publish pipeline.
2
+
3
+ Asserts pyproject.toml metadata is publish-ready and `prism --version`
4
+ round-trips with the version declared in pyproject.toml.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ import sys
10
+ import tomllib
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+
16
+ _PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
17
+
18
+
19
+ def _read_pyproject() -> dict:
20
+ return tomllib.loads(_PYPROJECT.read_text())
21
+
22
+
23
+ @pytest.mark.unit
24
+ def test_distribution_name_is_swisper_prism_cli() -> None:
25
+ """PyPI distribution name must be `swisper-prism-cli` (mirrors swisper-studio-sdk)."""
26
+ project = _read_pyproject()["project"]
27
+ assert project["name"] == "swisper-prism-cli", (
28
+ f"Expected name 'swisper-prism-cli' (PyPI), got {project['name']!r}"
29
+ )
30
+
31
+
32
+ @pytest.mark.unit
33
+ def test_pyproject_has_publish_required_fields() -> None:
34
+ """A publishable PyPI package needs name, version, description, authors,
35
+ license, readme, and a Homepage URL. Missing any of these makes
36
+ `twine check` warn or fail."""
37
+ project = _read_pyproject()["project"]
38
+ for field in ("name", "version", "description", "authors", "license", "readme"):
39
+ assert field in project, f"Missing required publish field: {field!r}"
40
+ urls = project.get("urls", {})
41
+ assert urls.get("Homepage"), "Missing [project.urls] Homepage"
42
+
43
+
44
+ @pytest.mark.unit
45
+ def test_prism_version_matches_pyproject() -> None:
46
+ """`prism --version` must print the version declared in pyproject.toml.
47
+ Catches mismatches between the package metadata and the bundled binary."""
48
+ declared = _read_pyproject()["project"]["version"]
49
+ result = subprocess.run(
50
+ [sys.executable, "-m", "prism_cli.cli", "--version"],
51
+ capture_output=True, text=True, check=True,
52
+ )
53
+ assert declared in result.stdout, (
54
+ f"`prism --version` output {result.stdout.strip()!r} does not contain "
55
+ f"declared pyproject version {declared!r}"
56
+ )
@@ -0,0 +1,116 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.13.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "idna" },
11
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
12
+ ]
13
+ sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "certifi"
20
+ version = "2026.4.22"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "click"
29
+ version = "8.3.3"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ dependencies = [
32
+ { name = "colorama", marker = "sys_platform == 'win32'" },
33
+ ]
34
+ sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
35
+ wheels = [
36
+ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
37
+ ]
38
+
39
+ [[package]]
40
+ name = "colorama"
41
+ version = "0.4.6"
42
+ source = { registry = "https://pypi.org/simple" }
43
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
44
+ wheels = [
45
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
46
+ ]
47
+
48
+ [[package]]
49
+ name = "h11"
50
+ version = "0.16.0"
51
+ source = { registry = "https://pypi.org/simple" }
52
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
53
+ wheels = [
54
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "httpcore"
59
+ version = "1.0.9"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ dependencies = [
62
+ { name = "certifi" },
63
+ { name = "h11" },
64
+ ]
65
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
66
+ wheels = [
67
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
68
+ ]
69
+
70
+ [[package]]
71
+ name = "httpx"
72
+ version = "0.28.1"
73
+ source = { registry = "https://pypi.org/simple" }
74
+ dependencies = [
75
+ { name = "anyio" },
76
+ { name = "certifi" },
77
+ { name = "httpcore" },
78
+ { name = "idna" },
79
+ ]
80
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
81
+ wheels = [
82
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
83
+ ]
84
+
85
+ [[package]]
86
+ name = "idna"
87
+ version = "3.13"
88
+ source = { registry = "https://pypi.org/simple" }
89
+ sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "swisper-prism-cli"
96
+ version = "0.1.0"
97
+ source = { editable = "." }
98
+ dependencies = [
99
+ { name = "click" },
100
+ { name = "httpx" },
101
+ ]
102
+
103
+ [package.metadata]
104
+ requires-dist = [
105
+ { name = "click", specifier = ">=8.1" },
106
+ { name = "httpx", specifier = ">=0.27" },
107
+ ]
108
+
109
+ [[package]]
110
+ name = "typing-extensions"
111
+ version = "4.15.0"
112
+ source = { registry = "https://pypi.org/simple" }
113
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
114
+ wheels = [
115
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
116
+ ]