chattermate-cli 0.2.4__tar.gz → 0.3.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.
Files changed (27) hide show
  1. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/PKG-INFO +1 -1
  2. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/__init__.py +1 -1
  3. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/client.py +29 -3
  4. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/commands/knowledge.py +62 -4
  5. chattermate_cli-0.3.0/chattermate_cli/commands/widget.py +63 -0
  6. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/main.py +2 -0
  7. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli.egg-info/PKG-INFO +1 -1
  8. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli.egg-info/SOURCES.txt +4 -1
  9. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/pyproject.toml +1 -1
  10. chattermate_cli-0.3.0/tests/test_knowledge.py +82 -0
  11. chattermate_cli-0.3.0/tests/test_widget.py +53 -0
  12. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/LICENSE +0 -0
  13. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/README.md +0 -0
  14. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/commands/__init__.py +0 -0
  15. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/commands/agent.py +0 -0
  16. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/commands/auth.py +0 -0
  17. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/commands/workflow.py +0 -0
  18. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/config.py +0 -0
  19. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/context.py +0 -0
  20. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli/mcp_server.py +0 -0
  21. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli.egg-info/dependency_links.txt +0 -0
  22. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli.egg-info/entry_points.txt +0 -0
  23. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli.egg-info/requires.txt +0 -0
  24. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/chattermate_cli.egg-info/top_level.txt +0 -0
  25. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/setup.cfg +0 -0
  26. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/tests/test_client.py +0 -0
  27. {chattermate_cli-0.2.4 → chattermate_cli-0.3.0}/tests/test_mcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chattermate-cli
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: ChatterMate CLI and MCP server — sign up, authenticate, and configure agents, workflows and knowledge from the terminal or an AI agent.
5
5
  Author: ChatterMate
6
6
  License: AGPL-3.0-or-later
@@ -16,4 +16,4 @@ You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>
17
17
  """
18
18
 
19
- __version__ = "0.2.4"
19
+ __version__ = "0.3.0"
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>
17
17
  """
18
18
 
19
- from typing import Any, Callable, Dict, List, Optional
19
+ from typing import Any, Callable, Dict, List, Optional, Tuple
20
20
 
21
21
  import httpx
22
22
 
@@ -112,6 +112,7 @@ class Client:
112
112
  json: Any = None,
113
113
  params: Optional[Dict[str, Any]] = None,
114
114
  data: Optional[Dict[str, Any]] = None,
115
+ files: Any = None,
115
116
  auth: bool = True,
116
117
  _retry: bool = True,
117
118
  ) -> Any:
@@ -119,14 +120,16 @@ class Client:
119
120
  headers = self._auth_headers() if auth else {}
120
121
  try:
121
122
  resp = self._http.request(
122
- method, url, json=json, params=params, data=data, headers=headers
123
+ method, url, json=json, params=params, data=data, files=files, headers=headers
123
124
  )
124
125
  except httpx.HTTPError as e:
125
126
  raise ChatterMateError(f"Could not reach {self.api_url}: {e}") from e
126
127
 
127
128
  if resp.status_code == 401 and auth and _retry and self._refresh():
129
+ # files are (name, bytes, type) tuples — safe to re-send on retry.
128
130
  return self.request(
129
- method, path, json=json, params=params, data=data, auth=auth, _retry=False
131
+ method, path, json=json, params=params, data=data, files=files,
132
+ auth=auth, _retry=False,
130
133
  )
131
134
 
132
135
  if resp.status_code >= 400:
@@ -256,6 +259,18 @@ class Client:
256
259
  body["agent_id"] = agent_id
257
260
  return self.request("POST", "/knowledge/add/urls", json=body)
258
261
 
262
+ def add_knowledge_files(
263
+ self, org_id: str, files: List[Tuple[str, bytes]], agent_id: Optional[str] = None,
264
+ ) -> Any:
265
+ """Upload local PDF files. ``files`` is a list of ``(filename, content_bytes)``."""
266
+ multipart = [
267
+ ("files", (name, content, "application/pdf")) for name, content in files
268
+ ]
269
+ data: Dict[str, Any] = {"org_id": org_id}
270
+ if agent_id:
271
+ data["agent_id"] = agent_id
272
+ return self.request("POST", "/knowledge/upload/pdf", data=data, files=multipart)
273
+
259
274
  def list_knowledge_for_agent(self, agent_id: str) -> Any:
260
275
  return self.request("GET", f"/knowledge/agent/{agent_id}")
261
276
 
@@ -273,3 +288,14 @@ class Client:
273
288
 
274
289
  def knowledge_queue_status(self, queue_id: int) -> Any:
