jabb-cli 0.1.0__tar.gz → 0.2.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.
- jabb_cli-0.2.0/LICENSE +21 -0
- jabb_cli-0.2.0/PKG-INFO +160 -0
- jabb_cli-0.2.0/README.md +135 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli/cli.py +6 -0
- jabb_cli-0.2.0/jabb_cli/mcp_server.py +156 -0
- jabb_cli-0.2.0/jabb_cli.egg-info/PKG-INFO +160 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli.egg-info/SOURCES.txt +4 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli.egg-info/requires.txt +1 -0
- jabb_cli-0.2.0/pyproject.toml +35 -0
- jabb_cli-0.2.0/tests/test_mcp_entry.py +28 -0
- jabb_cli-0.2.0/tests/test_mcp_tools.py +134 -0
- jabb_cli-0.1.0/PKG-INFO +0 -9
- jabb_cli-0.1.0/README.md +0 -69
- jabb_cli-0.1.0/jabb_cli.egg-info/PKG-INFO +0 -9
- jabb_cli-0.1.0/pyproject.toml +0 -19
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli/__init__.py +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli/client.py +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli/output.py +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli.egg-info/dependency_links.txt +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli.egg-info/entry_points.txt +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/jabb_cli.egg-info/top_level.txt +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/setup.cfg +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/tests/test_cli_errors.py +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/tests/test_cli_resources.py +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/tests/test_client.py +0 -0
- {jabb_cli-0.1.0 → jabb_cli-0.2.0}/tests/test_output.py +0 -0
jabb_cli-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JABB (JAB-Bers)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jabb_cli-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jabb-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
|
|
5
|
+
Author: JABB
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JAB-Bers/jabb-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/JAB-Bers/jabb-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/JAB-Bers/jabb-cli/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: click>=8.1
|
|
20
|
+
Requires-Dist: requests>=2.31
|
|
21
|
+
Requires-Dist: mcp<2,>=1.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# jabb
|
|
27
|
+
|
|
28
|
+
CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export JABB_API_KEY=jabb_test_...
|
|
40
|
+
jabb locations list
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Time to first result: under 5 minutes from a minted key.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
|
|
52
|
+
|
|
53
|
+
- `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
|
|
54
|
+
- `--detailed` — request more fields per row (costs more tokens/bandwidth).
|
|
55
|
+
- `--cursor` — pass the previous response's `next_cursor` to page forward.
|
|
56
|
+
- `--limit` — rows per page, 1-100, default 20.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
jabb scores list --detailed
|
|
62
|
+
jabb reviews list --limit 5
|
|
63
|
+
jabb pnif list --json
|
|
64
|
+
jabb locations list --cursor eyJwIjo... --limit 50
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Auth
|
|
68
|
+
|
|
69
|
+
Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
|
|
70
|
+
|
|
71
|
+
For local development against a jabb-server instance running on your machine:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export JABB_API_BASE_URL=http://127.0.0.1:5500
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
|
|
78
|
+
|
|
79
|
+
## Exit codes
|
|
80
|
+
|
|
81
|
+
| Code | Meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| 0 | Success |
|
|
84
|
+
| 1 | Generic API error |
|
|
85
|
+
| 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
|
|
86
|
+
| 3 | Rate limited (message includes retry-after seconds) |
|
|
87
|
+
|
|
88
|
+
## MCP server (agent integration)
|
|
89
|
+
|
|
90
|
+
`jabb mcp` starts a **stdio MCP server** that exposes the same 5 resources as agent tools, so any MCP host (Claude Code, Claude Desktop, Cursor…) can query your JABB data with nothing but an API key.
|
|
91
|
+
|
|
92
|
+
### Tools exposed
|
|
93
|
+
|
|
94
|
+
| Tool | When to call |
|
|
95
|
+
|------|-------------|
|
|
96
|
+
| `jabb_list_locations` | User asks about branches/stores or needs location IDs |
|
|
97
|
+
| `jabb_get_cx_scores` | User wants quantitative CX quality metrics |
|
|
98
|
+
| `jabb_list_quick_jabbs` | User wants individual evaluation content / verbatim feedback |
|
|
99
|
+
| `jabb_get_pnif` | User asks for Price/Need/Impact/Frequency breakdown |
|
|
100
|
+
| `jabb_get_sentiment` | User asks for sentiment labels or topic-level trends |
|
|
101
|
+
|
|
102
|
+
All tools accept `cursor`, `limit`, `response_format` (`"concise"` | `"detailed"`). `jabb_get_pnif` and `jabb_get_sentiment` also accept `language` (ISO 639-1: `en`/`fr`/`ar`).
|
|
103
|
+
|
|
104
|
+
Responses are capped at ~25 KB. When truncated, the payload includes `"truncated": true` and preserves the original `next_cursor` so the agent can page forward.
|
|
105
|
+
|
|
106
|
+
API errors are returned as tool errors carrying the API's `code` + `hint` verbatim. 429 errors also include `Retry-After` seconds.
|
|
107
|
+
|
|
108
|
+
### Connect from Claude Code
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# One-time setup (project scope):
|
|
112
|
+
claude mcp add jabb -- jabb mcp
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or add a project-scope `.mcp.json` at your repo root:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"jabb": {
|
|
121
|
+
"command": "jabb",
|
|
122
|
+
"args": ["mcp"],
|
|
123
|
+
"env": {
|
|
124
|
+
"JABB_API_KEY": "${JABB_API_KEY}"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"jabb": {
|
|
137
|
+
"command": "jabb",
|
|
138
|
+
"args": ["mcp"],
|
|
139
|
+
"env": {
|
|
140
|
+
"JABB_API_KEY": "jabb_live_..."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Environment variables
|
|
148
|
+
|
|
149
|
+
| Variable | Required | Default | Description |
|
|
150
|
+
|----------|----------|---------|-------------|
|
|
151
|
+
| `JABB_API_KEY` | Yes | — | API key minted by your JABB admin |
|
|
152
|
+
| `JABB_API_BASE_URL` | No | `https://api.jabb.cx` | Override for local dev |
|
|
153
|
+
|
|
154
|
+
## For Claude Code users
|
|
155
|
+
|
|
156
|
+
See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data. The MCP server (`jabb mcp`) is an alternative path that works with any MCP-capable host without the skill.
|
|
157
|
+
|
|
158
|
+
## Scope
|
|
159
|
+
|
|
160
|
+
Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
|
jabb_cli-0.2.0/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# jabb
|
|
2
|
+
|
|
3
|
+
CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export JABB_API_KEY=jabb_test_...
|
|
15
|
+
jabb locations list
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Time to first result: under 5 minutes from a minted key.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
|
|
27
|
+
|
|
28
|
+
- `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
|
|
29
|
+
- `--detailed` — request more fields per row (costs more tokens/bandwidth).
|
|
30
|
+
- `--cursor` — pass the previous response's `next_cursor` to page forward.
|
|
31
|
+
- `--limit` — rows per page, 1-100, default 20.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
jabb scores list --detailed
|
|
37
|
+
jabb reviews list --limit 5
|
|
38
|
+
jabb pnif list --json
|
|
39
|
+
jabb locations list --cursor eyJwIjo... --limit 50
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Auth
|
|
43
|
+
|
|
44
|
+
Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
|
|
45
|
+
|
|
46
|
+
For local development against a jabb-server instance running on your machine:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export JABB_API_BASE_URL=http://127.0.0.1:5500
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
|
|
53
|
+
|
|
54
|
+
## Exit codes
|
|
55
|
+
|
|
56
|
+
| Code | Meaning |
|
|
57
|
+
|---|---|
|
|
58
|
+
| 0 | Success |
|
|
59
|
+
| 1 | Generic API error |
|
|
60
|
+
| 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
|
|
61
|
+
| 3 | Rate limited (message includes retry-after seconds) |
|
|
62
|
+
|
|
63
|
+
## MCP server (agent integration)
|
|
64
|
+
|
|
65
|
+
`jabb mcp` starts a **stdio MCP server** that exposes the same 5 resources as agent tools, so any MCP host (Claude Code, Claude Desktop, Cursor…) can query your JABB data with nothing but an API key.
|
|
66
|
+
|
|
67
|
+
### Tools exposed
|
|
68
|
+
|
|
69
|
+
| Tool | When to call |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `jabb_list_locations` | User asks about branches/stores or needs location IDs |
|
|
72
|
+
| `jabb_get_cx_scores` | User wants quantitative CX quality metrics |
|
|
73
|
+
| `jabb_list_quick_jabbs` | User wants individual evaluation content / verbatim feedback |
|
|
74
|
+
| `jabb_get_pnif` | User asks for Price/Need/Impact/Frequency breakdown |
|
|
75
|
+
| `jabb_get_sentiment` | User asks for sentiment labels or topic-level trends |
|
|
76
|
+
|
|
77
|
+
All tools accept `cursor`, `limit`, `response_format` (`"concise"` | `"detailed"`). `jabb_get_pnif` and `jabb_get_sentiment` also accept `language` (ISO 639-1: `en`/`fr`/`ar`).
|
|
78
|
+
|
|
79
|
+
Responses are capped at ~25 KB. When truncated, the payload includes `"truncated": true` and preserves the original `next_cursor` so the agent can page forward.
|
|
80
|
+
|
|
81
|
+
API errors are returned as tool errors carrying the API's `code` + `hint` verbatim. 429 errors also include `Retry-After` seconds.
|
|
82
|
+
|
|
83
|
+
### Connect from Claude Code
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# One-time setup (project scope):
|
|
87
|
+
claude mcp add jabb -- jabb mcp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or add a project-scope `.mcp.json` at your repo root:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"jabb": {
|
|
96
|
+
"command": "jabb",
|
|
97
|
+
"args": ["mcp"],
|
|
98
|
+
"env": {
|
|
99
|
+
"JABB_API_KEY": "${JABB_API_KEY}"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"jabb": {
|
|
112
|
+
"command": "jabb",
|
|
113
|
+
"args": ["mcp"],
|
|
114
|
+
"env": {
|
|
115
|
+
"JABB_API_KEY": "jabb_live_..."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Environment variables
|
|
123
|
+
|
|
124
|
+
| Variable | Required | Default | Description |
|
|
125
|
+
|----------|----------|---------|-------------|
|
|
126
|
+
| `JABB_API_KEY` | Yes | — | API key minted by your JABB admin |
|
|
127
|
+
| `JABB_API_BASE_URL` | No | `https://api.jabb.cx` | Override for local dev |
|
|
128
|
+
|
|
129
|
+
## For Claude Code users
|
|
130
|
+
|
|
131
|
+
See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data. The MCP server (`jabb mcp`) is an alternative path that works with any MCP-capable host without the skill.
|
|
132
|
+
|
|
133
|
+
## Scope
|
|
134
|
+
|
|
135
|
+
Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
|
|
@@ -75,3 +75,9 @@ def _make_group(name, path, help_text):
|
|
|
75
75
|
|
|
76
76
|
for _name, (_path, _help) in RESOURCES.items():
|
|
77
77
|
main.add_command(_make_group(_name, _path, _help))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@main.command(name="mcp", help="Start a stdio MCP server exposing JABB Public API v1 as agent tools.")
|
|
81
|
+
def mcp_serve():
|
|
82
|
+
from jabb_cli.mcp_server import serve
|
|
83
|
+
serve()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""stdio MCP server — exposes JABB Public API v1 as agent tools via FastMCP."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
from jabb_cli.client import ApiClient, ApiError, AuthError, RateLimitError
|
|
8
|
+
from jabb_cli.cli import RESOURCES
|
|
9
|
+
|
|
10
|
+
_MAX_RESPONSE_BYTES = 25_000 # ~25K tokens guard
|
|
11
|
+
|
|
12
|
+
mcp = FastMCP("jabb")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client() -> ApiClient:
|
|
16
|
+
"""Instantiate ApiClient; raises AuthError on missing key (propagated as tool error)."""
|
|
17
|
+
return ApiClient()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _call(path: str, params: dict) -> dict:
|
|
21
|
+
return _client().get(path, params=params)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _truncate(payload: dict) -> dict:
|
|
25
|
+
"""Cap payload at _MAX_RESPONSE_BYTES. Sets truncated+next_cursor when trimmed."""
|
|
26
|
+
raw = json.dumps(payload)
|
|
27
|
+
if len(raw.encode()) <= _MAX_RESPONSE_BYTES:
|
|
28
|
+
return payload
|
|
29
|
+
data = payload.get("data", [])
|
|
30
|
+
if isinstance(data, list):
|
|
31
|
+
# Binary-search for the largest prefix that fits
|
|
32
|
+
lo, hi = 0, len(data)
|
|
33
|
+
while lo < hi:
|
|
34
|
+
mid = (lo + hi + 1) // 2
|
|
35
|
+
candidate = dict(payload, data=data[:mid], truncated=True)
|
|
36
|
+
if len(json.dumps(candidate).encode()) <= _MAX_RESPONSE_BYTES:
|
|
37
|
+
lo = mid
|
|
38
|
+
else:
|
|
39
|
+
hi = mid - 1
|
|
40
|
+
return dict(payload, data=data[:lo], truncated=True)
|
|
41
|
+
# Non-list data: return as-is with truncation flag (can't safely trim objects)
|
|
42
|
+
return dict(payload, truncated=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _tool_error(e: ApiError) -> str:
|
|
46
|
+
parts = [f"[{e.code}] {e.message}"]
|
|
47
|
+
if e.hint:
|
|
48
|
+
parts.append(f"Hint: {e.hint}")
|
|
49
|
+
if isinstance(e, RateLimitError):
|
|
50
|
+
parts.append(f"Retry-After: {e.retry_after}s")
|
|
51
|
+
return " | ".join(parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _list_params(
|
|
55
|
+
cursor: Optional[str],
|
|
56
|
+
limit: int,
|
|
57
|
+
response_format: Literal["concise", "detailed"],
|
|
58
|
+
extra: Optional[dict] = None,
|
|
59
|
+
) -> dict:
|
|
60
|
+
p: dict = {"limit": limit, "response_format": response_format}
|
|
61
|
+
if cursor:
|
|
62
|
+
p["cursor"] = cursor
|
|
63
|
+
if extra:
|
|
64
|
+
p.update(extra)
|
|
65
|
+
return p
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def jabb_list_locations(
|
|
70
|
+
cursor: Optional[str] = None,
|
|
71
|
+
limit: int = 20,
|
|
72
|
+
response_format: Literal["concise", "detailed"] = "concise",
|
|
73
|
+
) -> dict:
|
|
74
|
+
"""Use when the user asks about physical branches/stores of the business, needs a
|
|
75
|
+
store/venue list, or requires location IDs to cross-reference other resources.
|
|
76
|
+
Returns paginated list of business locations. Use concise unless field depth is needed."""
|
|
77
|
+
try:
|
|
78
|
+
result = _call(RESOURCES["locations"][0], _list_params(cursor, limit, response_format))
|
|
79
|
+
return _truncate(result)
|
|
80
|
+
except ApiError as e:
|
|
81
|
+
raise ValueError(_tool_error(e)) from e
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@mcp.tool()
|
|
85
|
+
def jabb_get_cx_scores(
|
|
86
|
+
cursor: Optional[str] = None,
|
|
87
|
+
limit: int = 20,
|
|
88
|
+
response_format: Literal["concise", "detailed"] = "concise",
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""Use when the user asks for quantitative CX quality metrics, scores, or ratings per
|
|
91
|
+
entity. Do NOT use for raw review text — use jabb_list_quick_jabbs instead."""
|
|
92
|
+
try:
|
|
93
|
+
result = _call(RESOURCES["scores"][0], _list_params(cursor, limit, response_format))
|
|
94
|
+
return _truncate(result)
|
|
95
|
+
except ApiError as e:
|
|
96
|
+
raise ValueError(_tool_error(e)) from e
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@mcp.tool()
|
|
100
|
+
def jabb_list_quick_jabbs(
|
|
101
|
+
cursor: Optional[str] = None,
|
|
102
|
+
limit: int = 20,
|
|
103
|
+
response_format: Literal["concise", "detailed"] = "concise",
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""Use when the user asks about individual customer evaluations, review content, or
|
|
106
|
+
specific ratings with verbatim feedback. Do NOT use for aggregate scores — use
|
|
107
|
+
jabb_get_cx_scores instead."""
|
|
108
|
+
try:
|
|
109
|
+
result = _call(RESOURCES["reviews"][0], _list_params(cursor, limit, response_format))
|
|
110
|
+
return _truncate(result)
|
|
111
|
+
except ApiError as e:
|
|
112
|
+
raise ValueError(_tool_error(e)) from e
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
def jabb_get_pnif(
|
|
117
|
+
language: str = "en",
|
|
118
|
+
cursor: Optional[str] = None,
|
|
119
|
+
limit: int = 20,
|
|
120
|
+
response_format: Literal["concise", "detailed"] = "concise",
|
|
121
|
+
) -> dict:
|
|
122
|
+
"""Use when the user asks for the Price/Need/Impact/Frequency analysis breakdown.
|
|
123
|
+
Do NOT use for raw scores or review text. language: ISO 639-1 code (en/fr/ar)."""
|
|
124
|
+
try:
|
|
125
|
+
extra = {"language": language}
|
|
126
|
+
result = _call(RESOURCES["pnif"][0], _list_params(cursor, limit, response_format, extra))
|
|
127
|
+
return _truncate(result)
|
|
128
|
+
except ApiError as e:
|
|
129
|
+
raise ValueError(_tool_error(e)) from e
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
def jabb_get_sentiment(
|
|
134
|
+
language: str = "en",
|
|
135
|
+
cursor: Optional[str] = None,
|
|
136
|
+
limit: int = 20,
|
|
137
|
+
response_format: Literal["concise", "detailed"] = "concise",
|
|
138
|
+
) -> dict:
|
|
139
|
+
"""Use when the user asks for sentiment labels, trends, or topic-level sentiment mapping.
|
|
140
|
+
Do NOT use for raw scores or PNIF breakdown. language: ISO 639-1 code (en/fr/ar)."""
|
|
141
|
+
try:
|
|
142
|
+
extra = {"language": language}
|
|
143
|
+
result = _call(RESOURCES["sentiment"][0], _list_params(cursor, limit, response_format, extra))
|
|
144
|
+
return _truncate(result)
|
|
145
|
+
except ApiError as e:
|
|
146
|
+
raise ValueError(_tool_error(e)) from e
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def serve() -> None:
|
|
150
|
+
"""Entry point: validate key then run stdio MCP server."""
|
|
151
|
+
try:
|
|
152
|
+
ApiClient() # fail-fast: raises AuthError if JABB_API_KEY missing
|
|
153
|
+
except AuthError as e:
|
|
154
|
+
print(f"[jabb-mcp] {e.message} {e.hint}", file=sys.stderr)
|
|
155
|
+
sys.exit(2)
|
|
156
|
+
mcp.run(transport="stdio")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jabb-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
|
|
5
|
+
Author: JABB
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JAB-Bers/jabb-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/JAB-Bers/jabb-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/JAB-Bers/jabb-cli/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: click>=8.1
|
|
20
|
+
Requires-Dist: requests>=2.31
|
|
21
|
+
Requires-Dist: mcp<2,>=1.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# jabb
|
|
27
|
+
|
|
28
|
+
CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export JABB_API_KEY=jabb_test_...
|
|
40
|
+
jabb locations list
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Time to first result: under 5 minutes from a minted key.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
|
|
52
|
+
|
|
53
|
+
- `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
|
|
54
|
+
- `--detailed` — request more fields per row (costs more tokens/bandwidth).
|
|
55
|
+
- `--cursor` — pass the previous response's `next_cursor` to page forward.
|
|
56
|
+
- `--limit` — rows per page, 1-100, default 20.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
jabb scores list --detailed
|
|
62
|
+
jabb reviews list --limit 5
|
|
63
|
+
jabb pnif list --json
|
|
64
|
+
jabb locations list --cursor eyJwIjo... --limit 50
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Auth
|
|
68
|
+
|
|
69
|
+
Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
|
|
70
|
+
|
|
71
|
+
For local development against a jabb-server instance running on your machine:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export JABB_API_BASE_URL=http://127.0.0.1:5500
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
|
|
78
|
+
|
|
79
|
+
## Exit codes
|
|
80
|
+
|
|
81
|
+
| Code | Meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| 0 | Success |
|
|
84
|
+
| 1 | Generic API error |
|
|
85
|
+
| 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
|
|
86
|
+
| 3 | Rate limited (message includes retry-after seconds) |
|
|
87
|
+
|
|
88
|
+
## MCP server (agent integration)
|
|
89
|
+
|
|
90
|
+
`jabb mcp` starts a **stdio MCP server** that exposes the same 5 resources as agent tools, so any MCP host (Claude Code, Claude Desktop, Cursor…) can query your JABB data with nothing but an API key.
|
|
91
|
+
|
|
92
|
+
### Tools exposed
|
|
93
|
+
|
|
94
|
+
| Tool | When to call |
|
|
95
|
+
|------|-------------|
|
|
96
|
+
| `jabb_list_locations` | User asks about branches/stores or needs location IDs |
|
|
97
|
+
| `jabb_get_cx_scores` | User wants quantitative CX quality metrics |
|
|
98
|
+
| `jabb_list_quick_jabbs` | User wants individual evaluation content / verbatim feedback |
|
|
99
|
+
| `jabb_get_pnif` | User asks for Price/Need/Impact/Frequency breakdown |
|
|
100
|
+
| `jabb_get_sentiment` | User asks for sentiment labels or topic-level trends |
|
|
101
|
+
|
|
102
|
+
All tools accept `cursor`, `limit`, `response_format` (`"concise"` | `"detailed"`). `jabb_get_pnif` and `jabb_get_sentiment` also accept `language` (ISO 639-1: `en`/`fr`/`ar`).
|
|
103
|
+
|
|
104
|
+
Responses are capped at ~25 KB. When truncated, the payload includes `"truncated": true` and preserves the original `next_cursor` so the agent can page forward.
|
|
105
|
+
|
|
106
|
+
API errors are returned as tool errors carrying the API's `code` + `hint` verbatim. 429 errors also include `Retry-After` seconds.
|
|
107
|
+
|
|
108
|
+
### Connect from Claude Code
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# One-time setup (project scope):
|
|
112
|
+
claude mcp add jabb -- jabb mcp
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or add a project-scope `.mcp.json` at your repo root:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"jabb": {
|
|
121
|
+
"command": "jabb",
|
|
122
|
+
"args": ["mcp"],
|
|
123
|
+
"env": {
|
|
124
|
+
"JABB_API_KEY": "${JABB_API_KEY}"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"jabb": {
|
|
137
|
+
"command": "jabb",
|
|
138
|
+
"args": ["mcp"],
|
|
139
|
+
"env": {
|
|
140
|
+
"JABB_API_KEY": "jabb_live_..."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Environment variables
|
|
148
|
+
|
|
149
|
+
| Variable | Required | Default | Description |
|
|
150
|
+
|----------|----------|---------|-------------|
|
|
151
|
+
| `JABB_API_KEY` | Yes | — | API key minted by your JABB admin |
|
|
152
|
+
| `JABB_API_BASE_URL` | No | `https://api.jabb.cx` | Override for local dev |
|
|
153
|
+
|
|
154
|
+
## For Claude Code users
|
|
155
|
+
|
|
156
|
+
See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data. The MCP server (`jabb mcp`) is an alternative path that works with any MCP-capable host without the skill.
|
|
157
|
+
|
|
158
|
+
## Scope
|
|
159
|
+
|
|
160
|
+
Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
jabb_cli/__init__.py
|
|
4
5
|
jabb_cli/cli.py
|
|
5
6
|
jabb_cli/client.py
|
|
7
|
+
jabb_cli/mcp_server.py
|
|
6
8
|
jabb_cli/output.py
|
|
7
9
|
jabb_cli.egg-info/PKG-INFO
|
|
8
10
|
jabb_cli.egg-info/SOURCES.txt
|
|
@@ -13,4 +15,6 @@ jabb_cli.egg-info/top_level.txt
|
|
|
13
15
|
tests/test_cli_errors.py
|
|
14
16
|
tests/test_cli_resources.py
|
|
15
17
|
tests/test_client.py
|
|
18
|
+
tests/test_mcp_entry.py
|
|
19
|
+
tests/test_mcp_tools.py
|
|
16
20
|
tests/test_output.py
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jabb-cli"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment."
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
authors = [{ name = "JABB" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
]
|
|
17
|
+
dependencies = ["click>=8.1", "requests>=2.31", "mcp>=1.0,<2"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/JAB-Bers/jabb-cli"
|
|
21
|
+
Repository = "https://github.com/JAB-Bers/jabb-cli"
|
|
22
|
+
Issues = "https://github.com/JAB-Bers/jabb-cli/issues"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = ["pytest>=8.0"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
jabb = "jabb_cli.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
include = ["jabb_cli*"]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["setuptools>=68"]
|
|
35
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Task 1 tests: mcp subcommand wired + missing-key fail-fast."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
from jabb_cli.cli import main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_mcp_subcommand_registered():
|
|
9
|
+
"""jabb mcp --help must exit 0 (subcommand is registered)."""
|
|
10
|
+
result = CliRunner().invoke(main, ["mcp", "--help"])
|
|
11
|
+
assert result.exit_code == 0
|
|
12
|
+
assert "stdio" in result.output.lower() or "mcp" in result.output.lower()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_mcp_missing_key_exits_nonzero(monkeypatch):
|
|
16
|
+
"""jabb mcp without JABB_API_KEY must exit 2 and print a message to stderr."""
|
|
17
|
+
monkeypatch.delenv("JABB_API_KEY", raising=False)
|
|
18
|
+
with patch("jabb_cli.mcp_server.mcp.run"): # prevent actual stdio server start
|
|
19
|
+
result = CliRunner().invoke(main, ["mcp"])
|
|
20
|
+
assert result.exit_code == 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_mcp_with_key_calls_serve(monkeypatch):
|
|
24
|
+
"""jabb mcp with a valid key proceeds to mcp.run (no early exit)."""
|
|
25
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
26
|
+
with patch("jabb_cli.mcp_server.mcp.run") as mock_run:
|
|
27
|
+
CliRunner().invoke(main, ["mcp"])
|
|
28
|
+
mock_run.assert_called_once_with(transport="stdio")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Task 2 tests: MCP tool path mapping, param forwarding, concise/detailed, error mapping, truncation."""
|
|
2
|
+
import json
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
from jabb_cli.mcp_server import (
|
|
6
|
+
jabb_list_locations,
|
|
7
|
+
jabb_get_cx_scores,
|
|
8
|
+
jabb_list_quick_jabbs,
|
|
9
|
+
jabb_get_pnif,
|
|
10
|
+
jabb_get_sentiment,
|
|
11
|
+
_truncate,
|
|
12
|
+
_MAX_RESPONSE_BYTES,
|
|
13
|
+
)
|
|
14
|
+
from jabb_cli.client import ApiError, AuthError, RateLimitError
|
|
15
|
+
from jabb_cli.cli import RESOURCES
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── path mapping ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
TOOL_PATH_CASES = [
|
|
21
|
+
(jabb_list_locations, "locations"),
|
|
22
|
+
(jabb_get_cx_scores, "scores"),
|
|
23
|
+
(jabb_list_quick_jabbs, "reviews"),
|
|
24
|
+
(jabb_get_pnif, "pnif"),
|
|
25
|
+
(jabb_get_sentiment, "sentiment"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("tool_fn,resource_key", TOOL_PATH_CASES)
|
|
30
|
+
def test_tool_calls_correct_path(monkeypatch, tool_fn, resource_key):
|
|
31
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
32
|
+
expected_path = RESOURCES[resource_key][0]
|
|
33
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": [], "next_cursor": None}) as m:
|
|
34
|
+
tool_fn()
|
|
35
|
+
assert m.call_args.args[0] == expected_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── param forwarding ──────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
def test_cursor_and_limit_forwarded(monkeypatch):
|
|
41
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
42
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": [], "next_cursor": None}) as m:
|
|
43
|
+
jabb_list_locations(cursor="tok_abc", limit=5)
|
|
44
|
+
params = m.call_args.kwargs["params"]
|
|
45
|
+
assert params["cursor"] == "tok_abc"
|
|
46
|
+
assert params["limit"] == 5
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_language_forwarded_pnif(monkeypatch):
|
|
50
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
51
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": {}, "next_cursor": None}) as m:
|
|
52
|
+
jabb_get_pnif(language="ar")
|
|
53
|
+
assert m.call_args.kwargs["params"]["language"] == "ar"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_language_forwarded_sentiment(monkeypatch):
|
|
57
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
58
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": {}, "next_cursor": None}) as m:
|
|
59
|
+
jabb_get_sentiment(language="fr")
|
|
60
|
+
assert m.call_args.kwargs["params"]["language"] == "fr"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── concise vs detailed ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("fmt", ["concise", "detailed"])
|
|
66
|
+
def test_response_format_forwarded(monkeypatch, fmt):
|
|
67
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
68
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": [], "next_cursor": None}) as m:
|
|
69
|
+
jabb_list_locations(response_format=fmt)
|
|
70
|
+
assert m.call_args.kwargs["params"]["response_format"] == fmt
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── error mapping ─────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def test_api_error_raises_value_error_with_hint(monkeypatch):
|
|
76
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
77
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", side_effect=ApiError("not_found", "Resource not found", "Check the entity ID.")):
|
|
78
|
+
with pytest.raises(ValueError) as exc:
|
|
79
|
+
jabb_list_locations()
|
|
80
|
+
assert "not_found" in str(exc.value)
|
|
81
|
+
assert "Check the entity ID." in str(exc.value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_rate_limit_error_includes_retry_after(monkeypatch):
|
|
85
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
86
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", side_effect=RateLimitError("rate_limited", "Too many requests", "Slow down.", 42)):
|
|
87
|
+
with pytest.raises(ValueError) as exc:
|
|
88
|
+
jabb_list_locations()
|
|
89
|
+
assert "42" in str(exc.value)
|
|
90
|
+
assert "Retry-After" in str(exc.value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_auth_error_propagated(monkeypatch):
|
|
94
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
95
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", side_effect=AuthError("forbidden", "Invalid key.", "Regenerate key in JABB admin.")):
|
|
96
|
+
with pytest.raises(ValueError) as exc:
|
|
97
|
+
jabb_list_locations()
|
|
98
|
+
assert "forbidden" in str(exc.value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── 25K truncation ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def test_truncate_small_payload_unchanged():
|
|
104
|
+
payload = {"data": [{"id": "1"}], "next_cursor": None}
|
|
105
|
+
assert _truncate(payload) == payload
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_truncate_large_list_sets_truncated_flag():
|
|
109
|
+
big_item = {"id": "x", "description": "a" * 500}
|
|
110
|
+
items = [big_item] * 200 # well over 25K
|
|
111
|
+
payload = {"data": items, "next_cursor": "tok_next"}
|
|
112
|
+
result = _truncate(payload)
|
|
113
|
+
assert result["truncated"] is True
|
|
114
|
+
assert len(result["data"]) < 200
|
|
115
|
+
assert len(json.dumps(result).encode()) <= _MAX_RESPONSE_BYTES
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_truncate_sets_truncated_and_next_cursor_survives(monkeypatch):
|
|
119
|
+
monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
|
|
120
|
+
big_item = {"id": "x", "description": "a" * 500}
|
|
121
|
+
items = [big_item] * 200
|
|
122
|
+
large_response = {"data": items, "next_cursor": "tok_next"}
|
|
123
|
+
with patch("jabb_cli.mcp_server.ApiClient.get", return_value=large_response):
|
|
124
|
+
result = jabb_list_locations()
|
|
125
|
+
assert result["truncated"] is True
|
|
126
|
+
assert result["next_cursor"] == "tok_next"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_truncate_non_list_data_flag_only():
|
|
130
|
+
big_obj = {"key": "v" * 30_000}
|
|
131
|
+
payload = {"data": big_obj, "next_cursor": None}
|
|
132
|
+
result = _truncate(payload)
|
|
133
|
+
assert result["truncated"] is True
|
|
134
|
+
assert result["data"] == big_obj # object preserved, can't trim safely
|
jabb_cli-0.1.0/PKG-INFO
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: jabb-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: click>=8.1
|
|
7
|
-
Requires-Dist: requests>=2.31
|
|
8
|
-
Provides-Extra: dev
|
|
9
|
-
Requires-Dist: pytest>=8.0; extra == "dev"
|
jabb_cli-0.1.0/README.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# jabb
|
|
2
|
-
|
|
3
|
-
CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install -e .
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quickstart
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
export JABB_API_KEY=jabb_test_...
|
|
15
|
-
jabb locations list
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Time to first result: under 5 minutes from a minted key.
|
|
19
|
-
|
|
20
|
-
## Usage
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
|
|
27
|
-
|
|
28
|
-
- `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
|
|
29
|
-
- `--detailed` — request more fields per row (costs more tokens/bandwidth).
|
|
30
|
-
- `--cursor` — pass the previous response's `next_cursor` to page forward.
|
|
31
|
-
- `--limit` — rows per page, 1-100, default 20.
|
|
32
|
-
|
|
33
|
-
Examples:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
jabb scores list --detailed
|
|
37
|
-
jabb reviews list --limit 5
|
|
38
|
-
jabb pnif list --json
|
|
39
|
-
jabb locations list --cursor eyJwIjo... --limit 50
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Auth
|
|
43
|
-
|
|
44
|
-
Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
|
|
45
|
-
|
|
46
|
-
For local development against a jabb-server instance running on your machine:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
export JABB_API_BASE_URL=http://127.0.0.1:5500
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
|
|
53
|
-
|
|
54
|
-
## Exit codes
|
|
55
|
-
|
|
56
|
-
| Code | Meaning |
|
|
57
|
-
|---|---|
|
|
58
|
-
| 0 | Success |
|
|
59
|
-
| 1 | Generic API error |
|
|
60
|
-
| 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
|
|
61
|
-
| 3 | Rate limited (message includes retry-after seconds) |
|
|
62
|
-
|
|
63
|
-
## For Claude Code users
|
|
64
|
-
|
|
65
|
-
See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data.
|
|
66
|
-
|
|
67
|
-
## Scope
|
|
68
|
-
|
|
69
|
-
Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: jabb-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: click>=8.1
|
|
7
|
-
Requires-Dist: requests>=2.31
|
|
8
|
-
Provides-Extra: dev
|
|
9
|
-
Requires-Dist: pytest>=8.0; extra == "dev"
|
jabb_cli-0.1.0/pyproject.toml
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "jabb-cli"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
description = "CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment."
|
|
5
|
-
requires-python = ">=3.11"
|
|
6
|
-
dependencies = ["click>=8.1", "requests>=2.31"]
|
|
7
|
-
|
|
8
|
-
[project.optional-dependencies]
|
|
9
|
-
dev = ["pytest>=8.0"]
|
|
10
|
-
|
|
11
|
-
[project.scripts]
|
|
12
|
-
jabb = "jabb_cli.cli:main"
|
|
13
|
-
|
|
14
|
-
[tool.setuptools.packages.find]
|
|
15
|
-
include = ["jabb_cli*"]
|
|
16
|
-
|
|
17
|
-
[build-system]
|
|
18
|
-
requires = ["setuptools>=68"]
|
|
19
|
-
build-backend = "setuptools.build_meta"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|