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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pt = primethink:cli
@@ -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