primethink-cli 1.0.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.
- primethink.py +1002 -0
- primethink_cli-1.0.0.dist-info/METADATA +307 -0
- primethink_cli-1.0.0.dist-info/RECORD +7 -0
- primethink_cli-1.0.0.dist-info/WHEEL +5 -0
- primethink_cli-1.0.0.dist-info/entry_points.txt +2 -0
- primethink_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- primethink_cli-1.0.0.dist-info/top_level.txt +1 -0
primethink.py
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import contextlib
|
|
7
|
+
import click
|
|
8
|
+
import requests
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, Any, List
|
|
11
|
+
|
|
12
|
+
# Define default config file location
|
|
13
|
+
CONFIG_DIR = Path.home() / ".primethink"
|
|
14
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
15
|
+
|
|
16
|
+
# Default API URL
|
|
17
|
+
DEFAULT_API_URL = "https://api.primethink.ai"
|
|
18
|
+
|
|
19
|
+
# Default timeout for HTTP requests (seconds)
|
|
20
|
+
REQUEST_TIMEOUT = 30
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _sanitize_filename(name: str) -> str:
|
|
24
|
+
"""Strip path components and unsafe characters from a server-provided filename."""
|
|
25
|
+
basename = Path(name).name
|
|
26
|
+
basename = basename.lstrip('.')
|
|
27
|
+
if not basename:
|
|
28
|
+
return "unnamed_file"
|
|
29
|
+
return basename
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def cli():
|
|
33
|
+
"""PrimeThink CLI - A tool for interacting with PrimeThink API."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
def version():
|
|
38
|
+
"""Display the version of PrimeThink CLI."""
|
|
39
|
+
click.echo("PrimeThink CLI v1.0.0")
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# Token Management Commands
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
@cli.command()
|
|
46
|
+
@click.option('--token', '-t', required=True, help='API token')
|
|
47
|
+
@click.option('--profile', '-p', default='default', help='Profile name (default: default)')
|
|
48
|
+
@click.option('--api-url', '-u', default=None, help='Custom API URL (default: https://api.primethink.ai)')
|
|
49
|
+
def configure(token, profile, api_url):
|
|
50
|
+
"""Configure API token for a profile."""
|
|
51
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
52
|
+
|
|
53
|
+
# Load existing config
|
|
54
|
+
config = load_config()
|
|
55
|
+
|
|
56
|
+
# Initialize profiles if not exists
|
|
57
|
+
if 'profiles' not in config:
|
|
58
|
+
config['profiles'] = {}
|
|
59
|
+
|
|
60
|
+
# Save token and API URL for the profile
|
|
61
|
+
profile_config = {'token': token}
|
|
62
|
+
if api_url:
|
|
63
|
+
profile_config['api_url'] = api_url
|
|
64
|
+
|
|
65
|
+
config['profiles'][profile] = profile_config
|
|
66
|
+
|
|
67
|
+
# Set as active profile if it's the first one or default
|
|
68
|
+
if 'active_profile' not in config or profile == 'default':
|
|
69
|
+
config['active_profile'] = profile
|
|
70
|
+
|
|
71
|
+
# Save config
|
|
72
|
+
save_config(config)
|
|
73
|
+
|
|
74
|
+
api_display = api_url if api_url else DEFAULT_API_URL
|
|
75
|
+
click.echo(f"✓ Token configured for profile '{profile}' (API: {api_display})")
|
|
76
|
+
click.echo(f"✓ Profile '{profile}' set as active")
|
|
77
|
+
|
|
78
|
+
@cli.command()
|
|
79
|
+
@click.argument('profile')
|
|
80
|
+
def use(profile):
|
|
81
|
+
"""Switch to a different token profile."""
|
|
82
|
+
config = load_config()
|
|
83
|
+
|
|
84
|
+
if 'profiles' not in config or profile not in config['profiles']:
|
|
85
|
+
click.echo(f"Error: Profile '{profile}' not found. Use 'pt configure' to create it.")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
config['active_profile'] = profile
|
|
89
|
+
save_config(config)
|
|
90
|
+
|
|
91
|
+
api_url = config['profiles'][profile].get('api_url', DEFAULT_API_URL)
|
|
92
|
+
click.echo(f"✓ Switched to profile '{profile}' (API: {api_url})")
|
|
93
|
+
|
|
94
|
+
@cli.command()
|
|
95
|
+
def list_profiles():
|
|
96
|
+
"""List all configured profiles."""
|
|
97
|
+
config = load_config()
|
|
98
|
+
|
|
99
|
+
if 'profiles' not in config or not config['profiles']:
|
|
100
|
+
click.echo("No profiles configured. Use 'pt configure' to add one.")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
active_profile = config.get('active_profile', '')
|
|
104
|
+
|
|
105
|
+
click.echo("Configured profiles:")
|
|
106
|
+
for name, data in config['profiles'].items():
|
|
107
|
+
active = "* " if name == active_profile else " "
|
|
108
|
+
api_url = data.get('api_url', DEFAULT_API_URL)
|
|
109
|
+
click.echo(f"{active}{name} ({api_url})")
|
|
110
|
+
|
|
111
|
+
@cli.command()
|
|
112
|
+
@click.argument('profile')
|
|
113
|
+
def remove_profile(profile):
|
|
114
|
+
"""Remove a token profile."""
|
|
115
|
+
config = load_config()
|
|
116
|
+
|
|
117
|
+
if 'profiles' not in config or profile not in config['profiles']:
|
|
118
|
+
click.echo(f"Error: Profile '{profile}' not found.")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
del config['profiles'][profile]
|
|
122
|
+
|
|
123
|
+
# If the removed profile was active, switch to another one
|
|
124
|
+
if config.get('active_profile') == profile:
|
|
125
|
+
if config['profiles']:
|
|
126
|
+
config['active_profile'] = list(config['profiles'].keys())[0]
|
|
127
|
+
click.echo(f"✓ Switched to profile '{config['active_profile']}'")
|
|
128
|
+
else:
|
|
129
|
+
del config['active_profile']
|
|
130
|
+
|
|
131
|
+
save_config(config)
|
|
132
|
+
click.echo(f"✓ Profile '{profile}' removed")
|
|
133
|
+
|
|
134
|
+
# ============================================================================
|
|
135
|
+
# API Commands
|
|
136
|
+
# ============================================================================
|
|
137
|
+
|
|
138
|
+
@cli.command()
|
|
139
|
+
@click.option('--profile', '-p', default=None, help='Profile to use for this request')
|
|
140
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
141
|
+
def available_actions(profile, api_url):
|
|
142
|
+
"""Get list of available task actions."""
|
|
143
|
+
config = get_active_config(profile)
|
|
144
|
+
api_url = api_url or config['api_url']
|
|
145
|
+
token = config['token']
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
response = requests.get(
|
|
149
|
+
f"{api_url}/api/v1/tasks/available_task_actions",
|
|
150
|
+
headers={
|
|
151
|
+
"accept": "application/json",
|
|
152
|
+
"Authorization": f"Token {token}"
|
|
153
|
+
},
|
|
154
|
+
timeout=REQUEST_TIMEOUT
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if response.status_code == 200:
|
|
158
|
+
actions = response.json()
|
|
159
|
+
click.echo(json.dumps(actions, indent=2))
|
|
160
|
+
else:
|
|
161
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
except requests.exceptions.RequestException as e:
|
|
164
|
+
click.echo(f"Error connecting to API: {e}")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
@cli.command()
|
|
168
|
+
@click.option('--action', '-a', required=True, help='Task action name')
|
|
169
|
+
@click.option('--message', '-m', required=True, help='Message input')
|
|
170
|
+
@click.option('--files', '-f', multiple=True, type=click.Path(exists=True), help='Files to attach')
|
|
171
|
+
@click.option('--return-original', is_flag=True, help='Return original message')
|
|
172
|
+
@click.option('--profile', '-p', default=None, help='Profile to use for this request')
|
|
173
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
174
|
+
def execute_action(action, message, files, return_original, profile, api_url):
|
|
175
|
+
"""Execute a task action."""
|
|
176
|
+
config = get_active_config(profile)
|
|
177
|
+
api_url = api_url or config['api_url']
|
|
178
|
+
token = config['token']
|
|
179
|
+
|
|
180
|
+
# Prepare multipart form data
|
|
181
|
+
form_data = {
|
|
182
|
+
'task_action_name': action,
|
|
183
|
+
'message_input': message,
|
|
184
|
+
'return_original_message': str(return_original).lower()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
with contextlib.ExitStack() as stack:
|
|
188
|
+
files_data = []
|
|
189
|
+
for file_path in files:
|
|
190
|
+
f = stack.enter_context(open(file_path, 'rb'))
|
|
191
|
+
files_data.append(('files', f))
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
response = requests.post(
|
|
195
|
+
f"{api_url}/api/v1/tasks/execute_task_action",
|
|
196
|
+
headers={
|
|
197
|
+
"accept": "application/json",
|
|
198
|
+
"Authorization": f"Token {token}"
|
|
199
|
+
},
|
|
200
|
+
data=form_data,
|
|
201
|
+
files=files_data if files_data else None,
|
|
202
|
+
timeout=REQUEST_TIMEOUT
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if response.status_code == 200:
|
|
206
|
+
result = response.json()
|
|
207
|
+
click.echo(json.dumps(result, indent=2))
|
|
208
|
+
else:
|
|
209
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
except requests.exceptions.RequestException as e:
|
|
212
|
+
click.echo(f"Error connecting to API: {e}")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
@cli.command()
|
|
216
|
+
@click.argument('chat_id_or_mention', required=False)
|
|
217
|
+
@click.option('--message', '-m', required=True, help='Message input')
|
|
218
|
+
@click.option('--files', '-f', multiple=True, type=click.Path(exists=True), help='Files to attach')
|
|
219
|
+
@click.option('--async', 'async_request', is_flag=True, default=False, help='Asynchronous request')
|
|
220
|
+
@click.option('--agent', '-a', type=int, help='Agent ID to send message to')
|
|
221
|
+
@click.option('--profile', '-p', default=None, help='Profile to use for this request')
|
|
222
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
223
|
+
def send_message(chat_id_or_mention, message, files, async_request, agent, profile, api_url):
|
|
224
|
+
"""Send a message to a chat by ID/mention name, or to an agent by ID."""
|
|
225
|
+
# Validate that either chat_id_or_mention or agent is provided, but not both
|
|
226
|
+
if not chat_id_or_mention and not agent:
|
|
227
|
+
click.echo("Error: Either CHAT_ID_OR_MENTION or --agent must be provided")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
|
|
230
|
+
if chat_id_or_mention and agent:
|
|
231
|
+
click.echo("Error: Cannot specify both CHAT_ID_OR_MENTION and --agent")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
config = get_active_config(profile)
|
|
235
|
+
api_url = api_url or config['api_url']
|
|
236
|
+
token = config['token']
|
|
237
|
+
|
|
238
|
+
# Prepare multipart form data
|
|
239
|
+
form_data = {
|
|
240
|
+
'message_input': message
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Add is_sync only for chat messages
|
|
244
|
+
if chat_id_or_mention:
|
|
245
|
+
is_sync = str(not async_request).lower()
|
|
246
|
+
form_data['is_sync'] = is_sync
|
|
247
|
+
|
|
248
|
+
# Determine the endpoint based on whether we're sending to a chat or agent
|
|
249
|
+
if agent:
|
|
250
|
+
endpoint = f"{api_url}/api/v1/virtual-assistants/{agent}/messages"
|
|
251
|
+
else:
|
|
252
|
+
endpoint = f"{api_url}/api/v1/chats/{chat_id_or_mention}/messages"
|
|
253
|
+
|
|
254
|
+
with contextlib.ExitStack() as stack:
|
|
255
|
+
files_data = []
|
|
256
|
+
for file_path in files:
|
|
257
|
+
f = stack.enter_context(open(file_path, 'rb'))
|
|
258
|
+
files_data.append(('files', f))
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
response = requests.post(
|
|
262
|
+
endpoint,
|
|
263
|
+
headers={
|
|
264
|
+
"accept": "application/json",
|
|
265
|
+
"Authorization": f"Token {token}"
|
|
266
|
+
},
|
|
267
|
+
data=form_data,
|
|
268
|
+
files=files_data if files_data else None,
|
|
269
|
+
timeout=REQUEST_TIMEOUT
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if response.status_code == 200:
|
|
273
|
+
result = response.json()
|
|
274
|
+
click.echo(json.dumps(result, indent=2))
|
|
275
|
+
else:
|
|
276
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
except requests.exceptions.RequestException as e:
|
|
279
|
+
click.echo(f"Error connecting to API: {e}")
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
|
|
282
|
+
# ============================================================================
|
|
283
|
+
# Chat Subcommands
|
|
284
|
+
# ============================================================================
|
|
285
|
+
|
|
286
|
+
@cli.group()
|
|
287
|
+
def chat():
|
|
288
|
+
"""Chat file management commands."""
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
@chat.command()
|
|
292
|
+
@click.argument('chat_id')
|
|
293
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
294
|
+
@click.option('--path', '-p', default=None, help='Directory path within the chat to list (e.g., /subfolder)')
|
|
295
|
+
def list_files(chat_id, api_url, path):
|
|
296
|
+
"""List files and directories in a chat."""
|
|
297
|
+
config = get_active_config()
|
|
298
|
+
api_url = api_url or config['api_url']
|
|
299
|
+
token = config['token']
|
|
300
|
+
|
|
301
|
+
params = {}
|
|
302
|
+
if path:
|
|
303
|
+
params['path'] = path
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
response = requests.get(
|
|
307
|
+
f"{api_url}/api/v1/chats/{chat_id}/directories",
|
|
308
|
+
headers={
|
|
309
|
+
"accept": "application/json",
|
|
310
|
+
"Authorization": f"Token {token}"
|
|
311
|
+
},
|
|
312
|
+
params=params,
|
|
313
|
+
timeout=REQUEST_TIMEOUT
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if response.status_code == 200:
|
|
317
|
+
result = response.json()
|
|
318
|
+
click.echo(json.dumps(result, indent=2))
|
|
319
|
+
else:
|
|
320
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
except requests.exceptions.RequestException as e:
|
|
323
|
+
click.echo(f"Error connecting to API: {e}")
|
|
324
|
+
sys.exit(1)
|
|
325
|
+
|
|
326
|
+
@chat.command()
|
|
327
|
+
@click.argument('chat_id')
|
|
328
|
+
@click.argument('files', nargs=-1, required=True, type=click.Path(exists=True))
|
|
329
|
+
@click.option('--path', '-p', default=None, help='Directory path within the chat to upload to (e.g., /subfolder)')
|
|
330
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
331
|
+
def upload_files(chat_id, files, path, api_url):
|
|
332
|
+
"""Upload local files to a chat."""
|
|
333
|
+
config = get_active_config()
|
|
334
|
+
api_url = api_url or config['api_url']
|
|
335
|
+
token = config['token']
|
|
336
|
+
|
|
337
|
+
params = {}
|
|
338
|
+
if path:
|
|
339
|
+
params['path'] = path
|
|
340
|
+
|
|
341
|
+
with contextlib.ExitStack() as stack:
|
|
342
|
+
files_data = []
|
|
343
|
+
for file_path in files:
|
|
344
|
+
f = stack.enter_context(open(file_path, 'rb'))
|
|
345
|
+
files_data.append(('documents', f))
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
response = requests.post(
|
|
349
|
+
f"{api_url}/api/v1/chats/{chat_id}/documents",
|
|
350
|
+
headers={
|
|
351
|
+
"accept": "application/json",
|
|
352
|
+
"Authorization": f"Token {token}"
|
|
353
|
+
},
|
|
354
|
+
files=files_data,
|
|
355
|
+
params=params,
|
|
356
|
+
timeout=REQUEST_TIMEOUT
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if response.status_code == 200:
|
|
360
|
+
result = response.json()
|
|
361
|
+
click.echo(json.dumps(result, indent=2))
|
|
362
|
+
click.echo(f"Uploaded {len(files)} file(s) to chat {chat_id}")
|
|
363
|
+
else:
|
|
364
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
365
|
+
sys.exit(1)
|
|
366
|
+
except requests.exceptions.RequestException as e:
|
|
367
|
+
click.echo(f"Error connecting to API: {e}")
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
|
|
370
|
+
@chat.command()
|
|
371
|
+
@click.argument('chat_id')
|
|
372
|
+
@click.argument('document_id')
|
|
373
|
+
@click.option('--output', '-o', default=None, type=click.Path(), help='Output file path (default: use original filename)')
|
|
374
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
375
|
+
def download_file(chat_id, document_id, output, api_url):
|
|
376
|
+
"""Download a file from a chat to the local filesystem."""
|
|
377
|
+
config = get_active_config()
|
|
378
|
+
api_url = api_url or config['api_url']
|
|
379
|
+
token = config['token']
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
meta_response = requests.get(
|
|
383
|
+
f"{api_url}/api/v1/chats/{chat_id}/document/{document_id}",
|
|
384
|
+
headers={
|
|
385
|
+
"accept": "application/json",
|
|
386
|
+
"Authorization": f"Token {token}"
|
|
387
|
+
},
|
|
388
|
+
timeout=REQUEST_TIMEOUT
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if meta_response.status_code != 200:
|
|
392
|
+
click.echo(f"Error fetching document metadata: {meta_response.status_code} - {meta_response.text}")
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
|
|
395
|
+
doc_meta = meta_response.json()
|
|
396
|
+
filename = output or _sanitize_filename(
|
|
397
|
+
doc_meta.get('filename') or doc_meta.get('name') or f"document_{document_id}"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
download_response = requests.get(
|
|
401
|
+
f"{api_url}/api/v1/documents/{document_id}/download",
|
|
402
|
+
headers={
|
|
403
|
+
"Authorization": f"Token {token}"
|
|
404
|
+
},
|
|
405
|
+
stream=True,
|
|
406
|
+
timeout=REQUEST_TIMEOUT
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if download_response.status_code == 200:
|
|
410
|
+
output_path = Path(filename)
|
|
411
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
412
|
+
with open(output_path, 'wb') as f:
|
|
413
|
+
for chunk in download_response.iter_content(chunk_size=8192):
|
|
414
|
+
f.write(chunk)
|
|
415
|
+
click.echo(f"Downloaded document {document_id} to {output_path}")
|
|
416
|
+
else:
|
|
417
|
+
click.echo(f"Error downloading document: {download_response.status_code} - {download_response.text}")
|
|
418
|
+
sys.exit(1)
|
|
419
|
+
except requests.exceptions.RequestException as e:
|
|
420
|
+
click.echo(f"Error connecting to API: {e}")
|
|
421
|
+
sys.exit(1)
|
|
422
|
+
|
|
423
|
+
@chat.command()
|
|
424
|
+
@click.argument('chat_id')
|
|
425
|
+
@click.argument('local_dir', type=click.Path(exists=True, file_okay=False))
|
|
426
|
+
@click.option('--path', '-p', default=None, help='Target directory path within the chat (e.g., /subfolder)')
|
|
427
|
+
@click.option('--pattern', default='*', help='Glob pattern to filter files (default: *)')
|
|
428
|
+
@click.option('--recursive', '-r', is_flag=True, help='Include files in subdirectories')
|
|
429
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
430
|
+
def sync_to(chat_id, local_dir, path, pattern, recursive, api_url):
|
|
431
|
+
"""Sync local directory files to a chat."""
|
|
432
|
+
config = get_active_config()
|
|
433
|
+
api_url = api_url or config['api_url']
|
|
434
|
+
token = config['token']
|
|
435
|
+
|
|
436
|
+
local_path = Path(local_dir)
|
|
437
|
+
|
|
438
|
+
if recursive:
|
|
439
|
+
file_list = [f for f in local_path.rglob(pattern) if f.is_file()]
|
|
440
|
+
else:
|
|
441
|
+
file_list = [f for f in local_path.glob(pattern) if f.is_file()]
|
|
442
|
+
|
|
443
|
+
if not file_list:
|
|
444
|
+
click.echo(f"No files matching pattern '{pattern}' found in {local_dir}")
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
click.echo(f"Found {len(file_list)} file(s) to sync")
|
|
448
|
+
|
|
449
|
+
files_by_dir: Dict[str, List[Path]] = {}
|
|
450
|
+
for file in file_list:
|
|
451
|
+
rel_path = file.relative_to(local_path)
|
|
452
|
+
parent_dir = rel_path.parent.as_posix()
|
|
453
|
+
if parent_dir == '.':
|
|
454
|
+
parent_dir = ''
|
|
455
|
+
files_by_dir.setdefault(parent_dir, []).append(file)
|
|
456
|
+
|
|
457
|
+
uploaded_count = 0
|
|
458
|
+
error_count = 0
|
|
459
|
+
|
|
460
|
+
for sub_dir, dir_files in files_by_dir.items():
|
|
461
|
+
if path and sub_dir:
|
|
462
|
+
target_path = f"{path.rstrip('/')}/{sub_dir}"
|
|
463
|
+
elif path:
|
|
464
|
+
target_path = path
|
|
465
|
+
elif sub_dir:
|
|
466
|
+
target_path = f"/{sub_dir}"
|
|
467
|
+
else:
|
|
468
|
+
target_path = None
|
|
469
|
+
|
|
470
|
+
params = {}
|
|
471
|
+
if target_path:
|
|
472
|
+
params['path'] = target_path
|
|
473
|
+
|
|
474
|
+
with contextlib.ExitStack() as stack:
|
|
475
|
+
files_data = []
|
|
476
|
+
for file in dir_files:
|
|
477
|
+
f = stack.enter_context(open(file, 'rb'))
|
|
478
|
+
files_data.append(('documents', f))
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
response = requests.post(
|
|
482
|
+
f"{api_url}/api/v1/chats/{chat_id}/documents",
|
|
483
|
+
headers={
|
|
484
|
+
"accept": "application/json",
|
|
485
|
+
"Authorization": f"Token {token}"
|
|
486
|
+
},
|
|
487
|
+
files=files_data,
|
|
488
|
+
params=params,
|
|
489
|
+
timeout=REQUEST_TIMEOUT
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if response.status_code == 200:
|
|
493
|
+
uploaded_count += len(dir_files)
|
|
494
|
+
dir_display = target_path or '/'
|
|
495
|
+
click.echo(f" Uploaded {len(dir_files)} file(s) to {dir_display}")
|
|
496
|
+
else:
|
|
497
|
+
error_count += len(dir_files)
|
|
498
|
+
click.echo(f" Error uploading to {target_path or '/'}: {response.status_code} - {response.text}")
|
|
499
|
+
except requests.exceptions.RequestException as e:
|
|
500
|
+
error_count += len(dir_files)
|
|
501
|
+
click.echo(f" Error connecting to API: {e}")
|
|
502
|
+
|
|
503
|
+
click.echo(f"Sync complete: {uploaded_count} uploaded, {error_count} failed")
|
|
504
|
+
|
|
505
|
+
@chat.command()
|
|
506
|
+
@click.argument('chat_id')
|
|
507
|
+
@click.argument('local_dir', type=click.Path(file_okay=False))
|
|
508
|
+
@click.option('--path', '-p', default=None, help='Source directory path within the chat (e.g., /subfolder)')
|
|
509
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
510
|
+
def sync_from(chat_id, local_dir, path, api_url):
|
|
511
|
+
"""Sync chat files to a local directory."""
|
|
512
|
+
config = get_active_config()
|
|
513
|
+
api_url = api_url or config['api_url']
|
|
514
|
+
token = config['token']
|
|
515
|
+
|
|
516
|
+
output_dir = Path(local_dir)
|
|
517
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
518
|
+
|
|
519
|
+
downloaded_count = 0
|
|
520
|
+
error_count = 0
|
|
521
|
+
|
|
522
|
+
def download_directory(chat_path, local_base):
|
|
523
|
+
nonlocal downloaded_count, error_count
|
|
524
|
+
|
|
525
|
+
params = {}
|
|
526
|
+
if chat_path:
|
|
527
|
+
params['path'] = chat_path
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
response = requests.get(
|
|
531
|
+
f"{api_url}/api/v1/chats/{chat_id}/directories",
|
|
532
|
+
headers={
|
|
533
|
+
"accept": "application/json",
|
|
534
|
+
"Authorization": f"Token {token}"
|
|
535
|
+
},
|
|
536
|
+
params=params,
|
|
537
|
+
timeout=REQUEST_TIMEOUT
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if response.status_code != 200:
|
|
541
|
+
error_count += 1
|
|
542
|
+
click.echo(f" Error listing {chat_path or '/'}: {response.status_code} - {response.text}")
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
result = response.json()
|
|
546
|
+
dirs = result.get('dirs', [])
|
|
547
|
+
documents = result.get('documents', [])
|
|
548
|
+
|
|
549
|
+
for doc in documents:
|
|
550
|
+
doc_id = doc.get('id')
|
|
551
|
+
filename = _sanitize_filename(
|
|
552
|
+
doc.get('filename') or doc.get('name') or f"document_{doc_id}"
|
|
553
|
+
)
|
|
554
|
+
file_output = local_base / filename
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
dl_response = requests.get(
|
|
558
|
+
f"{api_url}/api/v1/documents/{doc_id}/download",
|
|
559
|
+
headers={
|
|
560
|
+
"Authorization": f"Token {token}"
|
|
561
|
+
},
|
|
562
|
+
stream=True,
|
|
563
|
+
timeout=REQUEST_TIMEOUT
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if dl_response.status_code == 200:
|
|
567
|
+
file_output.parent.mkdir(parents=True, exist_ok=True)
|
|
568
|
+
with open(file_output, 'wb') as f:
|
|
569
|
+
for chunk in dl_response.iter_content(chunk_size=8192):
|
|
570
|
+
f.write(chunk)
|
|
571
|
+
downloaded_count += 1
|
|
572
|
+
click.echo(f" Downloaded: {file_output}")
|
|
573
|
+
else:
|
|
574
|
+
error_count += 1
|
|
575
|
+
click.echo(f" Error downloading {filename}: {dl_response.status_code}")
|
|
576
|
+
except requests.exceptions.RequestException as e:
|
|
577
|
+
error_count += 1
|
|
578
|
+
click.echo(f" Error downloading {filename}: {e}")
|
|
579
|
+
|
|
580
|
+
for d in dirs:
|
|
581
|
+
dir_path = d.get('path', '')
|
|
582
|
+
dir_name = d.get('name') or dir_path.rstrip('/').split('/')[-1] or dir_path
|
|
583
|
+
sub_local = local_base / dir_name
|
|
584
|
+
sub_local.mkdir(parents=True, exist_ok=True)
|
|
585
|
+
download_directory(dir_path, sub_local)
|
|
586
|
+
|
|
587
|
+
except requests.exceptions.RequestException as e:
|
|
588
|
+
error_count += 1
|
|
589
|
+
click.echo(f" Error connecting to API: {e}")
|
|
590
|
+
|
|
591
|
+
click.echo(f"Syncing files from chat {chat_id} to {local_dir}...")
|
|
592
|
+
download_directory(path, output_dir)
|
|
593
|
+
click.echo(f"Sync complete: {downloaded_count} downloaded, {error_count} failed")
|
|
594
|
+
|
|
595
|
+
# ============================================================================
|
|
596
|
+
# Collections Subcommands
|
|
597
|
+
# ============================================================================
|
|
598
|
+
|
|
599
|
+
@cli.group()
|
|
600
|
+
def collections():
|
|
601
|
+
"""Collection file management commands."""
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
@collections.command(name='list')
|
|
605
|
+
@click.option('--page', default=1, type=int, help='Page number (default: 1)')
|
|
606
|
+
@click.option('--page-size', default=20, type=int, help='Results per page (default: 20)')
|
|
607
|
+
@click.option('--search', '-s', default=None, help='Search collections by name')
|
|
608
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
609
|
+
def collections_list(page, page_size, search, api_url):
|
|
610
|
+
"""List all collections."""
|
|
611
|
+
config = get_active_config()
|
|
612
|
+
api_url = api_url or config['api_url']
|
|
613
|
+
token = config['token']
|
|
614
|
+
|
|
615
|
+
params = {'page': page, 'page_size': page_size}
|
|
616
|
+
if search:
|
|
617
|
+
params['search'] = search
|
|
618
|
+
|
|
619
|
+
try:
|
|
620
|
+
response = requests.get(
|
|
621
|
+
f"{api_url}/api/v1/collections",
|
|
622
|
+
headers={
|
|
623
|
+
"accept": "application/json",
|
|
624
|
+
"Authorization": f"Token {token}"
|
|
625
|
+
},
|
|
626
|
+
params=params,
|
|
627
|
+
timeout=REQUEST_TIMEOUT
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
if response.status_code == 200:
|
|
631
|
+
result = response.json()
|
|
632
|
+
click.echo(json.dumps(result, indent=2))
|
|
633
|
+
else:
|
|
634
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
635
|
+
sys.exit(1)
|
|
636
|
+
except requests.exceptions.RequestException as e:
|
|
637
|
+
click.echo(f"Error connecting to API: {e}")
|
|
638
|
+
sys.exit(1)
|
|
639
|
+
|
|
640
|
+
@collections.command()
|
|
641
|
+
@click.argument('collection_id')
|
|
642
|
+
@click.option('--path', '-p', default=None, help='Directory path within the collection (e.g., /subfolder)')
|
|
643
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
644
|
+
def list_files(collection_id, path, api_url):
|
|
645
|
+
"""List files and directories in a collection."""
|
|
646
|
+
config = get_active_config()
|
|
647
|
+
api_url = api_url or config['api_url']
|
|
648
|
+
token = config['token']
|
|
649
|
+
|
|
650
|
+
params = {}
|
|
651
|
+
if path:
|
|
652
|
+
params['path'] = path
|
|
653
|
+
|
|
654
|
+
try:
|
|
655
|
+
response = requests.get(
|
|
656
|
+
f"{api_url}/api/v1/collections/{collection_id}/directories",
|
|
657
|
+
headers={
|
|
658
|
+
"accept": "application/json",
|
|
659
|
+
"Authorization": f"Token {token}"
|
|
660
|
+
},
|
|
661
|
+
params=params,
|
|
662
|
+
timeout=REQUEST_TIMEOUT
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
if response.status_code == 200:
|
|
666
|
+
result = response.json()
|
|
667
|
+
click.echo(json.dumps(result, indent=2))
|
|
668
|
+
else:
|
|
669
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
670
|
+
sys.exit(1)
|
|
671
|
+
except requests.exceptions.RequestException as e:
|
|
672
|
+
click.echo(f"Error connecting to API: {e}")
|
|
673
|
+
sys.exit(1)
|
|
674
|
+
|
|
675
|
+
@collections.command()
|
|
676
|
+
@click.argument('collection_id')
|
|
677
|
+
@click.argument('files', nargs=-1, required=True, type=click.Path(exists=True))
|
|
678
|
+
@click.option('--path', '-p', default=None, help='Directory path within the collection to upload to')
|
|
679
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
680
|
+
def upload_files(collection_id, files, path, api_url):
|
|
681
|
+
"""Upload local files to a collection."""
|
|
682
|
+
config = get_active_config()
|
|
683
|
+
api_url = api_url or config['api_url']
|
|
684
|
+
token = config['token']
|
|
685
|
+
|
|
686
|
+
form_data = {}
|
|
687
|
+
if path:
|
|
688
|
+
form_data['path'] = path
|
|
689
|
+
|
|
690
|
+
with contextlib.ExitStack() as stack:
|
|
691
|
+
files_data = []
|
|
692
|
+
for file_path in files:
|
|
693
|
+
f = stack.enter_context(open(file_path, 'rb'))
|
|
694
|
+
files_data.append(('documents', f))
|
|
695
|
+
|
|
696
|
+
try:
|
|
697
|
+
response = requests.post(
|
|
698
|
+
f"{api_url}/api/v1/collections/{collection_id}/documents",
|
|
699
|
+
headers={
|
|
700
|
+
"accept": "application/json",
|
|
701
|
+
"Authorization": f"Token {token}"
|
|
702
|
+
},
|
|
703
|
+
files=files_data,
|
|
704
|
+
data=form_data if form_data else None,
|
|
705
|
+
timeout=REQUEST_TIMEOUT
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if response.status_code == 200:
|
|
709
|
+
result = response.json()
|
|
710
|
+
click.echo(json.dumps(result, indent=2))
|
|
711
|
+
click.echo(f"Uploaded {len(files)} file(s) to collection {collection_id}")
|
|
712
|
+
else:
|
|
713
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
714
|
+
sys.exit(1)
|
|
715
|
+
except requests.exceptions.RequestException as e:
|
|
716
|
+
click.echo(f"Error connecting to API: {e}")
|
|
717
|
+
sys.exit(1)
|
|
718
|
+
|
|
719
|
+
@collections.command()
|
|
720
|
+
@click.argument('collection_id')
|
|
721
|
+
@click.argument('document_id')
|
|
722
|
+
@click.option('--output', '-o', default=None, type=click.Path(), help='Output file path (default: use original filename)')
|
|
723
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
724
|
+
def download_file(collection_id, document_id, output, api_url):
|
|
725
|
+
"""Download a file from a collection to the local filesystem."""
|
|
726
|
+
config = get_active_config()
|
|
727
|
+
api_url = api_url or config['api_url']
|
|
728
|
+
token = config['token']
|
|
729
|
+
|
|
730
|
+
try:
|
|
731
|
+
meta_response = requests.get(
|
|
732
|
+
f"{api_url}/api/v1/collections/{collection_id}",
|
|
733
|
+
headers={
|
|
734
|
+
"accept": "application/json",
|
|
735
|
+
"Authorization": f"Token {token}"
|
|
736
|
+
},
|
|
737
|
+
timeout=REQUEST_TIMEOUT
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if meta_response.status_code != 200:
|
|
741
|
+
click.echo(f"Error fetching collection details: {meta_response.status_code} - {meta_response.text}")
|
|
742
|
+
sys.exit(1)
|
|
743
|
+
|
|
744
|
+
collection_data = meta_response.json()
|
|
745
|
+
documents = collection_data.get('documents', [])
|
|
746
|
+
doc_meta = next((doc for doc in documents if str(doc.get('id')) == str(document_id)), None)
|
|
747
|
+
|
|
748
|
+
filename = output
|
|
749
|
+
if not filename and doc_meta:
|
|
750
|
+
filename = _sanitize_filename(
|
|
751
|
+
doc_meta.get('filename') or doc_meta.get('name') or f"document_{document_id}"
|
|
752
|
+
)
|
|
753
|
+
if not filename:
|
|
754
|
+
filename = f"document_{document_id}"
|
|
755
|
+
|
|
756
|
+
download_response = requests.get(
|
|
757
|
+
f"{api_url}/api/v1/documents/{document_id}/download",
|
|
758
|
+
headers={
|
|
759
|
+
"Authorization": f"Token {token}"
|
|
760
|
+
},
|
|
761
|
+
stream=True,
|
|
762
|
+
timeout=REQUEST_TIMEOUT
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
if download_response.status_code == 200:
|
|
766
|
+
output_path = Path(filename)
|
|
767
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
768
|
+
with open(output_path, 'wb') as f:
|
|
769
|
+
for chunk in download_response.iter_content(chunk_size=8192):
|
|
770
|
+
f.write(chunk)
|
|
771
|
+
click.echo(f"Downloaded document {document_id} to {output_path}")
|
|
772
|
+
else:
|
|
773
|
+
click.echo(f"Error downloading document: {download_response.status_code} - {download_response.text}")
|
|
774
|
+
sys.exit(1)
|
|
775
|
+
except requests.exceptions.RequestException as e:
|
|
776
|
+
click.echo(f"Error connecting to API: {e}")
|
|
777
|
+
sys.exit(1)
|
|
778
|
+
|
|
779
|
+
@collections.command()
|
|
780
|
+
@click.argument('collection_id')
|
|
781
|
+
@click.argument('local_dir', type=click.Path(exists=True, file_okay=False))
|
|
782
|
+
@click.option('--path', '-p', default=None, help='Target directory path within the collection')
|
|
783
|
+
@click.option('--pattern', default='*', help='Glob pattern to filter files (default: *)')
|
|
784
|
+
@click.option('--recursive', '-r', is_flag=True, help='Include files in subdirectories')
|
|
785
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
786
|
+
def sync_to(collection_id, local_dir, path, pattern, recursive, api_url):
|
|
787
|
+
"""Sync local directory files to a collection."""
|
|
788
|
+
config = get_active_config()
|
|
789
|
+
api_url = api_url or config['api_url']
|
|
790
|
+
token = config['token']
|
|
791
|
+
|
|
792
|
+
local_path = Path(local_dir)
|
|
793
|
+
|
|
794
|
+
if recursive:
|
|
795
|
+
file_list = [f for f in local_path.rglob(pattern) if f.is_file()]
|
|
796
|
+
else:
|
|
797
|
+
file_list = [f for f in local_path.glob(pattern) if f.is_file()]
|
|
798
|
+
|
|
799
|
+
if not file_list:
|
|
800
|
+
click.echo(f"No files matching pattern '{pattern}' found in {local_dir}")
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
click.echo(f"Found {len(file_list)} file(s) to sync")
|
|
804
|
+
|
|
805
|
+
files_by_dir: Dict[str, List[Path]] = {}
|
|
806
|
+
for file in file_list:
|
|
807
|
+
rel_path = file.relative_to(local_path)
|
|
808
|
+
parent_dir = rel_path.parent.as_posix()
|
|
809
|
+
if parent_dir == '.':
|
|
810
|
+
parent_dir = ''
|
|
811
|
+
files_by_dir.setdefault(parent_dir, []).append(file)
|
|
812
|
+
|
|
813
|
+
uploaded_count = 0
|
|
814
|
+
error_count = 0
|
|
815
|
+
|
|
816
|
+
for sub_dir, dir_files in files_by_dir.items():
|
|
817
|
+
if path and sub_dir:
|
|
818
|
+
target_path = f"{path.rstrip('/')}/{sub_dir}"
|
|
819
|
+
elif path:
|
|
820
|
+
target_path = path
|
|
821
|
+
elif sub_dir:
|
|
822
|
+
target_path = f"/{sub_dir}"
|
|
823
|
+
else:
|
|
824
|
+
target_path = None
|
|
825
|
+
|
|
826
|
+
form_data = {}
|
|
827
|
+
if target_path:
|
|
828
|
+
form_data['path'] = target_path
|
|
829
|
+
|
|
830
|
+
with contextlib.ExitStack() as stack:
|
|
831
|
+
files_data = []
|
|
832
|
+
for file in dir_files:
|
|
833
|
+
f = stack.enter_context(open(file, 'rb'))
|
|
834
|
+
files_data.append(('documents', f))
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
response = requests.post(
|
|
838
|
+
f"{api_url}/api/v1/collections/{collection_id}/documents",
|
|
839
|
+
headers={
|
|
840
|
+
"accept": "application/json",
|
|
841
|
+
"Authorization": f"Token {token}"
|
|
842
|
+
},
|
|
843
|
+
files=files_data,
|
|
844
|
+
data=form_data if form_data else None,
|
|
845
|
+
timeout=REQUEST_TIMEOUT
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
if response.status_code == 200:
|
|
849
|
+
uploaded_count += len(dir_files)
|
|
850
|
+
dir_display = target_path or '/'
|
|
851
|
+
click.echo(f" Uploaded {len(dir_files)} file(s) to {dir_display}")
|
|
852
|
+
else:
|
|
853
|
+
error_count += len(dir_files)
|
|
854
|
+
click.echo(f" Error uploading to {target_path or '/'}: {response.status_code} - {response.text}")
|
|
855
|
+
except requests.exceptions.RequestException as e:
|
|
856
|
+
error_count += len(dir_files)
|
|
857
|
+
click.echo(f" Error connecting to API: {e}")
|
|
858
|
+
|
|
859
|
+
click.echo(f"Sync complete: {uploaded_count} uploaded, {error_count} failed")
|
|
860
|
+
|
|
861
|
+
@collections.command()
|
|
862
|
+
@click.argument('collection_id')
|
|
863
|
+
@click.argument('local_dir', type=click.Path(file_okay=False))
|
|
864
|
+
@click.option('--path', '-p', default=None, help='Source directory path within the collection')
|
|
865
|
+
@click.option('--api-url', '-u', default=None, help='Override API URL for this request')
|
|
866
|
+
def sync_from(collection_id, local_dir, path, api_url):
|
|
867
|
+
"""Sync collection files to a local directory."""
|
|
868
|
+
config = get_active_config()
|
|
869
|
+
api_url = api_url or config['api_url']
|
|
870
|
+
token = config['token']
|
|
871
|
+
|
|
872
|
+
output_dir = Path(local_dir)
|
|
873
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
874
|
+
|
|
875
|
+
downloaded_count = 0
|
|
876
|
+
error_count = 0
|
|
877
|
+
|
|
878
|
+
def download_directory(coll_path, local_base):
|
|
879
|
+
nonlocal downloaded_count, error_count
|
|
880
|
+
|
|
881
|
+
params = {}
|
|
882
|
+
if coll_path:
|
|
883
|
+
params['path'] = coll_path
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
response = requests.get(
|
|
887
|
+
f"{api_url}/api/v1/collections/{collection_id}/directories",
|
|
888
|
+
headers={
|
|
889
|
+
"accept": "application/json",
|
|
890
|
+
"Authorization": f"Token {token}"
|
|
891
|
+
},
|
|
892
|
+
params=params,
|
|
893
|
+
timeout=REQUEST_TIMEOUT
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
if response.status_code != 200:
|
|
897
|
+
error_count += 1
|
|
898
|
+
click.echo(f" Error listing {coll_path or '/'}: {response.status_code} - {response.text}")
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
result = response.json()
|
|
902
|
+
dirs = result.get('dirs', [])
|
|
903
|
+
documents = result.get('documents', [])
|
|
904
|
+
|
|
905
|
+
for doc in documents:
|
|
906
|
+
doc_id = doc.get('id')
|
|
907
|
+
filename = _sanitize_filename(
|
|
908
|
+
doc.get('filename') or doc.get('name') or f"document_{doc_id}"
|
|
909
|
+
)
|
|
910
|
+
file_output = local_base / filename
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
dl_response = requests.get(
|
|
914
|
+
f"{api_url}/api/v1/documents/{doc_id}/download",
|
|
915
|
+
headers={
|
|
916
|
+
"Authorization": f"Token {token}"
|
|
917
|
+
},
|
|
918
|
+
stream=True,
|
|
919
|
+
timeout=REQUEST_TIMEOUT
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
if dl_response.status_code == 200:
|
|
923
|
+
file_output.parent.mkdir(parents=True, exist_ok=True)
|
|
924
|
+
with open(file_output, 'wb') as f:
|
|
925
|
+
for chunk in dl_response.iter_content(chunk_size=8192):
|
|
926
|
+
f.write(chunk)
|
|
927
|
+
downloaded_count += 1
|
|
928
|
+
click.echo(f" Downloaded: {file_output}")
|
|
929
|
+
else:
|
|
930
|
+
error_count += 1
|
|
931
|
+
click.echo(f" Error downloading {filename}: {dl_response.status_code}")
|
|
932
|
+
except requests.exceptions.RequestException as e:
|
|
933
|
+
error_count += 1
|
|
934
|
+
click.echo(f" Error downloading {filename}: {e}")
|
|
935
|
+
|
|
936
|
+
for d in dirs:
|
|
937
|
+
dir_path = d.get('path', '')
|
|
938
|
+
dir_name = d.get('name') or dir_path.rstrip('/').split('/')[-1] or dir_path
|
|
939
|
+
sub_local = local_base / dir_name
|
|
940
|
+
sub_local.mkdir(parents=True, exist_ok=True)
|
|
941
|
+
download_directory(dir_path, sub_local)
|
|
942
|
+
|
|
943
|
+
except requests.exceptions.RequestException as e:
|
|
944
|
+
error_count += 1
|
|
945
|
+
click.echo(f" Error connecting to API: {e}")
|
|
946
|
+
|
|
947
|
+
click.echo(f"Syncing files from collection {collection_id} to {local_dir}...")
|
|
948
|
+
download_directory(path, output_dir)
|
|
949
|
+
click.echo(f"Sync complete: {downloaded_count} downloaded, {error_count} failed")
|
|
950
|
+
|
|
951
|
+
# ============================================================================
|
|
952
|
+
# Helper Functions
|
|
953
|
+
# ============================================================================
|
|
954
|
+
|
|
955
|
+
def load_config() -> Dict[str, Any]:
|
|
956
|
+
"""Load configuration from file."""
|
|
957
|
+
if not CONFIG_FILE.exists():
|
|
958
|
+
return {}
|
|
959
|
+
|
|
960
|
+
try:
|
|
961
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
962
|
+
return json.load(f)
|
|
963
|
+
except json.JSONDecodeError:
|
|
964
|
+
return {}
|
|
965
|
+
|
|
966
|
+
def save_config(config: Dict[str, Any]):
|
|
967
|
+
"""Save configuration to file."""
|
|
968
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
969
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
970
|
+
json.dump(config, f, indent=2)
|
|
971
|
+
|
|
972
|
+
def get_active_config(profile: Optional[str] = None) -> Dict[str, str]:
|
|
973
|
+
"""Get active profile configuration or specific profile if provided."""
|
|
974
|
+
config = load_config()
|
|
975
|
+
|
|
976
|
+
# Use specified profile or fall back to active profile
|
|
977
|
+
if profile:
|
|
978
|
+
target_profile = profile
|
|
979
|
+
else:
|
|
980
|
+
if 'active_profile' not in config:
|
|
981
|
+
click.echo("Error: No active profile. Use 'pt configure' to set one up.")
|
|
982
|
+
sys.exit(1)
|
|
983
|
+
target_profile = config['active_profile']
|
|
984
|
+
|
|
985
|
+
if 'profiles' not in config or target_profile not in config['profiles']:
|
|
986
|
+
available = ', '.join(config.get('profiles', {}).keys())
|
|
987
|
+
if available:
|
|
988
|
+
click.echo(f"Error: Profile '{target_profile}' not found.")
|
|
989
|
+
click.echo(f"Available profiles: {available}")
|
|
990
|
+
click.echo("Use 'pt list-profiles' to see all profiles.")
|
|
991
|
+
else:
|
|
992
|
+
click.echo("Error: No profiles configured. Use 'pt configure' to create one.")
|
|
993
|
+
sys.exit(1)
|
|
994
|
+
|
|
995
|
+
profile_data = config['profiles'][target_profile]
|
|
996
|
+
return {
|
|
997
|
+
'token': profile_data['token'],
|
|
998
|
+
'api_url': profile_data.get('api_url', DEFAULT_API_URL)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if __name__ == '__main__':
|
|
1002
|
+
cli()
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: primethink-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: PrimeThink CLI - A powerful tool for interacting with PrimeThink AI API
|
|
5
|
+
Home-page: https://github.com/primethink-ai/primethink-cli
|
|
6
|
+
Author: PrimeThink
|
|
7
|
+
Author-email: PrimeThink <support@primethink.ai>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Project-URL: Homepage, https://primethink.ai
|
|
10
|
+
Project-URL: Documentation, https://primethink.ai/cli/install
|
|
11
|
+
Project-URL: Repository, https://github.com/primethink-ai/primethink-cli
|
|
12
|
+
Project-URL: Issues, https://github.com/primethink-ai/primethink-cli/issues
|
|
13
|
+
Keywords: cli,ai,primethink,api,assistant
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.7
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: click>=8.0.0
|
|
28
|
+
Requires-Dist: requests>=2.25.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
|
|
37
|
+
# PrimeThink CLI
|
|
38
|
+
|
|
39
|
+
Command Line Interface for interacting with the PrimeThink API.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or install from requirements:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -r requirements.txt
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
1. Configure your API token:
|
|
56
|
+
```bash
|
|
57
|
+
primethink configure --token YOUR_API_TOKEN
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
2. Use the CLI commands:
|
|
61
|
+
```bash
|
|
62
|
+
primethink available-actions
|
|
63
|
+
primethink execute-action --action "action-name" --message "your message"
|
|
64
|
+
primethink send-message CHAT_ID --message "your message"
|
|
65
|
+
primethink send-message --agent AGENT_ID --message "your message"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
### Token Management
|
|
71
|
+
|
|
72
|
+
The CLI supports multiple token profiles, allowing you to switch between different accounts or API endpoints.
|
|
73
|
+
|
|
74
|
+
#### Configure a new profile
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
primethink configure --token YOUR_API_TOKEN --profile default
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Options:
|
|
81
|
+
- `--token, -t`: Your API token (required)
|
|
82
|
+
- `--profile, -p`: Profile name (default: "default")
|
|
83
|
+
- `--api-url, -u`: Custom API URL (optional, default: https://api.primethink.ai)
|
|
84
|
+
|
|
85
|
+
#### List all profiles
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
primethink list-profiles
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Switch to a different profile
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
primethink use PROFILE_NAME
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Remove a profile
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
primethink remove-profile PROFILE_NAME
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
### Version
|
|
106
|
+
|
|
107
|
+
Display the CLI version:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
primethink version
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Available Actions
|
|
114
|
+
|
|
115
|
+
Get a list of available task actions:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
primethink available-actions
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
You can use a specific profile for a single request without switching the active profile:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
primethink available-actions --profile production
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
You can also override the API URL for a single request:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
primethink available-actions --api-url https://custom-api.example.com
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Execute Task Action
|
|
134
|
+
|
|
135
|
+
Execute a task action with a message and optional files:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
primethink execute-action --action "action-name" --message "your message"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
With files:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
primethink execute-action --action "action-name" --message "your message" --files file1.txt --files file2.txt
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Options:
|
|
148
|
+
- `--action, -a`: Task action name (required)
|
|
149
|
+
- `--message, -m`: Message input (required)
|
|
150
|
+
- `--files, -f`: Files to attach (can be specified multiple times)
|
|
151
|
+
- `--return-original`: Return original message (flag)
|
|
152
|
+
- `--profile, -p`: Profile to use for this request (optional)
|
|
153
|
+
- `--api-url, -u`: Override API URL for this request (optional)
|
|
154
|
+
|
|
155
|
+
### Send Message to Chat
|
|
156
|
+
|
|
157
|
+
Send a message to an existing chat by ID or mention name:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
primethink send-message CHAT_ID_OR_MENTION --message "your message"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
You can use either a chat ID or mention name:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# By chat ID
|
|
167
|
+
primethink send-message 123 --message "your message"
|
|
168
|
+
|
|
169
|
+
# By mention name
|
|
170
|
+
primethink send-message @my-assistant --message "your message"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With files:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
primethink send-message 123 --message "your message" --files file1.txt --files file2.txt
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Options:
|
|
180
|
+
- `CHAT_ID_OR_MENTION`: The chat ID or mention name (optional, positional argument)
|
|
181
|
+
- `--message, -m`: Message input (required)
|
|
182
|
+
- `--files, -f`: Files to attach (can be specified multiple times)
|
|
183
|
+
- `--async`: Asynchronous request (flag, default: false, applies only to chat messages)
|
|
184
|
+
- `--agent, -a`: Agent ID to send message to (optional, use instead of CHAT_ID_OR_MENTION)
|
|
185
|
+
- `--profile, -p`: Profile to use for this request (optional)
|
|
186
|
+
- `--api-url, -u`: Override API URL for this request (optional)
|
|
187
|
+
|
|
188
|
+
**Note:** You must provide either `CHAT_ID_OR_MENTION` or `--agent`, but not both.
|
|
189
|
+
|
|
190
|
+
### Send Message to Agent
|
|
191
|
+
|
|
192
|
+
Send a message directly to an agent using the `--agent` option:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
primethink send-message --agent 1 --message "your message"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
With files:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
primethink send-message --agent 1 --message "Analyze this data" --files data.csv
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Configuration File
|
|
205
|
+
|
|
206
|
+
The CLI stores configuration in `~/.primethink/config.json`. This file contains:
|
|
207
|
+
- Active profile
|
|
208
|
+
- All configured profiles with their tokens and optional custom API URLs
|
|
209
|
+
|
|
210
|
+
Example structure:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"active_profile": "default",
|
|
215
|
+
"profiles": {
|
|
216
|
+
"default": {
|
|
217
|
+
"token": "your-api-token"
|
|
218
|
+
},
|
|
219
|
+
"custom": {
|
|
220
|
+
"token": "your-custom-token",
|
|
221
|
+
"api_url": "https://custom-api.example.com"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
### Install development dependencies
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install -e ".[dev]"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Run tests
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
pytest
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Run tests with coverage
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
pytest --cov=primethink --cov-report=html
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Examples
|
|
248
|
+
|
|
249
|
+
### Example 1: Configure and use the CLI
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Configure your token
|
|
253
|
+
primethink configure --token YOUR_API_TOKEN
|
|
254
|
+
|
|
255
|
+
# List available actions
|
|
256
|
+
primethink available-actions
|
|
257
|
+
|
|
258
|
+
# Execute an action
|
|
259
|
+
primethink execute-action --action "summarize" --message "Summarize this text"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Example 2: Using multiple profiles
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Configure default profile
|
|
266
|
+
primethink configure --token YOUR_TOKEN --profile default
|
|
267
|
+
|
|
268
|
+
# Configure a custom API endpoint profile
|
|
269
|
+
primethink configure --token CUSTOM_TOKEN --profile production --api-url https://prod-api.example.com
|
|
270
|
+
|
|
271
|
+
# List profiles
|
|
272
|
+
primethink list-profiles
|
|
273
|
+
|
|
274
|
+
# Switch to production profile
|
|
275
|
+
primethink use production
|
|
276
|
+
|
|
277
|
+
# Or use a specific profile for a single command without switching
|
|
278
|
+
primethink available-actions --profile production
|
|
279
|
+
primethink send-message 123 --message "Hello" --profile default
|
|
280
|
+
|
|
281
|
+
# Switch back to default
|
|
282
|
+
primethink use default
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Example 3: Sending messages with files
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# Send a message to a chat with multiple files
|
|
289
|
+
primethink send-message 123 --message "Here are the documents" --files report.pdf --files data.csv
|
|
290
|
+
|
|
291
|
+
# Send message by mention name
|
|
292
|
+
primethink send-message @my-assistant --message "Review this"
|
|
293
|
+
|
|
294
|
+
# Send to agent with a specific profile
|
|
295
|
+
primethink send-message --agent 1 --message "Help me with this task" --files data.csv --profile production
|
|
296
|
+
|
|
297
|
+
# Use a custom API URL for a single request
|
|
298
|
+
primethink send-message 123 --message "Hello" --api-url https://custom-api.example.com
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT License
|
|
304
|
+
|
|
305
|
+
## Support
|
|
306
|
+
|
|
307
|
+
For issues and questions, please visit: https://primethink.ai
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
primethink.py,sha256=bv-eOYO271YQGOo_FaSFrSMrCov_visQlWj8UytobyY,37657
|
|
2
|
+
primethink_cli-1.0.0.dist-info/licenses/LICENSE,sha256=UxGf3lQlPQ5LS5gHeJ1qpVvlutxtTeNGCSdgWlCB4gg,1067
|
|
3
|
+
primethink_cli-1.0.0.dist-info/METADATA,sha256=NsxrHDdnfli-dph2RE8kGhk4E0VInlesfijtxcvYz_s,7378
|
|
4
|
+
primethink_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
primethink_cli-1.0.0.dist-info/entry_points.txt,sha256=UPFuP23YqanhsicGLir0lX-6qLlA88Z60Xl1QddOAzY,38
|
|
6
|
+
primethink_cli-1.0.0.dist-info/top_level.txt,sha256=nzEk_Oen-qTbKFMoifnqyauyzPuwYAI3kIxr7DQHC2s,11
|
|
7
|
+
primethink_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 PrimeThink
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
primethink
|