chattermate-cli 0.2.0__py3-none-any.whl
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.
- chattermate_cli/__init__.py +19 -0
- chattermate_cli/client.py +275 -0
- chattermate_cli/commands/__init__.py +17 -0
- chattermate_cli/commands/agent.py +122 -0
- chattermate_cli/commands/auth.py +202 -0
- chattermate_cli/commands/knowledge.py +104 -0
- chattermate_cli/commands/workflow.py +129 -0
- chattermate_cli/config.py +101 -0
- chattermate_cli/context.py +78 -0
- chattermate_cli/main.py +74 -0
- chattermate_cli/mcp_server.py +253 -0
- chattermate_cli-0.2.0.dist-info/METADATA +113 -0
- chattermate_cli-0.2.0.dist-info/RECORD +17 -0
- chattermate_cli-0.2.0.dist-info/WHEEL +5 -0
- chattermate_cli-0.2.0.dist-info/entry_points.txt +4 -0
- chattermate_cli-0.2.0.dist-info/licenses/LICENSE +18 -0
- chattermate_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatterMate - MCP Server
|
|
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
|
+
ChatterMate MCP server (stdio). Exposes ChatterMate configuration to AI agents.
|
|
20
|
+
|
|
21
|
+
Configuration via environment:
|
|
22
|
+
CHATTERMATE_TOKEN A personal access token (cmat_...) — required.
|
|
23
|
+
CHATTERMATE_API_URL API base URL (default https://api.chattermate.chat).
|
|
24
|
+
Set to http://localhost:8000 for a local backend.
|
|
25
|
+
|
|
26
|
+
Run: chattermate-mcp (or: uvx --from chattermate-cli chattermate-mcp)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from typing import Any, Dict, List, Optional
|
|
30
|
+
|
|
31
|
+
from mcp.server.fastmcp import FastMCP
|
|
32
|
+
|
|
33
|
+
from . import config
|
|
34
|
+
from .client import ChatterMateError, Client
|
|
35
|
+
|
|
36
|
+
mcp = FastMCP("chattermate")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _client() -> Client:
|
|
40
|
+
api_url = config.resolve_api_url()
|
|
41
|
+
token = config.resolve_token()
|
|
42
|
+
if not token:
|
|
43
|
+
raise ChatterMateError(
|
|
44
|
+
"No credentials. Set CHATTERMATE_TOKEN to a personal access token (cmat_...)."
|
|
45
|
+
)
|
|
46
|
+
cfg = config.load_config()
|
|
47
|
+
return Client(
|
|
48
|
+
api_url=api_url,
|
|
49
|
+
token=token,
|
|
50
|
+
refresh_token=cfg.get("refresh_token"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _org_id(client: Client) -> Optional[str]:
|
|
55
|
+
org = config.load_config().get("organization_id")
|
|
56
|
+
if org:
|
|
57
|
+
return org
|
|
58
|
+
try:
|
|
59
|
+
me = client.whoami()
|
|
60
|
+
except ChatterMateError:
|
|
61
|
+
return None
|
|
62
|
+
return me.get("organization_id") if isinstance(me, dict) else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _call(fn) -> Any:
|
|
66
|
+
"""Run an API call, returning a structured error dict instead of raising."""
|
|
67
|
+
try:
|
|
68
|
+
return fn()
|
|
69
|
+
except ChatterMateError as e:
|
|
70
|
+
return {"error": str(e), "status_code": e.status_code}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# -- read-only tools -------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
@mcp.tool()
|
|
76
|
+
def whoami() -> Any:
|
|
77
|
+
"""Return the authenticated user (id, email, organization, role)."""
|
|
78
|
+
client = _client()
|
|
79
|
+
return _call(client.whoami)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@mcp.tool()
|
|
83
|
+
def list_agents() -> Any:
|
|
84
|
+
"""List all AI agents in the organization."""
|
|
85
|
+
client = _client()
|
|
86
|
+
return _call(client.list_agents)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
def get_agent(agent_id: str) -> Any:
|
|
91
|
+
"""Get a single agent by id."""
|
|
92
|
+
client = _client()
|
|
93
|
+
return _call(lambda: client.get_agent(agent_id))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def get_workflow(agent_id: str) -> Any:
|
|
98
|
+
"""Get the workflow attached to an agent."""
|
|
99
|
+
client = _client()
|
|
100
|
+
return _call(lambda: client.get_workflow_for_agent(agent_id))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@mcp.tool()
|
|
104
|
+
def get_workflow_nodes(workflow_id: str) -> Any:
|
|
105
|
+
"""Get all nodes and connections for a workflow."""
|
|
106
|
+
client = _client()
|
|
107
|
+
return _call(lambda: client.get_workflow_nodes(workflow_id))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
def list_knowledge(agent_id: str) -> Any:
|
|
112
|
+
"""List knowledge sources linked to an agent."""
|
|
113
|
+
client = _client()
|
|
114
|
+
return _call(lambda: client.list_knowledge_for_agent(agent_id))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp.tool()
|
|
118
|
+
def get_ingestion_status(queue_id: int) -> Any:
|
|
119
|
+
"""Get the status of a knowledge ingestion queue item."""
|
|
120
|
+
client = _client()
|
|
121
|
+
return _call(lambda: client.knowledge_queue_status(queue_id))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# -- mutating tools --------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def create_agent(
|
|
128
|
+
name: str,
|
|
129
|
+
agent_type: str = "CUSTOM",
|
|
130
|
+
instructions: Optional[List[str]] = None,
|
|
131
|
+
description: Optional[str] = None,
|
|
132
|
+
display_name: Optional[str] = None,
|
|
133
|
+
) -> Any:
|
|
134
|
+
"""
|
|
135
|
+
Create an AI agent. agent_type is one of customer_support, sales, tech_support,
|
|
136
|
+
general, custom (case-insensitive). The organization is taken from the authenticated user.
|
|
137
|
+
"""
|
|
138
|
+
client = _client()
|
|
139
|
+
org_id = _org_id(client)
|
|
140
|
+
if not org_id:
|
|
141
|
+
return {"error": "Could not determine organization for the current credentials."}
|
|
142
|
+
payload: Dict[str, Any] = {
|
|
143
|
+
"name": name,
|
|
144
|
+
"agent_type": agent_type.lower(),
|
|
145
|
+
"instructions": instructions or [f"You are {name}, a helpful assistant."],
|
|
146
|
+
"organization_id": org_id,
|
|
147
|
+
}
|
|
148
|
+
if description is not None:
|
|
149
|
+
payload["description"] = description
|
|
150
|
+
if display_name is not None:
|
|
151
|
+
payload["display_name"] = display_name
|
|
152
|
+
return _call(lambda: client.create_agent(payload))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@mcp.tool()
|
|
156
|
+
def update_agent(
|
|
157
|
+
agent_id: str,
|
|
158
|
+
instructions: Optional[List[str]] = None,
|
|
159
|
+
display_name: Optional[str] = None,
|
|
160
|
+
is_active: Optional[bool] = None,
|
|
161
|
+
) -> Any:
|
|
162
|
+
"""Update an agent's instructions, display name, or active state."""
|
|
163
|
+
payload: Dict[str, Any] = {}
|
|
164
|
+
if instructions is not None:
|
|
165
|
+
payload["instructions"] = instructions
|
|
166
|
+
if display_name is not None:
|
|
167
|
+
payload["display_name"] = display_name
|
|
168
|
+
if is_active is not None:
|
|
169
|
+
payload["is_active"] = is_active
|
|
170
|
+
if not payload:
|
|
171
|
+
return {"error": "Nothing to update."}
|
|
172
|
+
client = _client()
|
|
173
|
+
return _call(lambda: client.update_agent(agent_id, payload))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@mcp.tool()
|
|
177
|
+
def create_workflow(agent_id: str, name: str, description: Optional[str] = None) -> Any:
|
|
178
|
+
"""Create a workflow for an agent."""
|
|
179
|
+
payload: Dict[str, Any] = {"name": name, "agent_id": agent_id}
|
|
180
|
+
if description is not None:
|
|
181
|
+
payload["description"] = description
|
|
182
|
+
client = _client()
|
|
183
|
+
return _call(lambda: client.create_workflow(payload))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@mcp.tool()
|
|
187
|
+
def update_workflow(
|
|
188
|
+
workflow_id: str,
|
|
189
|
+
name: Optional[str] = None,
|
|
190
|
+
description: Optional[str] = None,
|
|
191
|
+
status: Optional[str] = None,
|
|
192
|
+
) -> Any:
|
|
193
|
+
"""Update a workflow's metadata. status is draft, published or archived (case-insensitive)."""
|
|
194
|
+
payload: Dict[str, Any] = {}
|
|
195
|
+
if name is not None:
|
|
196
|
+
payload["name"] = name
|
|
197
|
+
if description is not None:
|
|
198
|
+
payload["description"] = description
|
|
199
|
+
if status is not None:
|
|
200
|
+
payload["status"] = status.lower()
|
|
201
|
+
if not payload:
|
|
202
|
+
return {"error": "Nothing to update."}
|
|
203
|
+
client = _client()
|
|
204
|
+
return _call(lambda: client.update_workflow(workflow_id, payload))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@mcp.tool()
|
|
208
|
+
def update_workflow_nodes(workflow_id: str, payload: Dict[str, Any]) -> Any:
|
|
209
|
+
"""Replace a workflow's nodes/connections with the given JSON payload."""
|
|
210
|
+
client = _client()
|
|
211
|
+
return _call(lambda: client.update_workflow_nodes(workflow_id, payload))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@mcp.tool()
|
|
215
|
+
def add_knowledge_url(
|
|
216
|
+
websites: Optional[List[str]] = None,
|
|
217
|
+
pdf_urls: Optional[List[str]] = None,
|
|
218
|
+
agent_id: Optional[str] = None,
|
|
219
|
+
) -> Any:
|
|
220
|
+
"""Add website and/or PDF URLs to the knowledge base, optionally linking to an agent."""
|
|
221
|
+
if not websites and not pdf_urls:
|
|
222
|
+
return {"error": "Provide at least one website or pdf_url."}
|
|
223
|
+
client = _client()
|
|
224
|
+
org_id = _org_id(client)
|
|
225
|
+
if not org_id:
|
|
226
|
+
return {"error": "Could not determine organization for the current credentials."}
|
|
227
|
+
return _call(lambda: client.add_knowledge(
|
|
228
|
+
org_id=org_id, pdf_urls=pdf_urls or [], websites=websites or [], agent_id=agent_id,
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@mcp.tool()
|
|
233
|
+
def link_knowledge(knowledge_id: int, agent_id: str) -> Any:
|
|
234
|
+
"""Link a knowledge source to an agent."""
|
|
235
|
+
client = _client()
|
|
236
|
+
result = _call(lambda: client.link_knowledge(knowledge_id, agent_id))
|
|
237
|
+
return result if result is not None else {"ok": True}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@mcp.tool()
|
|
241
|
+
def unlink_knowledge(knowledge_id: int, agent_id: str) -> Any:
|
|
242
|
+
"""Unlink a knowledge source from an agent."""
|
|
243
|
+
client = _client()
|
|
244
|
+
result = _call(lambda: client.unlink_knowledge(knowledge_id, agent_id))
|
|
245
|
+
return result if result is not None else {"ok": True}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def main():
|
|
249
|
+
mcp.run(transport="stdio")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
main()
|
|
@@ -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,17 @@
|
|
|
1
|
+
chattermate_cli/__init__.py,sha256=Fl2jq4a5tuyCsxms3gKGE8P9vEGyAXUmw_N7Xqlygw8,724
|
|
2
|
+
chattermate_cli/client.py,sha256=q48K6n9snPqzwcRE958noctenI8y62lhc3q4bJmd29A,10604
|
|
3
|
+
chattermate_cli/config.py,sha256=zagLyijC4HmuwCsEhz45X3Tf5N5PGanc18k0em9SO8I,3360
|
|
4
|
+
chattermate_cli/context.py,sha256=zK27rN9-PH_NTkxbOw1VgTQisLmqw73ppOCgoueykR0,2479
|
|
5
|
+
chattermate_cli/main.py,sha256=JFoRDdPzjGifFgzOxaPleciDnvwWbFwlG3nJ85HDm3g,2107
|
|
6
|
+
chattermate_cli/mcp_server.py,sha256=62m9QQBQAzQCHx5O-vYD8wABwMYfr9I114h68gcRVSU,8051
|
|
7
|
+
chattermate_cli/commands/__init__.py,sha256=I0IyYLYZl-b8dcdVpjaOYQ2lVNYVFov-4eTaCW4jEEQ,700
|
|
8
|
+
chattermate_cli/commands/agent.py,sha256=R81d6XJK0ooAMo4lk15fWgEJSMjZ8LzO4EANTX98gEI,4634
|
|
9
|
+
chattermate_cli/commands/auth.py,sha256=PiNAEM_0IEXa6puF95NxfXA850ZtVX-y6QJFRtIhuw8,7966
|
|
10
|
+
chattermate_cli/commands/knowledge.py,sha256=q10EBzoK0IhqVMQM1swXKHvHr1lxquip9rmpBOs0DdY,4002
|
|
11
|
+
chattermate_cli/commands/workflow.py,sha256=kwyf6Dc8IBFqUC4sQ3lg4WRIifpaW7wcHDMfCpxXTT4,4859
|
|
12
|
+
chattermate_cli-0.2.0.dist-info/licenses/LICENSE,sha256=60gz0wEB0a2wZOoendAWGBrLzAM7jvKAAKdOOm4WEdg,820
|
|
13
|
+
chattermate_cli-0.2.0.dist-info/METADATA,sha256=cqFVk-EV6KD_UgXHhXWDVUSMZocEYR-DkhrMIVQMz40,3520
|
|
14
|
+
chattermate_cli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
chattermate_cli-0.2.0.dist-info/entry_points.txt,sha256=HYy6P-pExh28Y5vXbJ6PrQTG8iwZZ2oKX-qZWyugXBc,142
|
|
16
|
+
chattermate_cli-0.2.0.dist-info/top_level.txt,sha256=kDj5LfQJr-s1ce222gYWMuQ0kFdxsN241ZmXWdJZw28,16
|
|
17
|
+
chattermate_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
chattermate_cli
|