moss-cli 0.1.1__tar.gz → 0.2.1__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.
Files changed (32) hide show
  1. {moss_cli-0.1.1/src/moss_cli.egg-info → moss_cli-0.2.1}/PKG-INFO +69 -24
  2. {moss_cli-0.1.1 → moss_cli-0.2.1}/README.md +58 -14
  3. {moss_cli-0.1.1 → moss_cli-0.2.1}/pyproject.toml +12 -11
  4. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/__init__.py +1 -1
  5. moss_cli-0.2.1/src/moss_cli/api.py +106 -0
  6. moss_cli-0.2.1/src/moss_cli/commands/auth.py +249 -0
  7. moss_cli-0.2.1/src/moss_cli/commands/common.py +29 -0
  8. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/init_cmd.py +3 -2
  9. moss_cli-0.2.1/src/moss_cli/commands/keys.py +90 -0
  10. moss_cli-0.2.1/src/moss_cli/commands/projects.py +37 -0
  11. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/search.py +4 -13
  12. moss_cli-0.2.1/src/moss_cli/config.py +135 -0
  13. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/main.py +19 -1
  14. {moss_cli-0.1.1 → moss_cli-0.2.1/src/moss_cli.egg-info}/PKG-INFO +69 -24
  15. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/SOURCES.txt +7 -1
  16. moss_cli-0.2.1/src/moss_cli.egg-info/requires.txt +12 -0
  17. moss_cli-0.2.1/tests/test_login_and_keys.py +204 -0
  18. moss_cli-0.1.1/src/moss_cli/config.py +0 -57
  19. moss_cli-0.1.1/src/moss_cli.egg-info/requires.txt +0 -12
  20. {moss_cli-0.1.1 → moss_cli-0.2.1}/LICENSE +0 -0
  21. {moss_cli-0.1.1 → moss_cli-0.2.1}/setup.cfg +0 -0
  22. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/__init__.py +0 -0
  23. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/doc.py +0 -0
  24. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/index.py +0 -0
  25. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/job.py +0 -0
  26. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/version.py +0 -0
  27. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/documents.py +0 -0
  28. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/job_waiter.py +0 -0
  29. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/output.py +0 -0
  30. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/dependency_links.txt +0 -0
  31. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/entry_points.txt +0 -0
  32. {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/top_level.txt +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moss-cli
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: Command-line interface for Moss semantic search
5
5
  Author-email: "InferEdge Inc." <contact@usemoss.dev>
6
+ License-Expression: LicenseRef-PolyForm-Shield-1.0.0
6
7
  Keywords: search,semantic,cli,moss
7
8
  Classifier: Development Status :: 3 - Alpha
8
9
  Classifier: Intended Audience :: Developers
@@ -16,23 +17,23 @@ Requires-Python: >=3.10
16
17
  Description-Content-Type: text/markdown
17
18
  License-File: LICENSE
18
19
  Requires-Dist: moss>=1.0.0
19
- Requires-Dist: typer>=0.9.0
20
- Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: typer>=0.26.8
21
+ Requires-Dist: rich>=15.0.0
21
22
  Provides-Extra: dev
22
- Requires-Dist: pytest>=8.0.0; extra == "dev"
23
- Requires-Dist: black>=24.0.0; extra == "dev"
24
- Requires-Dist: isort>=5.0.0; extra == "dev"
25
- Requires-Dist: flake8>=7.0.0; extra == "dev"
26
- Requires-Dist: mypy>=1.0.0; extra == "dev"
27
- Requires-Dist: build>=1.0.0; extra == "dev"
28
- Requires-Dist: twine>=5.0.0; extra == "dev"
23
+ Requires-Dist: pytest>=9.1.1; extra == "dev"
24
+ Requires-Dist: black<26,>=25.9.0; extra == "dev"
25
+ Requires-Dist: isort<8,>=7.0.0; extra == "dev"
26
+ Requires-Dist: flake8>=7.3.0; extra == "dev"
27
+ Requires-Dist: mypy>=2.1.0; extra == "dev"
28
+ Requires-Dist: build>=1.5.0; extra == "dev"
29
+ Requires-Dist: twine>=6.2.0; extra == "dev"
29
30
  Dynamic: license-file
30
31
 
31
32
  # Moss CLI
32
33
 
33
34
  **Command-line interface for Moss semantic search** — manage indexes, documents, and queries directly from your terminal.
34
35
 
35
- Moss CLI wraps the [Moss Python SDK](https://docs.usemoss.dev/) so you can build and query semantic search indexes without writing any code. Ideal for quick prototyping, scripting, CI/CD pipelines, and data workflows.
36
+ Moss CLI wraps the [Moss Python SDK](https://docs.moss.dev/) so you can build and query semantic search indexes without writing any code. Ideal for quick prototyping, scripting, CI/CD pipelines, and data workflows.
36
37
 
37
38
  ## Features
38
39
 
@@ -40,8 +41,9 @@ Moss CLI wraps the [Moss Python SDK](https://docs.usemoss.dev/) so you can build
40
41
  - **Semantic search** — query indexes with configurable hybrid search (semantic + keyword)
41
42
  - **Index management** — create, list, inspect, and delete indexes
42
43
  - **Document management** — add, update, retrieve, and delete documents
43
- - **Local by default** — downloads indexes for on-device queries, `--cloud` to skip
44
+ - **Local by default** — downloads indexes and queries them on-device
44
45
  - **Flexible auth** — CLI flags, environment variables, or config file
46
+ - **Log in from the terminal** — `moss login`, then create project keys with `moss keys create`
45
47
  - **Multiple output formats** — rich tables for humans, `--json` for scripts
46
48
  - **Job tracking** — poll background jobs with live progress display
47
49
  - **Pipe-friendly** — stdin/stdout support for composing with other tools
@@ -66,9 +68,6 @@ moss index create my-index -f docs.json --wait
66
68
 
67
69
  # 4. Search it
68
70
  moss query my-index "what is machine learning"
69
-
70
- # 5. Search via cloud API (skips local download)
71
- moss query my-index "neural networks" --cloud
72
71
  ```
73
72
 
74
73
  ## Authentication
@@ -88,9 +87,56 @@ export MOSS_PROJECT_ID="your-project-id"
88
87
  export MOSS_PROJECT_KEY="your-project-key"
89
88
 
90
89
  # Option 3: Inline flags
91
- moss index list --project-id "..." --project-key "..."
90
+ moss --project-id "..." --project-key "..." index list
91
+ ```
92
+
93
+ ## Log in and create keys
94
+
95
+ `moss login` connects the CLI to your Moss account, so you can create project keys without opening the dashboard.
96
+
97
+ ```bash
98
+ # Opens the browser. Check the code matches, then click Approve.
99
+ moss login
100
+
101
+ moss whoami
102
+ moss projects list
103
+
104
+ # Prints the key once, plus MOSS_PROJECT_ID / MOSS_PROJECT_KEY lines
105
+ moss keys create --project <project-id> --name "prod server"
106
+ moss keys list --project <project-id>
107
+ moss keys delete --project <project-id> <key-id>
108
+
109
+ # Revoke the token and remove it from ~/.moss/config.json
110
+ moss logout
111
+ ```
112
+
113
+ The token is saved to `~/.moss/config.json`. It lives until you run `moss logout` or revoke it in Settings > API Keys. It acts as you: only workspace owners, admins and a project's owner can create or delete its keys. `--project` defaults to the project saved by `moss init`.
114
+
115
+ Use `moss login --no-browser` on a machine without a browser (for example over SSH) and open the printed link anywhere.
116
+
117
+ ## Agents and CI
118
+
119
+ CI that only queries or uploads to one project needs a project key, not a login. Create one with `moss keys create` and store it as `MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY`.
120
+
121
+ Agents that create or delete keys need a CLI token. Make a separate one for each agent, so logging in or out on your machine doesn't break it:
122
+
123
+ ```bash
124
+ # Approve in the browser; the token is printed, not saved
125
+ moss login --print-token --name "deploy agent"
126
+ ```
127
+
128
+ Store it as `MOSS_TOKEN` in your secret store, then:
129
+
130
+ ```bash
131
+ export MOSS_TOKEN="mossc_..."
132
+ moss keys create --project <project-id> --json
133
+ # {"project_id": "...", "key_id": "...", "secret": "moss_..."}
92
134
  ```
93
135
 
136
+ The token acts as you, so it loses access to a workspace when you do. Revoke it in Settings > API Keys.
137
+
138
+ The token is resolved from `--token`, then `MOSS_TOKEN`, then the config file. The saved token is only sent to the dashboard that issued it, so with `--api-url` pointing elsewhere, log in there or pass `--token`. A `--token` or `MOSS_TOKEN` goes to production unless `--api-url` / `MOSS_API_URL` says otherwise.
139
+
94
140
  ## Commands
95
141
 
96
142
  ### Index Management
@@ -141,9 +187,6 @@ moss query my-index "what is deep learning"
141
187
  # Tune results: more results, keyword-heavy
142
188
  moss query my-index "neural networks" --top-k 20 --alpha 0.3
143
189
 
144
- # Cloud mode (skip download, query via cloud API)
145
- moss query my-index "transformers" --cloud
146
-
147
190
  # With metadata filter (local only)
148
191
  moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq": "footwear"}}'
149
192
 
@@ -151,7 +194,7 @@ moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq":
151
194
  echo "what is AI" | moss query my-index
152
195
 
153
196
  # JSON output for scripting
154
- moss query my-index "query" --json | jq '.docs[0].text'
197
+ moss --json query my-index "query" | jq '.docs[0].text'
155
198
  ```
156
199
 
157
200
  ### Job Tracking
@@ -170,9 +213,9 @@ moss job status <job-id> --wait
170
213
  # Print version info
171
214
  moss version
172
215
 
173
- # Global JSON output
174
- moss index list --json
175
- moss doc get my-index --json
216
+ # Global JSON output, before the command
217
+ moss --json index list
218
+ moss --json doc get my-index
176
219
  ```
177
220
 
178
221
  ## Document File Format
@@ -211,6 +254,8 @@ cat docs.json | moss doc add my-index -f -
211
254
  |---|---|---|
212
255
  | `--project-id` | `-p` | Project ID (overrides env/config) |
213
256
  | `--project-key` | | Project key (overrides env/config) |
257
+ | `--token` | | CLI token from `moss login` (or `MOSS_TOKEN`) |
258
+ | `--api-url` | | Dashboard URL, for staging or local testing (or `MOSS_API_URL`) |
214
259
  | `--json` | | Machine-readable JSON output |
215
260
  | `--verbose` | `-v` | Enable debug logging |
216
261
 
@@ -226,4 +271,4 @@ cat docs.json | moss doc add my-index -f -
226
271
 
227
272
  Copyright (c) 2026 InferEdge Inc. — PolyForm Shield License 1.0.0.
228
273
 
229
- See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@usemoss.dev.
274
+ See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@moss.dev.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Command-line interface for Moss semantic search** — manage indexes, documents, and queries directly from your terminal.
4
4
 
5
- Moss CLI wraps the [Moss Python SDK](https://docs.usemoss.dev/) so you can build and query semantic search indexes without writing any code. Ideal for quick prototyping, scripting, CI/CD pipelines, and data workflows.
5
+ Moss CLI wraps the [Moss Python SDK](https://docs.moss.dev/) so you can build and query semantic search indexes without writing any code. Ideal for quick prototyping, scripting, CI/CD pipelines, and data workflows.
6
6
 
7
7
  ## Features
8
8
 
@@ -10,8 +10,9 @@ Moss CLI wraps the [Moss Python SDK](https://docs.usemoss.dev/) so you can build
10
10
  - **Semantic search** — query indexes with configurable hybrid search (semantic + keyword)
11
11
  - **Index management** — create, list, inspect, and delete indexes
12
12
  - **Document management** — add, update, retrieve, and delete documents
13
- - **Local by default** — downloads indexes for on-device queries, `--cloud` to skip
13
+ - **Local by default** — downloads indexes and queries them on-device
14
14
  - **Flexible auth** — CLI flags, environment variables, or config file
15
+ - **Log in from the terminal** — `moss login`, then create project keys with `moss keys create`
15
16
  - **Multiple output formats** — rich tables for humans, `--json` for scripts
16
17
  - **Job tracking** — poll background jobs with live progress display
17
18
  - **Pipe-friendly** — stdin/stdout support for composing with other tools
@@ -36,9 +37,6 @@ moss index create my-index -f docs.json --wait
36
37
 
37
38
  # 4. Search it
38
39
  moss query my-index "what is machine learning"
39
-
40
- # 5. Search via cloud API (skips local download)
41
- moss query my-index "neural networks" --cloud
42
40
  ```
43
41
 
44
42
  ## Authentication
@@ -58,9 +56,56 @@ export MOSS_PROJECT_ID="your-project-id"
58
56
  export MOSS_PROJECT_KEY="your-project-key"
59
57
 
60
58
  # Option 3: Inline flags
61
- moss index list --project-id "..." --project-key "..."
59
+ moss --project-id "..." --project-key "..." index list
60
+ ```
61
+
62
+ ## Log in and create keys
63
+
64
+ `moss login` connects the CLI to your Moss account, so you can create project keys without opening the dashboard.
65
+
66
+ ```bash
67
+ # Opens the browser. Check the code matches, then click Approve.
68
+ moss login
69
+
70
+ moss whoami
71
+ moss projects list
72
+
73
+ # Prints the key once, plus MOSS_PROJECT_ID / MOSS_PROJECT_KEY lines
74
+ moss keys create --project <project-id> --name "prod server"
75
+ moss keys list --project <project-id>
76
+ moss keys delete --project <project-id> <key-id>
77
+
78
+ # Revoke the token and remove it from ~/.moss/config.json
79
+ moss logout
80
+ ```
81
+
82
+ The token is saved to `~/.moss/config.json`. It lives until you run `moss logout` or revoke it in Settings > API Keys. It acts as you: only workspace owners, admins and a project's owner can create or delete its keys. `--project` defaults to the project saved by `moss init`.
83
+
84
+ Use `moss login --no-browser` on a machine without a browser (for example over SSH) and open the printed link anywhere.
85
+
86
+ ## Agents and CI
87
+
88
+ CI that only queries or uploads to one project needs a project key, not a login. Create one with `moss keys create` and store it as `MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY`.
89
+
90
+ Agents that create or delete keys need a CLI token. Make a separate one for each agent, so logging in or out on your machine doesn't break it:
91
+
92
+ ```bash
93
+ # Approve in the browser; the token is printed, not saved
94
+ moss login --print-token --name "deploy agent"
95
+ ```
96
+
97
+ Store it as `MOSS_TOKEN` in your secret store, then:
98
+
99
+ ```bash
100
+ export MOSS_TOKEN="mossc_..."
101
+ moss keys create --project <project-id> --json
102
+ # {"project_id": "...", "key_id": "...", "secret": "moss_..."}
62
103
  ```
63
104
 
105
+ The token acts as you, so it loses access to a workspace when you do. Revoke it in Settings > API Keys.
106
+
107
+ The token is resolved from `--token`, then `MOSS_TOKEN`, then the config file. The saved token is only sent to the dashboard that issued it, so with `--api-url` pointing elsewhere, log in there or pass `--token`. A `--token` or `MOSS_TOKEN` goes to production unless `--api-url` / `MOSS_API_URL` says otherwise.
108
+
64
109
  ## Commands
65
110
 
66
111
  ### Index Management
@@ -111,9 +156,6 @@ moss query my-index "what is deep learning"
111
156
  # Tune results: more results, keyword-heavy
112
157
  moss query my-index "neural networks" --top-k 20 --alpha 0.3
113
158
 
114
- # Cloud mode (skip download, query via cloud API)
115
- moss query my-index "transformers" --cloud
116
-
117
159
  # With metadata filter (local only)
118
160
  moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq": "footwear"}}'
119
161
 
@@ -121,7 +163,7 @@ moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq":
121
163
  echo "what is AI" | moss query my-index
122
164
 
123
165
  # JSON output for scripting
124
- moss query my-index "query" --json | jq '.docs[0].text'
166
+ moss --json query my-index "query" | jq '.docs[0].text'
125
167
  ```
126
168
 
127
169
  ### Job Tracking
@@ -140,9 +182,9 @@ moss job status <job-id> --wait
140
182
  # Print version info
141
183
  moss version
142
184
 
143
- # Global JSON output
144
- moss index list --json
145
- moss doc get my-index --json
185
+ # Global JSON output, before the command
186
+ moss --json index list
187
+ moss --json doc get my-index
146
188
  ```
147
189
 
148
190
  ## Document File Format
@@ -181,6 +223,8 @@ cat docs.json | moss doc add my-index -f -
181
223
  |---|---|---|
182
224
  | `--project-id` | `-p` | Project ID (overrides env/config) |
183
225
  | `--project-key` | | Project key (overrides env/config) |
226
+ | `--token` | | CLI token from `moss login` (or `MOSS_TOKEN`) |
227
+ | `--api-url` | | Dashboard URL, for staging or local testing (or `MOSS_API_URL`) |
184
228
  | `--json` | | Machine-readable JSON output |
185
229
  | `--verbose` | `-v` | Enable debug logging |
186
230
 
@@ -196,4 +240,4 @@ cat docs.json | moss doc add my-index -f -
196
240
 
197
241
  Copyright (c) 2026 InferEdge Inc. — PolyForm Shield License 1.0.0.
198
242
 
199
- See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@usemoss.dev.
243
+ See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@moss.dev.
@@ -1,13 +1,14 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "moss-cli"
7
- version = "0.1.1"
7
+ version = "0.2.1"
8
8
  description = "Command-line interface for Moss semantic search"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
+ license = "LicenseRef-PolyForm-Shield-1.0.0"
11
12
  license-files = ["LICENSE"]
12
13
  authors = [
13
14
  { name = "InferEdge Inc.", email = "contact@usemoss.dev" }
@@ -25,19 +26,19 @@ classifiers = [
25
26
  ]
26
27
  dependencies = [
27
28
  "moss>=1.0.0",
28
- "typer>=0.9.0",
29
- "rich>=13.0.0",
29
+ "typer>=0.26.8",
30
+ "rich>=15.0.0",
30
31
  ]
31
32
 
32
33
  [project.optional-dependencies]
33
34
  dev = [
34
- "pytest>=8.0.0",
35
- "black>=24.0.0",
36
- "isort>=5.0.0",
37
- "flake8>=7.0.0",
38
- "mypy>=1.0.0",
39
- "build>=1.0.0",
40
- "twine>=5.0.0",
35
+ "pytest>=9.1.1",
36
+ "black>=25.9.0,<26",
37
+ "isort>=7.0.0,<8",
38
+ "flake8>=7.3.0",
39
+ "mypy>=2.1.0",
40
+ "build>=1.5.0",
41
+ "twine>=6.2.0",
41
42
  ]
42
43
 
43
44
  [project.scripts]
@@ -1,3 +1,3 @@
1
1
  """Moss CLI - Command-line interface for Moss semantic search."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.2.1"
@@ -0,0 +1,106 @@
1
+ """HTTP client for the dashboard's CLI routes (login, projects, keys)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Any, Dict, Optional, Tuple
9
+
10
+ import moss_cli
11
+
12
+
13
+ class ApiError(Exception):
14
+ def __init__(self, status: int, message: str, code: Optional[str] = None) -> None:
15
+ super().__init__(message)
16
+ self.status = status
17
+ self.code = code
18
+
19
+
20
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
21
+ # urllib copies the Authorization header onto a redirect, even to another host.
22
+ def redirect_request(self, *args: Any, **kwargs: Any) -> None:
23
+ return None
24
+
25
+
26
+ _opener = urllib.request.build_opener(_NoRedirect)
27
+
28
+
29
+ def request(
30
+ api_url: str,
31
+ method: str,
32
+ path: str,
33
+ token: Optional[str] = None,
34
+ body: Optional[Dict[str, Any]] = None,
35
+ ) -> Tuple[int, Dict[str, Any]]:
36
+ """Send a request and return (status, json body). Never raises on an HTTP error status.
37
+
38
+ Raises ApiError only when the server doesn't answer like a Moss dashboard with CLI
39
+ login: a redirect or an HTML page. That's the sign of an old dashboard or a wrong
40
+ --api-url, not a transient failure, so a retry loop must not swallow it.
41
+ """
42
+ data = json.dumps(body).encode() if body is not None else None
43
+ headers = {
44
+ "Accept": "application/json",
45
+ "User-Agent": f"moss-cli/{moss_cli.__version__}",
46
+ }
47
+ if data is not None:
48
+ headers["Content-Type"] = "application/json"
49
+ if token:
50
+ headers["Authorization"] = f"Bearer {token}"
51
+ req = urllib.request.Request(
52
+ api_url.rstrip("/") + path, data=data, headers=headers, method=method
53
+ )
54
+ try:
55
+ with _opener.open(req, timeout=30) as res:
56
+ status, content_type, raw = (
57
+ res.status,
58
+ res.headers.get("Content-Type"),
59
+ res.read(),
60
+ )
61
+ except urllib.error.HTTPError as e:
62
+ status, content_type, raw = e.code, e.headers.get("Content-Type"), e.read()
63
+
64
+ # An old dashboard answers these routes with a redirect to the login page, and a wrong
65
+ # --api-url may answer with HTML. A 5xx or 429 may be a gateway page, which callers
66
+ # retry, so it stays a plain status.
67
+ retryable = status >= 500 or status == 429
68
+ if 300 <= status < 400 or (not retryable and raw and not _is_json(content_type)):
69
+ raise ApiError(
70
+ status,
71
+ f"{api_url} did not answer like a Moss dashboard with CLI login (HTTP {status}). "
72
+ "Check --api-url / MOSS_API_URL, or ask for the dashboard to be updated.",
73
+ )
74
+ return status, _parse(raw)
75
+
76
+
77
+ def call(
78
+ api_url: str,
79
+ method: str,
80
+ path: str,
81
+ token: Optional[str] = None,
82
+ body: Optional[Dict[str, Any]] = None,
83
+ ) -> Dict[str, Any]:
84
+ """Like request(), but raises ApiError for any non-2xx status."""
85
+ status, payload = request(api_url, method, path, token, body)
86
+ if status >= 400:
87
+ message = payload.get("error") or f"Request failed ({status})"
88
+ if status == 401 and token:
89
+ message = (
90
+ "Your CLI token is invalid or revoked. Run 'moss login' again, or replace "
91
+ "MOSS_TOKEN / --token with a new one from 'moss login --print-token'."
92
+ )
93
+ raise ApiError(status, message, payload.get("code"))
94
+ return payload
95
+
96
+
97
+ def _is_json(content_type: Optional[str]) -> bool:
98
+ return "json" in (content_type or "").lower()
99
+
100
+
101
+ def _parse(raw: bytes) -> Dict[str, Any]:
102
+ try:
103
+ parsed = json.loads(raw or b"{}")
104
+ except json.JSONDecodeError:
105
+ return {}
106
+ return parsed if isinstance(parsed, dict) else {}