orrin-cli 0.1.7__tar.gz → 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: orrin-cli
3
+ Version: 0.1.8
4
+ Summary: Orrin CLI
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: typer
9
+ Requires-Dist: requests
10
+ Requires-Dist: platformdirs
11
+ Dynamic: license-file
12
+
13
+ # Orrin CLI v0.0.8
14
+
15
+ Please refer to [the docs](https://stellr-company.com/orrin/sdk) for more information over Orrin CLI.
@@ -0,0 +1,3 @@
1
+ # Orrin CLI v0.0.8
2
+
3
+ Please refer to [the docs](https://stellr-company.com/orrin/sdk) for more information over Orrin CLI.
@@ -0,0 +1,139 @@
1
+ import requests
2
+ from textual.app import App, ComposeResult
3
+ from textual.widgets import Header, Footer, Input, Markdown
4
+ from textual.containers import ScrollableContainer
5
+ from textual import events
6
+
7
+
8
+ class OrrinCLI(App):
9
+ CSS = """
10
+ Screen {
11
+ background: $background;
12
+ }
13
+
14
+ #chat_container {
15
+ height: 1fr;
16
+ overflow-y: auto;
17
+ padding: 1 2;
18
+ }
19
+
20
+ Markdown {
21
+ margin: 0 0 1 0;
22
+ background: $surface;
23
+ border: tall $primary;
24
+ padding: 1;
25
+ }
26
+
27
+ /* Style user messages differently */
28
+ .user {
29
+ border: tall $accent;
30
+ background: $boost;
31
+ }
32
+
33
+ #input {
34
+ dock: bottom;
35
+ margin: 1 2;
36
+ }
37
+ """
38
+
39
+ def __init__(self, file_path: str, file_content: str):
40
+ super().__init__()
41
+ self.file_path = file_path
42
+ self.file_content = file_content
43
+
44
+ self.chat_history = [
45
+ {
46
+ 'role': 'system',
47
+ 'content': '''You are **OrrinCLI**, an expert with **Orrin SDK**, Exegesis CE (Centralized Execution), and the **Stellr API**...''' # ← keep your full system prompt here
48
+ },
49
+ {
50
+ 'role': 'assistant',
51
+ 'content': f'I obtained the contents of the file that I am connected to. Here they are:\n\n```python\n{file_content}\n```'
52
+ }
53
+ ]
54
+
55
+ def compose(self) -> ComposeResult:
56
+ yield Header()
57
+ yield ScrollableContainer(id="chat_container")
58
+ yield Input(
59
+ placeholder="Chat with AI about this file... (press Enter to send)",
60
+ id="input"
61
+ )
62
+ yield Footer()
63
+
64
+ def on_mount(self) -> None:
65
+ container = self.query_one("#chat_container", ScrollableContainer)
66
+
67
+ # Initial welcome message
68
+ welcome = Markdown(
69
+ f"📄 **Connected to:** `{self.file_path}`\n"
70
+ f"File size: `{len(self.file_content)}` characters\n\n"
71
+ "Ask me anything about this file!\n"
72
+ "─" * 60
73
+ )
74
+ container.mount(welcome)
75
+
76
+ # Show the initial AI message from history
77
+ initial_ai = Markdown(self.chat_history[1]['content'])
78
+ container.mount(initial_ai)
79
+
80
+ container.scroll_end(animate=False)
81
+
82
+ def on_input_submitted(self, event: Input.Submitted) -> None:
83
+ user_msg = event.value.strip()
84
+ if not user_msg:
85
+ return
86
+
87
+ event.input.value = ""
88
+
89
+ container = self.query_one("#chat_container", ScrollableContainer)
90
+
91
+ # 1. Add User Message (styled differently)
92
+ user_md = Markdown(f"🧑 **You:** {user_msg}")
93
+ user_md.add_class("user")
94
+ container.mount(user_md)
95
+
96
+ # Add to history
97
+ self.chat_history.append({'role': 'user', 'content': user_msg})
98
+
99
+ # 2. Add a "thinking" indicator (optional but nice)
100
+ thinking = Markdown("🤖 **OrrinCLI** is thinking...")
101
+ container.mount(thinking)
102
+ container.scroll_end(animate=False)
103
+
104
+ # 3. Get real AI response
105
+ ai_response = self.prompt_ai()
106
+
107
+ # 4. Remove thinking message and add real response
108
+ thinking.remove()
109
+ ai_md = Markdown(ai_response)
110
+ container.mount(ai_md)
111
+
112
+ # Add to history
113
+ self.chat_history.append({'role': 'assistant', 'content': ai_response})
114
+
115
+ container.scroll_end(animate=False)
116
+
117
+ def prompt_ai(self) -> str:
118
+ try:
119
+ resp = requests.post(
120
+ 'http://172.20.10.2:8080/api/prompt_grok',
121
+ headers={'authorization': 'a7759iujj590poeu52199093kmeyahwpqudjw'},
122
+ json={'history': self.chat_history},
123
+ timeout=90
124
+ )
125
+ if resp.ok:
126
+ return resp.json().get('response', 'No response received from AI.')
127
+ else:
128
+ return f"**Error:** HTTP {resp.status_code} — {resp.text[:200]}"
129
+ except Exception as e:
130
+ return f"**Connection error:** {str(e)}"
131
+
132
+
133
+ # For quick testing
134
+ if __name__ == "__main__":
135
+ app = AIChatApp(
136
+ file_path="example.py",
137
+ file_content="print('Hello world')"
138
+ )
139
+ app.run()
@@ -0,0 +1,451 @@
1
+ from click import Choice
2
+ import typer
3
+ import requests
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from platformdirs import user_config_dir
8
+ import yaml, tomli, toml
9
+ from pathlib import Path
10
+ from chat_tui import OrrinCLI
11
+
12
+ app = typer.Typer(help="""
13
+ Welcome to Orrin CLI v0.1.8!\n\n
14
+
15
+ Orrin CLI enables you to perform actions within your terminal that would otherwise require
16
+ an entire dashboard. Though there is a dashboard, Orrin CLI enables a feasible development
17
+ experience for all developers, as they can swiftly run a command to upload their frontend,
18
+ perform checks on their app review status, and more!
19
+ """, no_args_is_help=True)
20
+
21
+ config = typer.Typer(help="Configure your Orrin Apps developer API with Orrin CLI")
22
+ app.add_typer(config, name='config')
23
+
24
+ ui = typer.Typer(help="UI related commands")
25
+ app.add_typer(ui, name="ui")
26
+
27
+ projects = typer.Typer(help='OrrinSDK project commands')
28
+ app.add_typer(projects, name='projects')
29
+
30
+ API_BASE = "https://stellr-company.com"#"http://192.168.1.153:8080"
31
+
32
+ APP_NAME = "orrin"
33
+ APP_AUTHOR = "orrin" # optional but recommended on Windows
34
+
35
+ config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR))
36
+ config_dir.mkdir(parents=True, exist_ok=True)
37
+
38
+ config_file = config_dir / "config.json"
39
+
40
+ # ---- Configuration based commands/helper functions ----
41
+
42
+ def save_config_data(api_key: str, email: str):
43
+ data = {"api_key": api_key, 'email': email}
44
+ with open(config_file, "w") as f:
45
+ json.dump(data, f)
46
+
47
+ def load_config():
48
+ if not config_file.exists():
49
+ return None
50
+ with open(config_file) as f:
51
+ return json.load(f)#json.load(f).get("api_key")
52
+
53
+ @config.command(help="""
54
+ Add your Developer API key and email to the Orrin CLI to perform tasks explicitly relating to your developer account.
55
+ Your email will be added to your developer account details, and correspondence regarding your submission
56
+ will take place via the dashboard and email, if the email is valid.
57
+ """)
58
+ def configure_dev():
59
+ dev_email = typer.prompt(
60
+ 'Enter your email',
61
+ hide_input=False,
62
+ confirmation_prompt=True
63
+ )
64
+
65
+ dev_api = typer.prompt(
66
+ "Enter your Developer API key",
67
+ hide_input=True,
68
+ confirmation_prompt=True,
69
+ )
70
+
71
+ resp = requests.post(
72
+ f'{API_BASE}/api/orrin_apps/developer_api_exists',
73
+ json={'developer_api_key': dev_api}
74
+ )
75
+
76
+ if not resp.ok:
77
+ typer.echo(f'❌ No developer account was found with API key:\n\n{dev_api}')
78
+ raise typer.Exit(1)
79
+
80
+ # Add email to developer account
81
+ resp = requests.post(
82
+ f'{API_BASE}/api/orrin_apps/add_email_to_developer_account',
83
+ json={'email': dev_email, 'developer_id': dev_api}
84
+ )
85
+
86
+ if not resp.ok:
87
+ typer.echo(f'❌ Something went wrong adding email to developer account. Try again.\n\nIf the problem persists, contact us:\n\thttps://stellr-company.com/contact')
88
+ raise typer.Exit(1)
89
+
90
+ save_config_data(api_key=dev_api, email=dev_email)
91
+ typer.echo("✅ Developer API and email configured with Orrin CLI")
92
+
93
+ @config.command(help='Revokes the Developer API currently configured with Orrin CLI')
94
+ def revoke_config():
95
+ if not config_file.exists():
96
+ typer.echo("ℹ️ No configuration found to revoke.")
97
+ raise typer.Exit(code=0)
98
+
99
+ try:
100
+ config_file.unlink()
101
+ typer.echo("✅ Credentials revoked. Local config removed.")
102
+ except Exception as e:
103
+ typer.echo(f"❌ Failed to revoke credentials: {e}")
104
+ raise typer.Exit(code=1)
105
+
106
+ @config.command(help='Show configured Developer API key')
107
+ def show_configuration():
108
+ if not config_file.exists():
109
+ typer.echo("ℹ️ No Developer API key configured with Orrin CLI")
110
+ raise typer.Exit(code=0)
111
+
112
+ config = load_config()
113
+
114
+ typer.echo(f'\n\tConfigured Developer API key:\n\t\t{config["api_key"]}\n\n\tConfigured Developer Email:\n\t\t{config["email"]}\n')
115
+
116
+ # -------------------------------------------------------
117
+
118
+ # ---- UI based commands/helper functions ----
119
+
120
+ @ui.command(help='''Build static output for next.js code, and generate a zip file to be uploaded.\n\nEnsure you have the following in next.config.tsx:\n\n
121
+ ------------------------------------------------------\n\n
122
+ /** @type {import('next').NextConfig} */\n\n
123
+ const nextConfig = {\n\n
124
+ output: 'export',\n\n
125
+ trailingSlash: true,\n\n
126
+ basePath: '/<your_app_name>/current',\n\n
127
+ reactStrictMode: true,\n\n
128
+ };\n\n
129
+ \n\n
130
+ module.exports = nextConfig;
131
+ ------------------------------------------------------\n\n
132
+ \n\n
133
+ Where <your_app_name> is the name of your app, which was configured in your backend.
134
+ ''')
135
+ def generate_zip():
136
+ os.system('npm install && npm run build')
137
+ os.system('cd out && zip -r ../ui-build.zip .')
138
+
139
+ @ui.command(help="Upload your frontend code to be reviewed")
140
+ def upload(
141
+ #zip_path: Path = typer.Argument(..., exists=True),
142
+ app_name: str = typer.Option(..., "--app")
143
+ ):
144
+ """
145
+ Upload a UI zip to Orrin.
146
+ """
147
+ if not os.path.isfile(os.path.join(os.getcwd(), 'ui-build.zip')):
148
+ typer.echo(f"❌ Path {os.path.join(os.getcwd(), 'ui-build.zip')} does not exist.\n\nEnsure you run `orrin ui generate-zip`, or build the zip yourself.")
149
+ raise typer.Exit(1)
150
+
151
+ dev_api = load_config()['api_key']
152
+
153
+ if dev_api is None:
154
+ typer.echo("❌ You have not configured an API key. Please register with `orrin configure configure-dev-api`.")
155
+ raise typer.Exit(1)
156
+
157
+ with open(os.path.join(os.getcwd(), 'ui-build.zip'), "rb") as f:
158
+ response = requests.post(
159
+ f"{API_BASE}/ui/upload",
160
+ files={"file": f},
161
+ data={
162
+ "developer_id": dev_api,
163
+ "app_name": app_name
164
+ }
165
+ )
166
+
167
+ if response.status_code != 200:
168
+ try:
169
+ data = response.json()
170
+ typer.echo(f'❌ Upload failed: {response.status_code}\n\nMessage: {data["Message"]}')
171
+ except:
172
+ typer.echo("❌ Upload failed")
173
+ raise typer.Exit(1)
174
+
175
+ data = response.json()
176
+ typer.echo("✅ Uploaded")
177
+ typer.echo(f"Upload ID: {data.get('upload_id')}")
178
+
179
+ # --------------------------------------------
180
+
181
+ # ---- For SDK based projects ----
182
+
183
+ @projects.command(help='Create a project for an app backend with OrrinAppsSDK')
184
+ def init_app_backend_project():
185
+ project_name = typer.prompt(
186
+ "Project Name",
187
+ hide_input=False,
188
+ confirmation_prompt=True,
189
+ )
190
+
191
+ project_desc = typer.prompt(
192
+ 'Project Description',
193
+ hide_input=False
194
+ )
195
+
196
+ project_toml = {
197
+ 'general': {
198
+ 'project_type': 'app',
199
+ 'name': project_name,
200
+ 'desc': project_desc
201
+ },
202
+ }
203
+
204
+ path = os.path.join(os.getcwd(), project_name)
205
+ os.mkdir(path)
206
+
207
+ config = load_config()
208
+
209
+ source_code = f'''
210
+ from orrinsdk import OrrinAppsSDK
211
+
212
+ orrin_sdk = OrrinAppsSDK(
213
+ developer_api='{config["api_key"]}'
214
+ )
215
+
216
+ @orrin_sdk.action(
217
+ 'ExampleBackendAction',
218
+ required_payload=[{{'name': 'a', 'type': 'any'}}]
219
+ )
220
+ def ExampleBackendAction(a):
221
+ # Do something here
222
+ return {{ 'status': 200, ... }}
223
+
224
+ orrin_sdk.finalize()
225
+ '''
226
+
227
+ with open(os.path.join(path, 'project.toml'), 'w') as file:
228
+ file.write(toml.dumps(project_toml))
229
+
230
+ with open(os.path.join(path, 'main.py'), 'w') as file:
231
+ file.write(source_code)
232
+
233
+ @projects.command(help='init-app-backend-project')
234
+ def iabp():
235
+ init_app_backend_project()
236
+
237
+ @projects.command(help='Create a project for creating a tool with OrrinToolsSDK')
238
+ def init_tool_project():
239
+ project_name = typer.prompt(
240
+ "Project Name",
241
+ hide_input=False,
242
+ confirmation_prompt=True,
243
+ )
244
+
245
+ typer.echo(f'\n\t[orrin-cli] Version for {project_name} will default to 1.0.0\n')
246
+
247
+ project_desc = typer.prompt(
248
+ 'Project Description',
249
+ hide_input=False
250
+ )
251
+
252
+ has_plugins = typer.confirm(
253
+ 'Support Plugins?'
254
+ )
255
+
256
+ project_toml = {
257
+ 'general': {
258
+ 'project_type': 'tool',
259
+ 'name': project_name,
260
+ 'desc': project_desc,
261
+ 'nuances': {}
262
+ },
263
+ 'icons': {
264
+ '24x24': '<path>',
265
+ '32x32': '<path>',
266
+ '48x48': '<path>',
267
+ '64x64': '<path>'
268
+ },
269
+ 'plugins': {
270
+ 'supported': False
271
+ },
272
+ 'metadata': {}
273
+ }
274
+
275
+ # Create project
276
+ path = os.path.join(os.getcwd(), project_name)
277
+ os.mkdir(path)
278
+
279
+ config = load_config()
280
+
281
+ plugin_entries = []
282
+
283
+ if has_plugins:
284
+ project_toml['plugins']['supported'] = True
285
+ project_toml['plugins']['config'] = 'plugins.yaml'
286
+
287
+ number_of_plugins = int(typer.prompt(
288
+ 'How Many Plugins?',
289
+ hide_input=False
290
+ ))
291
+
292
+ plugins = []
293
+
294
+ for i in range(number_of_plugins):
295
+ plugin_for = typer.prompt(
296
+ f'Plugin {i} For',
297
+ hide_input=False
298
+ )
299
+
300
+ plugin_type = typer.prompt(
301
+ f'Plugin {i} Type',
302
+ hide_input=False,
303
+ type=Choice(["internal", "external"], case_sensitive=False)
304
+ )
305
+
306
+ plugin_name = typer.prompt(
307
+ f'Plugin {i} Name',
308
+ hide_input=False
309
+ )
310
+
311
+ plugin_display_name = typer.prompt(
312
+ f'Plugin {i} Display Name',
313
+ hide_input=False
314
+ )
315
+
316
+ plugins.append({
317
+ 'for': plugin_for,
318
+ 'type': plugin_type,
319
+ 'name': plugin_name,
320
+ 'display_name': plugin_display_name,
321
+ 'actions': [
322
+ {
323
+ 'action': '',
324
+ 'helper': '',
325
+ 'type': ''
326
+ }
327
+ ]
328
+ })
329
+
330
+ if plugin_for == 'blink':
331
+ plugin_entries.append('''
332
+ @orrin_sdk.plugin_entry(
333
+ plugin='blink'
334
+ )
335
+ def blink_plugin_entry(cc: any, all_context: any):
336
+ if not isinstance(cc, dict):
337
+ return {'status': 400, 'message': 'invalid_type'}
338
+
339
+ if not isinstance(all_context, str):
340
+ return {'status': 400, 'message': 'invalid_type'}
341
+
342
+ # Handoff to Exegesis or process contextual data yourself
343
+ pass
344
+ ''')
345
+ else:
346
+ plugin_entries.append(f'''
347
+ @orrin_sdk.plugin_entry(
348
+ plugin='{plugin_for}'
349
+ )
350
+ def custom_plugin_entry(cc: any, all_context: any):
351
+ if not isinstance(cc, dict):
352
+ return {{'status': 400, 'message': 'invalid_type'}}
353
+
354
+ if not isinstance(all_context, str):
355
+ return {{'status': 400, 'message': 'invalid_type'}}
356
+
357
+ # Handoff to Exegesis or process contextual data yourself
358
+ pass
359
+ ''')
360
+
361
+ with open(os.path.join(path, 'plugins.yaml'), 'w') as file:
362
+ yaml.safe_dump({'plugins': plugins}, file)
363
+ file.close()
364
+
365
+ plugin_entries.append('') # filler to trigger another newline
366
+
367
+ source_code = f'''
368
+ from orrinsdk import OrrinToolsSDK
369
+
370
+ orrin_sdk = OrrinToolsSDK(
371
+ developer_api_key='{config["api_key"]}'
372
+ )''' + "\n".join(plugin_entries) + '''@orrin_sdk.action(
373
+ name='ExampleAction',
374
+ payload_schema='',
375
+ desc=''
376
+ )
377
+ def ExampleAction(data: any):
378
+ if not isinstance(data, dict):
379
+ return { 'status': 400, 'message': 'invalid_data' }
380
+
381
+ # Process data. Perform some sort of action
382
+
383
+ return { 'status': 200 }
384
+
385
+ orrin_sdk.finalize()
386
+ '''
387
+
388
+ with open(os.path.join(path, 'project.toml'), 'w') as file:
389
+ file.write(toml.dumps(project_toml))
390
+
391
+ with open(os.path.join(path, 'main.py'), 'w') as file:
392
+ file.write(source_code)
393
+
394
+ """@projects.command(help='Get help writing code for a project that uses OrrinSDK')
395
+ def connect_to_project():
396
+ root = Path.cwd()
397
+
398
+ # 1. Find all Python files
399
+ python_files = list(root.rglob("*.py"))
400
+
401
+ # 2. Handle empty case
402
+ if not python_files:
403
+ typer.echo("❌ No Python files exist in this project.")
404
+ raise typer.Exit()
405
+
406
+ # 3. Display files
407
+ typer.echo("\n📂 Python files found:\n")
408
+ for idx, file in enumerate(python_files, start=1):
409
+ typer.echo(f"{idx}. {file.relative_to(root)}")
410
+
411
+ # 4. Prompt selection
412
+ while True:
413
+ choice = typer.prompt("\nSelect a file by number")
414
+
415
+ if not choice.isdigit():
416
+ typer.echo("❌ Please enter a valid number.")
417
+ continue
418
+
419
+ choice = int(choice)
420
+
421
+ if 1 <= choice <= len(python_files):
422
+ selected_file = python_files[choice - 1]
423
+ break
424
+
425
+ typer.echo("❌ Number out of range.")
426
+
427
+ # 5. "Connect" result
428
+ typer.echo(f"\n✅ Connected to: {selected_file}")
429
+
430
+ content = selected_file.read_text()
431
+
432
+ # Launch TUI
433
+ c = OrrinCLI(file_path=str(selected_file), file_content=content)
434
+ c.run()"""
435
+
436
+ @projects.command(help='init-tool-project')
437
+ def itp():
438
+ init_tool_project()
439
+
440
+ """@projects.command(help='Get help creating a tool or app backend')
441
+ def orrin_cli():
442
+ from minimal_display import OrrinCLI
443
+ OrrinCLI().start()"""
444
+
445
+ # --------------------------------
446
+
447
+ def main():
448
+ app()
449
+
450
+ if __name__ == "__main__":
451
+ main()