chattermate-cli 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.
@@ -0,0 +1,18 @@
1
+ ChatterMate CLI (chattermate-cli)
2
+ Copyright (C) 2024 ChatterMate
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ The full text of the GNU Affero General Public License v3.0 is available at:
18
+ https://www.gnu.org/licenses/agpl-3.0.txt
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: chattermate-cli
3
+ Version: 0.2.0
4
+ Summary: ChatterMate CLI and MCP server — sign up, authenticate, and configure agents, workflows and knowledge from the terminal or an AI agent.
5
+ Author: ChatterMate
6
+ License: AGPL-3.0-or-later
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: typer>=0.12
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: rich>=13.0
13
+ Requires-Dist: mcp>=1.2
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=8.0; extra == "test"
16
+ Requires-Dist: respx>=0.21; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # ChatterMate CLI & MCP server
20
+
21
+ `chattermate-cli` provides a command-line client and an MCP (Model Context Protocol)
22
+ server for ChatterMate. A human can sign up, log in and configure agents, workflows and
23
+ knowledge from the terminal; an AI agent can do the same through the MCP server.
24
+
25
+ This is an **enterprise** feature and depends on the enterprise backend's Personal Access
26
+ Tokens (PATs).
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install -e . # from this directory, for development
32
+ # or, once published:
33
+ pipx install chattermate-cli
34
+ ```
35
+
36
+ Console scripts installed: `chattermate` (alias `cmate`) and `chattermate-mcp`.
37
+
38
+ ## Configure the API endpoint
39
+
40
+ By default the CLI talks to the hosted API at `https://api.chattermate.chat`. To target a
41
+ local or self-hosted backend, set `CHATTERMATE_API_URL=http://localhost:8000` (or pass
42
+ `--api-url`). Resolution order: `--api-url` flag → `CHATTERMATE_API_URL` env → stored config →
43
+ `https://api.chattermate.chat`.
44
+
45
+ ## Quick start (human)
46
+
47
+ ```bash
48
+ # Create an organization + admin (community signup), or log in to an existing one
49
+ chattermate signup
50
+ chattermate login --email you@acme.com
51
+
52
+ chattermate whoami
53
+
54
+ # Mint a long-lived Personal Access Token for CI / AI agents (shown once!)
55
+ chattermate token create laptop-cli
56
+ chattermate token list
57
+ chattermate token revoke <token-id>
58
+
59
+ # Configure resources
60
+ chattermate agent list
61
+ chattermate agent create --name "Support" --type CUSTOMER_SUPPORT -i "Be concise"
62
+ chattermate workflow get <agent-id>
63
+ chattermate workflow create --agent-id <agent-id> --name "Onboarding"
64
+ chattermate knowledge add-url --website https://docs.acme.com --agent-id <agent-id>
65
+ chattermate knowledge status <queue-id>
66
+ ```
67
+
68
+ Every command accepts `--json` for machine-readable output.
69
+
70
+ ## Authentication
71
+
72
+ Two credentials are supported:
73
+
74
+ - **Personal Access Token** (`cmat_...`) — preferred for non-interactive use. Pass it via
75
+ the `CHATTERMATE_TOKEN` environment variable. Long-lived and revocable.
76
+ - **JWT login** — `chattermate login` stores access + refresh tokens in
77
+ `~/.chattermate/config.json` (mode `600`) and refreshes automatically.
78
+
79
+ ## MCP server (AI agents)
80
+
81
+ `chattermate-mcp` is a stdio MCP server. Point your MCP client at it with a PAT:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "chattermate": {
87
+ "command": "uvx",
88
+ "args": ["--from", "chattermate-cli", "chattermate-mcp"],
89
+ "env": {
90
+ "CHATTERMATE_TOKEN": "cmat_...",
91
+ "CHATTERMATE_API_URL": "https://your-chattermate-host"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ `chattermate token create` prints a ready-to-paste version of this snippet.
99
+
100
+ ### Tools
101
+
102
+ Read-only: `whoami`, `list_agents`, `get_agent`, `get_workflow`, `get_workflow_nodes`,
103
+ `list_knowledge`, `get_ingestion_status`.
104
+
105
+ Mutating: `create_agent`, `update_agent`, `create_workflow`, `update_workflow`,
106
+ `update_workflow_nodes`, `add_knowledge_url`, `link_knowledge`, `unlink_knowledge`.
107
+
108
+ ## Tests
109
+
110
+ ```bash
111
+ pip install -e ".[test]"
112
+ pytest tests/
113
+ ```
@@ -0,0 +1,95 @@
1
+ # ChatterMate CLI & MCP server
2
+
3
+ `chattermate-cli` provides a command-line client and an MCP (Model Context Protocol)
4
+ server for ChatterMate. A human can sign up, log in and configure agents, workflows and
5
+ knowledge from the terminal; an AI agent can do the same through the MCP server.
6
+
7
+ This is an **enterprise** feature and depends on the enterprise backend's Personal Access
8
+ Tokens (PATs).
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install -e . # from this directory, for development
14
+ # or, once published:
15
+ pipx install chattermate-cli
16
+ ```
17
+
18
+ Console scripts installed: `chattermate` (alias `cmate`) and `chattermate-mcp`.
19
+
20
+ ## Configure the API endpoint
21
+
22
+ By default the CLI talks to the hosted API at `https://api.chattermate.chat`. To target a
23
+ local or self-hosted backend, set `CHATTERMATE_API_URL=http://localhost:8000` (or pass
24
+ `--api-url`). Resolution order: `--api-url` flag → `CHATTERMATE_API_URL` env → stored config →
25
+ `https://api.chattermate.chat`.
26
+
27
+ ## Quick start (human)
28
+
29
+ ```bash
30
+ # Create an organization + admin (community signup), or log in to an existing one
31
+ chattermate signup
32
+ chattermate login --email you@acme.com
33
+
34
+ chattermate whoami
35
+
36
+ # Mint a long-lived Personal Access Token for CI / AI agents (shown once!)
37
+ chattermate token create laptop-cli
38
+ chattermate token list
39
+ chattermate token revoke <token-id>
40
+
41
+ # Configure resources
42
+ chattermate agent list
43
+ chattermate agent create --name "Support" --type CUSTOMER_SUPPORT -i "Be concise"
44
+ chattermate workflow get <agent-id>
45
+ chattermate workflow create --agent-id <agent-id> --name "Onboarding"
46
+ chattermate knowledge add-url --website https://docs.acme.com --agent-id <agent-id>
47
+ chattermate knowledge status <queue-id>
48
+ ```
49
+
50
+ Every command accepts `--json` for machine-readable output.
51
+
52
+ ## Authentication
53
+
54
+ Two credentials are supported:
55
+
56
+ - **Personal Access Token** (`cmat_...`) — preferred for non-interactive use. Pass it via
57
+ the `CHATTERMATE_TOKEN` environment variable. Long-lived and revocable.
58
+ - **JWT login** — `chattermate login` stores access + refresh tokens in
59
+ `~/.chattermate/config.json` (mode `600`) and refreshes automatically.
60
+
61
+ ## MCP server (AI agents)
62
+
63
+ `chattermate-mcp` is a stdio MCP server. Point your MCP client at it with a PAT:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "chattermate": {
69
+ "command": "uvx",
70
+ "args": ["--from", "chattermate-cli", "chattermate-mcp"],
71
+ "env": {
72
+ "CHATTERMATE_TOKEN": "cmat_...",
73
+ "CHATTERMATE_API_URL": "https://your-chattermate-host"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ `chattermate token create` prints a ready-to-paste version of this snippet.
81
+
82
+ ### Tools
83
+
84
+ Read-only: `whoami`, `list_agents`, `get_agent`, `get_workflow`, `get_workflow_nodes`,
85
+ `list_knowledge`, `get_ingestion_status`.
86
+
87
+ Mutating: `create_agent`, `update_agent`, `create_workflow`, `update_workflow`,
88
+ `update_workflow_nodes`, `add_knowledge_url`, `link_knowledge`, `unlink_knowledge`.
89
+
90
+ ## Tests
91
+
92
+ ```bash
93
+ pip install -e ".[test]"
94
+ pytest tests/
95
+ ```
@@ -0,0 +1,19 @@
1
+ """
2
+ ChatterMate - CLI & MCP SDK
3
+ Copyright (C) 2024 ChatterMate
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>
17
+ """
18
+
19
+ __version__ = "0.1.0"
@@ -0,0 +1,275 @@
1
+ """
2
+ ChatterMate - API Client
3
+ Copyright (C) 2024 ChatterMate
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>
17
+ """
18
+
19
+ from typing import Any, Callable, Dict, List, Optional
20
+
21
+ import httpx
22
+
23
+ API_PREFIX = "/api/v1"
24
+ PAT_PREFIX = "cmat_"
25
+
26
+
27
+ class ChatterMateError(Exception):
28
+ """Raised when the API returns an error or is unreachable."""
29
+
30
+ def __init__(self, message: str, status_code: Optional[int] = None):
31
+ super().__init__(message)
32
+ self.status_code = status_code
33
+
34
+
35
+ class Client:
36
+ """
37
+ Thin HTTP wrapper around the ChatterMate API.
38
+
39
+ Supports two credentials:
40
+ * a Personal Access Token (``cmat_...``) — preferred for non-interactive use, and
41
+ * a JWT access token from ``login`` (auto-refreshed once on 401 using the refresh token).
42
+
43
+ Used by both the CLI commands and the MCP server.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ api_url: str,
49
+ token: Optional[str] = None,
50
+ refresh_token: Optional[str] = None,
51
+ on_tokens: Optional[Callable[[str, Optional[str]], None]] = None,
52
+ timeout: float = 30.0,
53
+ ):
54
+ self.api_url = api_url.rstrip("/")
55
+ self.token = token
56
+ self.refresh_token = refresh_token
57
+ self.on_tokens = on_tokens
58
+ self._http = httpx.Client(timeout=timeout)
59
+
60
+ # -- internals ---------------------------------------------------------
61
+
62
+ def _is_pat(self) -> bool:
63
+ return bool(self.token and self.token.startswith(PAT_PREFIX))
64
+
65
+ def _auth_headers(self) -> Dict[str, str]:
66
+ return {"Authorization": f"Bearer {self.token}"} if self.token else {}
67
+
68
+ def _refresh(self) -> bool:
69
+ if not self.refresh_token or self._is_pat():
70
+ return False
71
+ try:
72
+ resp = self._http.post(
73
+ f"{self.api_url}{API_PREFIX}/users/refresh",
74
+ cookies={"refresh_token": self.refresh_token},
75
+ )
76
+ except httpx.HTTPError:
77
+ return False
78
+ if resp.status_code != 200:
79
+ return False
80
+ data = resp.json()
81
+ self.token = data.get("access_token", self.token)
82
+ if data.get("refresh_token"):
83
+ self.refresh_token = data["refresh_token"]
84
+ if self.on_tokens:
85
+ self.on_tokens(self.token, self.refresh_token)
86
+ return True
87
+
88
+ @staticmethod
89
+ def _format_error(resp: httpx.Response) -> str:
90
+ try:
91
+ detail = resp.json().get("detail")
92
+ except Exception:
93
+ return resp.text or f"HTTP {resp.status_code}"
94
+ if isinstance(detail, list):
95
+ # FastAPI validation errors: list of {loc, msg, ...} — but tolerate plain strings.
96
+ parts = []
97
+ for item in detail:
98
+ if isinstance(item, dict):
99
+ loc = ".".join(str(p) for p in item.get("loc", []) if p not in ("body",))
100
+ msg = item.get("msg", "")
101
+ parts.append(f"{loc}: {msg}".strip(": "))
102
+ else:
103
+ parts.append(str(item))
104
+ return "; ".join(p for p in parts if p) or f"HTTP {resp.status_code}"
105
+ return str(detail) if detail else f"HTTP {resp.status_code}"
106
+
107
+ def request(
108
+ self,
109
+ method: str,
110
+ path: str,
111
+ *,
112
+ json: Any = None,
113
+ params: Optional[Dict[str, Any]] = None,
114
+ data: Optional[Dict[str, Any]] = None,
115
+ auth: bool = True,
116
+ _retry: bool = True,
117
+ ) -> Any:
118
+ url = f"{self.api_url}{API_PREFIX}{path}"
119
+ headers = self._auth_headers() if auth else {}
120
+ try:
121
+ resp = self._http.request(
122
+ method, url, json=json, params=params, data=data, headers=headers
123
+ )
124
+ except httpx.HTTPError as e:
125
+ raise ChatterMateError(f"Could not reach {self.api_url}: {e}") from e
126
+
127
+ if resp.status_code == 401 and auth and _retry and self._refresh():
128
+ return self.request(
129
+ method, path, json=json, params=params, data=data, auth=auth, _retry=False
130
+ )
131
+
132
+ if resp.status_code >= 400:
133
+ raise ChatterMateError(self._format_error(resp), resp.status_code)
134
+
135
+ if resp.status_code == 204 or not resp.content:
136
+ return None
137
+ ctype = resp.headers.get("content-type", "")
138
+ return resp.json() if "application/json" in ctype else resp.text
139
+
140
+ # -- auth --------------------------------------------------------------
141
+
142
+ def login(self, email: str, password: str) -> Dict[str, Any]:
143
+ data = self.request(
144
+ "POST", "/users/login",
145
+ data={"username": email, "password": password}, auth=False,
146
+ )
147
+ self.token = data.get("access_token")
148
+ self.refresh_token = data.get("refresh_token")
149
+ return data
150
+
151
+ def signup(
152
+ self, name: str, domain: str, admin_email: str, admin_name: str,
153
+ admin_password: str, timezone: str = "UTC",
154
+ ) -> Dict[str, Any]:
155
+ body = {
156
+ "name": name, "domain": domain, "timezone": timezone,
157
+ "admin_email": admin_email, "admin_name": admin_name,
158
+ "admin_password": admin_password,
159
+ }
160
+ data = self.request("POST", "/organizations", json=body, auth=False)
161
+ if isinstance(data, dict) and data.get("access_token"):
162
+ self.token = data.get("access_token")
163
+ self.refresh_token = data.get("refresh_token")
164
+ return data
165
+
166
+ def signup_request_otp(self, email: str) -> Dict[str, Any]:
167
+ """Enterprise signup step 1: request an email OTP (multi-org instances)."""
168
+ return self.request(
169
+ "POST", "/enterprise/signup/verify-email", json={"email": email}, auth=False
170
+ )
171
+
172
+ def signup_verify_otp(
173
+ self, email: str, otp: str, admin_name: str, admin_password: str,
174
+ organization_name: str, domain: str, colorcode: Optional[str] = None,
175
+ ) -> Dict[str, Any]:
176
+ """Enterprise signup step 2: verify OTP and create the org + admin."""
177
+ body = {
178
+ "email": email, "otp": otp, "name": admin_name, "password": admin_password,
179
+ "organization_name": organization_name, "domain": domain,
180
+ }
181
+ if colorcode:
182
+ body["colorcode"] = colorcode
183
+ data = self.request("POST", "/enterprise/signup/verify-otp", json=body, auth=False)
184
+ if isinstance(data, dict) and data.get("access_token"):
185
+ self.token = data.get("access_token")
186
+ self.refresh_token = data.get("refresh_token")
187
+ return data
188
+
189
+ def whoami(self) -> Dict[str, Any]:
190
+ # No dedicated GET /me endpoint exists; an empty PATCH is a safe no-op that
191
+ # returns the authenticated user.
192
+ return self.request("PATCH", "/users/me", json={})
193
+
194
+ # -- personal access tokens -------------------------------------------
195
+
196
+ def create_pat(
197
+ self, name: str, scopes: Optional[List[str]] = None,
198
+ expires_in_days: Optional[int] = None,
199
+ ) -> Dict[str, Any]:
200
+ body: Dict[str, Any] = {"name": name}
201
+ if scopes:
202
+ body["scopes"] = scopes
203
+ if expires_in_days is not None:
204
+ body["expires_in_days"] = expires_in_days
205
+ return self.request("POST", "/enterprise/tokens", json=body)
206
+
207
+ def list_pats(self) -> List[Dict[str, Any]]:
208
+ return self.request("GET", "/enterprise/tokens")
209
+
210
+ def revoke_pat(self, token_id: str) -> None:
211
+ return self.request("DELETE", f"/enterprise/tokens/{token_id}")
212
+
213
+ # -- agents ------------------------------------------------------------
214
+
215
+ def list_agents(self) -> Any:
216
+ return self.request("GET", "/agent/list")
217
+
218
+ def get_agent(self, agent_id: str) -> Any:
219
+ return self.request("GET", f"/agent/{agent_id}")
220
+
221
+ def create_agent(self, payload: Dict[str, Any]) -> Any:
222
+ return self.request("POST", "/agent", json=payload)
223
+
224
+ def update_agent(self, agent_id: str, payload: Dict[str, Any]) -> Any:
225
+ return self.request("PUT", f"/agent/{agent_id}", json=payload)
226
+
227
+ # -- workflows ---------------------------------------------------------
228
+
229
+ def get_workflow_for_agent(self, agent_id: str) -> Any:
230
+ return self.request("GET", f"/workflow/agent/{agent_id}")
231
+
232
+ def create_workflow(self, payload: Dict[str, Any]) -> Any:
233
+ return self.request("POST", "/workflow", json=payload)
234
+
235
+ def update_workflow(self, workflow_id: str, payload: Dict[str, Any]) -> Any:
236
+ return self.request("PUT", f"/workflow/{workflow_id}", json=payload)
237
+
238
+ def get_workflow_nodes(self, workflow_id: str) -> Any:
239
+ return self.request("GET", f"/workflow/{workflow_id}/nodes")
240
+
241
+ def update_workflow_nodes(self, workflow_id: str, payload: Dict[str, Any]) -> Any:
242
+ return self.request("PUT", f"/workflow/{workflow_id}/nodes", json=payload)
243
+
244
+ # -- knowledge ---------------------------------------------------------
245
+
246
+ def add_knowledge(
247
+ self, org_id: str, pdf_urls: Optional[List[str]] = None,
248
+ websites: Optional[List[str]] = None, agent_id: Optional[str] = None,
249
+ ) -> Any:
250
+ body: Dict[str, Any] = {
251
+ "org_id": org_id,
252
+ "pdf_urls": pdf_urls or [],
253
+ "websites": websites or [],
254
+ }
255
+ if agent_id:
256
+ body["agent_id"] = agent_id
257
+ return self.request("POST", "/knowledge/add/urls", json=body)
258
+
259
+ def list_knowledge_for_agent(self, agent_id: str) -> Any:
260
+ return self.request("GET", f"/knowledge/agent/{agent_id}")
261
+
262
+ def link_knowledge(self, knowledge_id: int, agent_id: str) -> Any:
263
+ return self.request(
264
+ "POST", "/knowledge/link",
265
+ params={"knowledge_id": knowledge_id, "agent_id": agent_id},
266
+ )
267
+
268
+ def unlink_knowledge(self, knowledge_id: int, agent_id: str) -> Any:
269
+ return self.request(
270
+ "DELETE", "/knowledge/unlink",
271
+ params={"knowledge_id": knowledge_id, "agent_id": agent_id},
272
+ )
273
+
274
+ def knowledge_queue_status(self, queue_id: int) -> Any:
275
+ return self.request("GET", f"/knowledge/queue/{queue_id}")
@@ -0,0 +1,17 @@
1
+ """
2
+ ChatterMate - CLI Commands
3
+ Copyright (C) 2024 ChatterMate
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>
17
+ """
@@ -0,0 +1,122 @@
1
+ """
2
+ ChatterMate - CLI Agent Commands
3
+ Copyright (C) 2024 ChatterMate
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>
17
+ """
18
+
19
+ from typing import List, Optional
20
+
21
+ import typer
22
+ from rich.table import Table
23
+
24
+ from .. import config
25
+ from ..context import console, get_client, output, print_error, run
26
+
27
+ agent_app = typer.Typer(no_args_is_help=True, help="Create and manage AI agents.")
28
+
29
+
30
+ @agent_app.command("list")
31
+ def list_agents(as_json: bool = typer.Option(False, "--json", help="Output raw JSON.")):
32
+ """List the organization's agents."""
33
+ client = get_client()
34
+ data = run(client.list_agents)
35
+
36
+ def render(rows):
37
+ table = Table(title="Agents")
38
+ for col in ("id", "name", "type", "active"):
39
+ table.add_column(col)
40
+ for r in rows or []:
41
+ table.add_row(
42
+ str(r.get("id")), r.get("display_name") or r.get("name", ""),
43
+ str(r.get("agent_type", "")), "yes" if r.get("is_active") else "no",
44
+ )
45
+ console.print(table)
46
+
47
+ output(data, as_json, render)
48
+
49
+
50
+ @agent_app.command("get")
51
+ def get_agent(
52
+ agent_id: str = typer.Argument(..., help="Agent id."),
53
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
54
+ ):
55
+ """Show a single agent."""
56
+ client = get_client()
57
+ output(run(lambda: client.get_agent(agent_id)), as_json)
58
+
59
+
60
+ @agent_app.command("create")
61
+ def create_agent(
62
+ name: str = typer.Option(..., "--name", help="Agent name."),
63
+ agent_type: str = typer.Option(
64
+ "custom", "--type",
65
+ help="Agent type: customer_support, sales, tech_support, general, custom (case-insensitive).",
66
+ ),
67
+ instruction: List[str] = typer.Option(
68
+ [], "--instruction", "-i", help="An instruction line (repeatable)."
69
+ ),
70
+ description: Optional[str] = typer.Option(None, "--description"),
71
+ display_name: Optional[str] = typer.Option(None, "--display-name"),
72
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
73
+ ):
74
+ """Create an agent. Organization is taken from your stored session."""
75
+ org_id = config.load_config().get("organization_id")
76
+ if not org_id:
77
+ print_error("No organization in session. Run 'chattermate login' or 'whoami' first.")
78
+ raise typer.Exit(code=1)
79
+ payload = {
80
+ "name": name,
81
+ # API enum values are lowercase (customer_support, sales, ...); normalize for convenience.
82
+ "agent_type": agent_type.lower(),
83
+ "instructions": list(instruction) or [f"You are {name}, a helpful assistant."],
84
+ "organization_id": org_id,
85
+ }
86
+ if description is not None:
87
+ payload["description"] = description
88
+ if display_name is not None:
89
+ payload["display_name"] = display_name
90
+ client = get_client()
91
+ data = run(lambda: client.create_agent(payload))
92
+ if not as_json:
93
+ console.print(f"[green]Created agent[/green] {data.get('id')} ({name})")
94
+ output(data, as_json)
95
+
96
+
97
+ @agent_app.command("update")
98
+ def update_agent(
99
+ agent_id: str = typer.Argument(..., help="Agent id."),
100
+ instruction: List[str] = typer.Option(
101
+ [], "--instruction", "-i", help="Replace instructions (repeatable)."
102
+ ),
103
+ display_name: Optional[str] = typer.Option(None, "--display-name"),
104
+ active: Optional[bool] = typer.Option(None, "--active/--inactive"),
105
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
106
+ ):
107
+ """Update an agent's instructions, display name, or active state."""
108
+ payload: dict = {}
109
+ if instruction:
110
+ payload["instructions"] = list(instruction)
111
+ if display_name is not None:
112
+ payload["display_name"] = display_name
113
+ if active is not None:
114
+ payload["is_active"] = active
115
+ if not payload:
116
+ print_error("Nothing to update. Pass --instruction, --display-name or --active/--inactive.")
117
+ raise typer.Exit(code=1)
118
+ client = get_client()
119
+ data = run(lambda: client.update_agent(agent_id, payload))
120
+ if not as_json:
121
+ console.print(f"[green]Updated agent[/green] {agent_id}")
122
+ output(data, as_json)