275
290
  return self.request("GET", f"/knowledge/queue/{queue_id}")
291
+
292
+ # -- widgets -----------------------------------------------------------
293
+
294
+ def create_widget(self, name: str, agent_id: Optional[str] = None) -> Any:
295
+ body: Dict[str, Any] = {"name": name}
296
+ if agent_id:
297
+ body["agent_id"] = agent_id
298
+ return self.request("POST", "/widgets", json=body)
299
+
300
+ def list_widgets_for_agent(self, agent_id: str) -> Any:
301
+ return self.request("GET", f"/widgets/agent/{agent_id}")
@@ -16,6 +16,7 @@ You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>
17
17
  """
18
18
 
19
+ from pathlib import Path
19
20
  from typing import List, Optional
20
21
 
21
22
  import typer
@@ -26,6 +27,25 @@ from ..context import console, get_client, output, print_error, run
26
27
 
27
28
  knowledge_app = typer.Typer(no_args_is_help=True, help="Manage agent knowledge sources.")
28
29
 
30
+ PDF_MAGIC = b"%PDF-"
31
+
32
+
33
+ def _resolve_org_id(client) -> str:
34
+ """Resolve the current organization id from the saved session, else via whoami.
35
+
36
+ The config file is only populated by login/signup; token (PAT) auth has none, so
37
+ fall back to the authenticated user's org for headless/CI/agent use.
38
+ """
39
+ org_id = config.load_config().get("organization_id")
40
+ if org_id:
41
+ return str(org_id)
42
+ me = run(client.whoami)
43
+ org_id = me.get("organization_id") if isinstance(me, dict) else None
44
+ if not org_id:
45
+ print_error("Could not determine your organization. Run 'chattermate whoami' to check auth.")
46
+ raise typer.Exit(code=1)
47
+ return str(org_id)
48
+
29
49
 
30
50
  @knowledge_app.command("add-url")
31
51
  def add_url(
@@ -38,11 +58,8 @@ def add_url(
38
58
  if not website and not pdf_url:
39
59
  print_error("Provide at least one --website or --pdf-url.")
40
60
  raise typer.Exit(code=1)
41
- org_id = config.load_config().get("organization_id")
42
- if not org_id:
43
- print_error("No organization in session. Run 'chattermate login' or 'whoami' first.")
44
- raise typer.Exit(code=1)
45
61
  client = get_client()
62
+ org_id = _resolve_org_id(client)
46
63
  data = run(lambda: client.add_knowledge(
47
64
  org_id=org_id, pdf_urls=list(pdf_url), websites=list(website), agent_id=agent_id,
48
65
  ))
@@ -51,6 +68,47 @@ def add_url(
51
68
  output(data, as_json)
52
69
 
53
70
 
71
+ @knowledge_app.command("add-file")
72
+ def add_file(
73
+ paths: List[Path] = typer.Argument(..., help="Local PDF file path(s) to upload."),
74
+ agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Attach to this agent."),
75
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
76
+ ):
77
+ """Upload local PDF file(s) to the knowledge base."""
78
+ files = []
79
+ for path in paths:
80
+ if not path.is_file():
81
+ print_error(f"Not a file: {path}")
82
+ raise typer.Exit(code=1)
83
+ if path.suffix.lower() != ".pdf":
84
+ print_error(f"Not a .pdf file: {path}")
85
+ raise typer.Exit(code=1)
86
+ try:
87
+ content = path.read_bytes()
88
+ except OSError as exc:
89
+ print_error(f"Cannot read {path}: {exc}")
90
+ raise typer.Exit(code=1)
91
+ if not content.startswith(PDF_MAGIC):
92
+ print_error(f"Not a valid PDF (missing %PDF- header): {path}")
93
+ raise typer.Exit(code=1)
94
+ files.append((path.name, content))
95
+
96
+ client = get_client()
97
+ org_id = _resolve_org_id(client)
98
+ data = run(lambda: client.add_knowledge_files(org_id=org_id, files=files, agent_id=agent_id))
99
+
100
+ if as_json:
101
+ output(data, True)
102
+ return
103
+ console.print(f"[green]Queued {len(files)} file(s) for ingestion.[/green]")
104
+ items = data.get("queue_items", []) if isinstance(data, dict) else []
105
+ for it in items:
106
+ console.print(
107
+ f" queue {it.get('id')} — {it.get('status')} "
108
+ f"(track with: chattermate knowledge status {it.get('id')})"
109
+ )
110
+
111
+
54
112
  @knowledge_app.command("list")
55
113
  def list_knowledge(
56
114
  agent_id: str = typer.Argument(..., help="Agent id."),
@@ -0,0 +1,63 @@
1
+ """
2
+ ChatterMate - CLI Widget 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
+ import typer
20
+ from rich.table import Table
21
+
22
+ from ..context import console, get_client, output, run
23
+
24
+ widget_app = typer.Typer(no_args_is_help=True, help="Manage embeddable chat widgets.")
25
+
26
+
27
+ @widget_app.command("create")
28
+ def create_widget(
29
+ agent_id: str = typer.Option(..., "--agent-id", help="Agent to attach the widget to."),
30
+ name: str = typer.Option("Website widget", "--name", help="Widget name."),
31
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
32
+ ):
33
+ """Create an embeddable widget for an agent and print its widget id + embed snippet."""
34
+ client = get_client()
35
+ data = run(lambda: client.create_widget(name=name, agent_id=agent_id))
36
+ if as_json:
37
+ output(data, True)
38
+ return
39
+ widget_id = data.get("id") if isinstance(data, dict) else None
40
+ console.print(f"[green]Created widget[/green] {widget_id}")
41
+ console.print("Embed it on your site:")
42
+ console.print(f" <script>window.chattermateId='{widget_id}';</script>")
43
+ console.print(' <script src="https://app.chattermate.chat/webclient/chattermate.min.js"></script>')
44
+
45
+
46
+ @widget_app.command("list")
47
+ def list_widgets(
48
+ agent_id: str = typer.Argument(..., help="Agent id."),
49
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
50
+ ):
51
+ """List the widgets attached to an agent (their ids are what you embed)."""
52
+ client = get_client()
53
+ data = run(lambda: client.list_widgets_for_agent(agent_id))
54
+
55
+ def render(rows):
56
+ table = Table(title="Widgets")
57
+ for col in ("id", "name", "agent_id"):
58
+ table.add_column(col)
59
+ for r in rows or []:
60
+ table.add_row(str(r.get("id")), str(r.get("name", "")), str(r.get("agent_id", "")))
61
+ console.print(table)
62
+
63
+ output(data, as_json, render)
@@ -24,6 +24,7 @@ from . import __version__, context
24
24
  from .commands import auth as auth_cmd
25
25
  from .commands.agent import agent_app
26
26
  from .commands.knowledge import knowledge_app
27
+ from .commands.widget import widget_app
27
28
  from .commands.workflow import workflow_app
28
29
 
29
30
  app = typer.Typer(
@@ -62,6 +63,7 @@ app.command("whoami")(auth_cmd.whoami)
62
63
  # Sub-command groups
63
64
  app.add_typer(auth_cmd.token_app, name="token")
64
65
  app.add_typer(agent_app, name="agent")
66
+ app.add_typer(widget_app, name="widget")
65
67
  app.add_typer(workflow_app, name="workflow")
66
68
  app.add_typer(knowledge_app, name="knowledge")
67
69
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chattermate-cli
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: ChatterMate CLI and MCP server — sign up, authenticate, and configure agents, workflows and knowledge from the terminal or an AI agent.
5
5
  Author: ChatterMate
6
6
  License: AGPL-3.0-or-later
@@ -17,6 +17,9 @@ chattermate_cli/commands/__init__.py
17
17
  chattermate_cli/commands/agent.py
18
18
  chattermate_cli/commands/auth.py
19
19
  chattermate_cli/commands/knowledge.py
20
+ chattermate_cli/commands/widget.py
20
21
  chattermate_cli/commands/workflow.py
21
22
  tests/test_client.py
22
- tests/test_mcp.py
23
+ tests/test_knowledge.py
24
+ tests/test_mcp.py
25
+ tests/test_widget.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chattermate-cli"
7
- version = "0.2.4"
7
+ version = "0.3.0"
8
8
  description = "ChatterMate CLI and MCP server — sign up, authenticate, and configure agents, workflows and knowledge from the terminal or an AI agent."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,82 @@
1
+ """
2
+ ChatterMate - CLI Knowledge Command Tests
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
+
11
+ import httpx
12
+ from typer.testing import CliRunner
13
+
14
+ from chattermate_cli.client import Client
15
+ from chattermate_cli.commands import knowledge as kn
16
+ from chattermate_cli.commands.knowledge import knowledge_app
17
+
18
+ runner = CliRunner()
19
+
20
+
21
+ def make_client(handler, token="cmat_x"):
22
+ client = Client(api_url="http://test", token=token)
23
+ client._http = httpx.Client(transport=httpx.MockTransport(handler))
24
+ return client
25
+
26
+
27
+ def test_add_knowledge_files_sends_multipart():
28
+ seen = {}
29
+
30
+ def handler(request):
31
+ seen["ctype"] = request.headers.get("content-type", "")
32
+ seen["body"] = request.content
33
+ seen["path"] = request.url.path
34
+ return httpx.Response(200, json={"message": "ok", "queue_items": [{"id": 7, "status": "pending"}]})
35
+
36
+ client = make_client(handler)
37
+ data = client.add_knowledge_files(
38
+ org_id="org-1", files=[("a.pdf", b"%PDF-1.4 body")], agent_id="ag-1"
39
+ )
40
+
41
+ assert seen["path"] == "/api/v1/knowledge/upload/pdf"
42
+ assert "multipart/form-data" in seen["ctype"]
43
+ body = seen["body"]
44
+ assert b'name="files"' in body and b"a.pdf" in body and b"%PDF-1.4" in body
45
+ assert b'name="org_id"' in body and b"org-1" in body
46
+ assert b'name="agent_id"' in body and b"ag-1" in body
47
+ assert data["queue_items"][0]["id"] == 7
48
+
49
+
50
+ def test_add_file_rejects_missing_path(tmp_path):
51
+ result = runner.invoke(knowledge_app, ["add-file", str(tmp_path / "nope.pdf")])
52
+ assert result.exit_code == 1
53
+ assert "Not a file" in result.output
54
+
55
+
56
+ def test_add_file_rejects_non_pdf_extension(tmp_path):
57
+ p = tmp_path / "notes.txt"
58
+ p.write_text("hello")
59
+ result = runner.invoke(knowledge_app, ["add-file", str(p)])
60
+ assert result.exit_code == 1
61
+ assert ".pdf" in result.output
62
+
63
+
64
+ def test_add_file_rejects_bad_magic_bytes(tmp_path):
65
+ p = tmp_path / "fake.pdf"
66
+ p.write_bytes(b"this is not a pdf")
67
+ result = runner.invoke(knowledge_app, ["add-file", str(p)])
68
+ assert result.exit_code == 1
69
+ assert "valid PDF" in result.output
70
+
71
+
72
+ def test_resolve_org_id_prefers_config(monkeypatch):
73
+ monkeypatch.setattr(kn.config, "load_config", lambda: {"organization_id": "org-from-config"})
74
+ # whoami must NOT be called when config has the org.
75
+ client = make_client(lambda req: httpx.Response(500, json={"detail": "should not be called"}))
76
+ assert kn._resolve_org_id(client) == "org-from-config"
77
+
78
+
79
+ def test_resolve_org_id_falls_back_to_whoami(monkeypatch):
80
+ monkeypatch.setattr(kn.config, "load_config", lambda: {})
81
+ client = make_client(lambda req: httpx.Response(200, json={"organization_id": "org-xyz"}))
82
+ assert kn._resolve_org_id(client) == "org-xyz"
@@ -0,0 +1,53 @@
1
+ """
2
+ ChatterMate - CLI Widget Command Tests
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
+
11
+ import json
12
+
13
+ import httpx
14
+
15
+ from chattermate_cli.client import Client
16
+
17
+
18
+ def make_client(handler, token="cmat_x"):
19
+ client = Client(api_url="http://test", token=token)
20
+ client._http = httpx.Client(transport=httpx.MockTransport(handler))
21
+ return client
22
+
23
+
24
+ def test_create_widget_posts_name_and_agent():
25
+ seen = {}
26
+
27
+ def handler(request):
28
+ seen["method"] = request.method
29
+ seen["path"] = request.url.path
30
+ seen["json"] = json.loads(request.content)
31
+ return httpx.Response(200, json={"id": "w-123", "agent_id": "ag-1", "name": "Site"})
32
+
33
+ client = make_client(handler)
34
+ data = client.create_widget(name="Site", agent_id="ag-1")
35
+
36
+ assert seen["method"] == "POST"
37
+ assert seen["path"] == "/api/v1/widgets"
38
+ assert seen["json"] == {"name": "Site", "agent_id": "ag-1"}
39
+ assert data["id"] == "w-123"
40
+
41
+
42
+ def test_list_widgets_for_agent_gets_by_agent():
43
+ seen = {}
44
+
45
+ def handler(request):
46
+ seen["path"] = request.url.path
47
+ return httpx.Response(200, json=[{"id": "w-1", "name": "Site", "agent_id": "ag-1"}])
48
+
49
+ client = make_client(handler)
50
+ rows = client.list_widgets_for_agent("ag-1")
51
+
52
+ assert seen["path"] == "/api/v1/widgets/agent/ag-1"
53
+ assert rows[0]["id"] == "w-1"
File without changes