kashcloud 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kashcloud-0.2.0/.gitignore +12 -0
- kashcloud-0.2.0/LICENSE +21 -0
- kashcloud-0.2.0/PKG-INFO +117 -0
- kashcloud-0.2.0/README.md +89 -0
- kashcloud-0.2.0/__init__.py +0 -0
- kashcloud-0.2.0/main.py +528 -0
- kashcloud-0.2.0/pyproject.toml +52 -0
- kashcloud-0.2.0/setup.py +22 -0
- kashcloud-0.2.0/src/kashcloud/__init__.py +3 -0
- kashcloud-0.2.0/src/kashcloud/cli.py +532 -0
kashcloud-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 KashCloud
|
|
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.
|
kashcloud-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kashcloud
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: KashCloud CLI -- AI terminal + agent infrastructure. Like Claude Code, but yours.
|
|
5
|
+
Project-URL: Homepage, https://kashagent.com/developers
|
|
6
|
+
Project-URL: Documentation, https://kashagent.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/pacoai-afk/kashagent
|
|
8
|
+
Project-URL: Issues, https://github.com/pacoai-afk/kashagent/issues
|
|
9
|
+
Author-email: KashCloud <hello@kashagent.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,cli,cloud,deploy,llm,terminal
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx>=0.28.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# KashCloud CLI
|
|
30
|
+
|
|
31
|
+
AI terminal + agent infrastructure. Like Claude Code, but yours.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install kashcloud
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Authenticate
|
|
43
|
+
kashcloud login
|
|
44
|
+
|
|
45
|
+
# Interactive AI terminal (Claude Code-style)
|
|
46
|
+
kashcloud
|
|
47
|
+
|
|
48
|
+
# One-shot message
|
|
49
|
+
kashcloud chat "explain this codebase"
|
|
50
|
+
|
|
51
|
+
# Deploy a persistent AI agent
|
|
52
|
+
kashcloud deploy agent.py
|
|
53
|
+
|
|
54
|
+
# Manage agents
|
|
55
|
+
kashcloud list
|
|
56
|
+
kashcloud logs <agent_id>
|
|
57
|
+
kashcloud stop <agent_id>
|
|
58
|
+
kashcloud restart <agent_id>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Interactive Terminal
|
|
62
|
+
|
|
63
|
+
Running `kashcloud` with no arguments launches an interactive AI terminal powered by Claude. It can:
|
|
64
|
+
|
|
65
|
+
- Read and write files in your project
|
|
66
|
+
- Run shell commands
|
|
67
|
+
- Explain and refactor code
|
|
68
|
+
- Deploy agents to KashCloud
|
|
69
|
+
|
|
70
|
+
### Terminal Commands
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
|---------|-------------|
|
|
74
|
+
| `/help` | Show all commands |
|
|
75
|
+
| `/read FILE` | Read a file |
|
|
76
|
+
| `/run CMD` | Run a shell command |
|
|
77
|
+
| `/deploy FILE` | Deploy an agent |
|
|
78
|
+
| `/agents` | List your agents |
|
|
79
|
+
| `/clear` | Clear conversation |
|
|
80
|
+
| `/exit` | Quit |
|
|
81
|
+
|
|
82
|
+
Or just type naturally: *"read main.py and explain it"*, *"fix the bug on line 42"*, *"create a REST API for user management"*.
|
|
83
|
+
|
|
84
|
+
## Agent Deployment
|
|
85
|
+
|
|
86
|
+
Deploy persistent Python agents that run 24/7 on KashCloud:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Basic deploy
|
|
90
|
+
kashcloud deploy my_agent.py
|
|
91
|
+
|
|
92
|
+
# With options
|
|
93
|
+
kashcloud deploy my_agent.py --name "price-tracker" --model claude-sonnet --memory 512
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Agents get:
|
|
97
|
+
- Persistent memory
|
|
98
|
+
- Automatic restarts
|
|
99
|
+
- Log streaming
|
|
100
|
+
- Model routing (cheap/mid/premium)
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
Config is stored at `~/.kashcloud/config.json`. You can also use environment variables:
|
|
105
|
+
|
|
106
|
+
- `KASHCLOUD_API_KEY` -- Your API key
|
|
107
|
+
- `KASHCLOUD_API_URL` -- Custom API endpoint
|
|
108
|
+
|
|
109
|
+
## Links
|
|
110
|
+
|
|
111
|
+
- Website: [kashagent.com](https://kashagent.com)
|
|
112
|
+
- Developer docs: [kashagent.com/docs](https://kashagent.com/docs)
|
|
113
|
+
- Dashboard: [kashagent.com/dashboard](https://kashagent.com/dashboard)
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# KashCloud CLI
|
|
2
|
+
|
|
3
|
+
AI terminal + agent infrastructure. Like Claude Code, but yours.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kashcloud
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Authenticate
|
|
15
|
+
kashcloud login
|
|
16
|
+
|
|
17
|
+
# Interactive AI terminal (Claude Code-style)
|
|
18
|
+
kashcloud
|
|
19
|
+
|
|
20
|
+
# One-shot message
|
|
21
|
+
kashcloud chat "explain this codebase"
|
|
22
|
+
|
|
23
|
+
# Deploy a persistent AI agent
|
|
24
|
+
kashcloud deploy agent.py
|
|
25
|
+
|
|
26
|
+
# Manage agents
|
|
27
|
+
kashcloud list
|
|
28
|
+
kashcloud logs <agent_id>
|
|
29
|
+
kashcloud stop <agent_id>
|
|
30
|
+
kashcloud restart <agent_id>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Interactive Terminal
|
|
34
|
+
|
|
35
|
+
Running `kashcloud` with no arguments launches an interactive AI terminal powered by Claude. It can:
|
|
36
|
+
|
|
37
|
+
- Read and write files in your project
|
|
38
|
+
- Run shell commands
|
|
39
|
+
- Explain and refactor code
|
|
40
|
+
- Deploy agents to KashCloud
|
|
41
|
+
|
|
42
|
+
### Terminal Commands
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
|---------|-------------|
|
|
46
|
+
| `/help` | Show all commands |
|
|
47
|
+
| `/read FILE` | Read a file |
|
|
48
|
+
| `/run CMD` | Run a shell command |
|
|
49
|
+
| `/deploy FILE` | Deploy an agent |
|
|
50
|
+
| `/agents` | List your agents |
|
|
51
|
+
| `/clear` | Clear conversation |
|
|
52
|
+
| `/exit` | Quit |
|
|
53
|
+
|
|
54
|
+
Or just type naturally: *"read main.py and explain it"*, *"fix the bug on line 42"*, *"create a REST API for user management"*.
|
|
55
|
+
|
|
56
|
+
## Agent Deployment
|
|
57
|
+
|
|
58
|
+
Deploy persistent Python agents that run 24/7 on KashCloud:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Basic deploy
|
|
62
|
+
kashcloud deploy my_agent.py
|
|
63
|
+
|
|
64
|
+
# With options
|
|
65
|
+
kashcloud deploy my_agent.py --name "price-tracker" --model claude-sonnet --memory 512
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Agents get:
|
|
69
|
+
- Persistent memory
|
|
70
|
+
- Automatic restarts
|
|
71
|
+
- Log streaming
|
|
72
|
+
- Model routing (cheap/mid/premium)
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Config is stored at `~/.kashcloud/config.json`. You can also use environment variables:
|
|
77
|
+
|
|
78
|
+
- `KASHCLOUD_API_KEY` -- Your API key
|
|
79
|
+
- `KASHCLOUD_API_URL` -- Custom API endpoint
|
|
80
|
+
|
|
81
|
+
## Links
|
|
82
|
+
|
|
83
|
+
- Website: [kashagent.com](https://kashagent.com)
|
|
84
|
+
- Developer docs: [kashagent.com/docs](https://kashagent.com/docs)
|
|
85
|
+
- Dashboard: [kashagent.com/dashboard](https://kashagent.com/dashboard)
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
File without changes
|
kashcloud-0.2.0/main.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""KashCloud CLI — Claude Code-style interactive AI terminal.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
kashcloud Interactive AI terminal (like Claude Code)
|
|
6
|
+
kashcloud chat "your message" One-shot message
|
|
7
|
+
kashcloud deploy agent.py Deploy a Python agent
|
|
8
|
+
kashcloud list List all your agents
|
|
9
|
+
kashcloud logs <agent_id> View agent logs
|
|
10
|
+
kashcloud stop <agent_id> Stop an agent
|
|
11
|
+
kashcloud restart <agent_id> Restart an agent
|
|
12
|
+
kashcloud login Authenticate with API key
|
|
13
|
+
kashcloud status <agent_id> Check agent status
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import textwrap
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import httpx
|
|
27
|
+
except ImportError:
|
|
28
|
+
print("Missing dependency. Run: pip install httpx")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
# --- Config ---
|
|
32
|
+
CONFIG_PATH = os.path.expanduser("~/.kashcloud/config.json")
|
|
33
|
+
DEFAULT_API_URL = "https://backend-production-bca0.up.railway.app"
|
|
34
|
+
VERSION = "0.2.0"
|
|
35
|
+
|
|
36
|
+
EMERALD = "\033[38;2;16;185;129m"
|
|
37
|
+
CYAN = "\033[38;2;100;220;255m"
|
|
38
|
+
DIM = "\033[2m"
|
|
39
|
+
BOLD = "\033[1m"
|
|
40
|
+
RESET = "\033[0m"
|
|
41
|
+
RED = "\033[31m"
|
|
42
|
+
YELLOW = "\033[33m"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_config() -> dict:
|
|
46
|
+
if os.path.exists(CONFIG_PATH):
|
|
47
|
+
with open(CONFIG_PATH) as f:
|
|
48
|
+
return json.load(f)
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_config(config: dict):
|
|
53
|
+
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
|
54
|
+
with open(CONFIG_PATH, "w") as f:
|
|
55
|
+
json.dump(config, f, indent=2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_api_url() -> str:
|
|
59
|
+
return get_config().get("api_url", os.getenv("KASHCLOUD_API_URL", DEFAULT_API_URL))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_api_key() -> str:
|
|
63
|
+
key = get_config().get("api_key", os.getenv("KASHCLOUD_API_KEY", ""))
|
|
64
|
+
if not key:
|
|
65
|
+
print(f"{RED}Not authenticated.{RESET} Run: {EMERALD}kashcloud login{RESET}")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
return key
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def headers() -> dict:
|
|
71
|
+
return {"Authorization": f"Bearer {get_api_key()}", "Content-Type": "application/json"}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --- Interactive REPL (Claude Code style) ---
|
|
75
|
+
|
|
76
|
+
def print_banner():
|
|
77
|
+
print(f"""
|
|
78
|
+
{EMERALD}╔══════════════════════════════════════════════════════════╗
|
|
79
|
+
║ {BOLD}KashCloud{RESET}{EMERALD} v{VERSION} ║
|
|
80
|
+
║ Interactive AI Terminal — powered by Claude ║
|
|
81
|
+
╚══════════════════════════════════════════════════════════╝{RESET}
|
|
82
|
+
|
|
83
|
+
{DIM} Model: Claude Sonnet · Memory: Persistent · Tools: Enabled
|
|
84
|
+
Type a message, or use commands:
|
|
85
|
+
/help Show commands
|
|
86
|
+
/read FILE Read a file
|
|
87
|
+
/run CMD Run a shell command
|
|
88
|
+
/deploy F Deploy an agent
|
|
89
|
+
/agents List agents
|
|
90
|
+
/clear Clear history
|
|
91
|
+
/exit Quit
|
|
92
|
+
{RESET}""")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def call_ai(messages: list[dict], api_url: str, api_key: str) -> str:
|
|
96
|
+
"""Call the KashCloud chat completions API."""
|
|
97
|
+
try:
|
|
98
|
+
resp = httpx.post(
|
|
99
|
+
f"{api_url}/v1/chat/completions",
|
|
100
|
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
101
|
+
json={
|
|
102
|
+
"model": "anthropic/claude-sonnet-4",
|
|
103
|
+
"max_tokens": 4096,
|
|
104
|
+
"messages": messages,
|
|
105
|
+
},
|
|
106
|
+
timeout=120.0,
|
|
107
|
+
)
|
|
108
|
+
if resp.status_code == 200:
|
|
109
|
+
data = resp.json()
|
|
110
|
+
return data["choices"][0]["message"]["content"]
|
|
111
|
+
elif resp.status_code == 429:
|
|
112
|
+
return f"{RED}Rate limited. Wait a moment and try again.{RESET}"
|
|
113
|
+
else:
|
|
114
|
+
return f"{RED}API error ({resp.status_code}): {resp.text[:200]}{RESET}"
|
|
115
|
+
except httpx.TimeoutException:
|
|
116
|
+
return f"{RED}Request timed out. Try again.{RESET}"
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return f"{RED}Connection error: {e}{RESET}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def process_tool_calls(response: str) -> str:
|
|
122
|
+
"""Execute any tool calls in the AI response."""
|
|
123
|
+
import re as _re
|
|
124
|
+
|
|
125
|
+
# Execute <run> commands
|
|
126
|
+
for match in _re.finditer(r'<run>(.*?)</run>', response, _re.DOTALL):
|
|
127
|
+
cmd = match.group(1).strip()
|
|
128
|
+
print(f"\n{DIM} Running: {cmd}{RESET}")
|
|
129
|
+
try:
|
|
130
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30, cwd=os.getcwd())
|
|
131
|
+
output = result.stdout + (f"\nSTDERR: {result.stderr}" if result.stderr else "")
|
|
132
|
+
response = response.replace(match.group(0), f"\n```\n{output.strip()}\n```\n")
|
|
133
|
+
except subprocess.TimeoutExpired:
|
|
134
|
+
response = response.replace(match.group(0), "\n```\nCommand timed out (30s limit)\n```\n")
|
|
135
|
+
|
|
136
|
+
# Execute <read> file reads
|
|
137
|
+
for match in _re.finditer(r'<read>(.*?)</read>', response, _re.DOTALL):
|
|
138
|
+
path = match.group(1).strip()
|
|
139
|
+
print(f"\n{DIM} Reading: {path}{RESET}")
|
|
140
|
+
try:
|
|
141
|
+
with open(os.path.expanduser(path)) as f:
|
|
142
|
+
content = f.read()
|
|
143
|
+
if len(content) > 5000:
|
|
144
|
+
content = content[:5000] + f"\n... ({len(content)} chars total, truncated)"
|
|
145
|
+
response = response.replace(match.group(0), f"\n```\n{content}\n```\n")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
response = response.replace(match.group(0), f"\n```\nError reading {path}: {e}\n```\n")
|
|
148
|
+
|
|
149
|
+
# Execute <write> file writes
|
|
150
|
+
for match in _re.finditer(r'<write path="([^"]+)">(.*?)</write>', response, _re.DOTALL):
|
|
151
|
+
path = match.group(1).strip()
|
|
152
|
+
content = match.group(2)
|
|
153
|
+
print(f"\n{DIM} Writing: {path}{RESET}")
|
|
154
|
+
try:
|
|
155
|
+
os.makedirs(os.path.dirname(os.path.expanduser(path)) or ".", exist_ok=True)
|
|
156
|
+
with open(os.path.expanduser(path), "w") as f:
|
|
157
|
+
f.write(content)
|
|
158
|
+
response = response.replace(match.group(0), f"\n{EMERALD}Wrote {len(content)} chars to {path}{RESET}\n")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
response = response.replace(match.group(0), f"\n{RED}Error writing {path}: {e}{RESET}\n")
|
|
161
|
+
|
|
162
|
+
# Clean any remaining XML tags
|
|
163
|
+
response = _re.sub(r'</?(?:run|read|write)[^>]*>', '', response)
|
|
164
|
+
return response
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def interactive_repl():
|
|
168
|
+
"""Claude Code-style interactive terminal."""
|
|
169
|
+
api_url = get_api_url()
|
|
170
|
+
api_key = get_api_key()
|
|
171
|
+
|
|
172
|
+
print_banner()
|
|
173
|
+
|
|
174
|
+
cwd = os.getcwd()
|
|
175
|
+
system_prompt = f"""You are Kash, an AI coding assistant running in the KashCloud terminal. You are like Claude Code.
|
|
176
|
+
|
|
177
|
+
CURRENT DIRECTORY: {cwd}
|
|
178
|
+
PLATFORM: {sys.platform}
|
|
179
|
+
|
|
180
|
+
YOU HAVE REAL TOOLS. Use them by embedding XML tags in your response:
|
|
181
|
+
|
|
182
|
+
1. RUN SHELL COMMANDS:
|
|
183
|
+
<run>ls -la</run>
|
|
184
|
+
<run>git status</run>
|
|
185
|
+
<run>python3 script.py</run>
|
|
186
|
+
|
|
187
|
+
2. READ FILES:
|
|
188
|
+
<read>path/to/file.py</read>
|
|
189
|
+
|
|
190
|
+
3. WRITE/CREATE FILES:
|
|
191
|
+
<write path="path/to/file.py">
|
|
192
|
+
file content here
|
|
193
|
+
</write>
|
|
194
|
+
|
|
195
|
+
RULES:
|
|
196
|
+
- When asked to look at code, READ the file first
|
|
197
|
+
- When asked to modify code, READ it first, then WRITE the updated version
|
|
198
|
+
- When asked to run something, use <run>
|
|
199
|
+
- Be concise and direct — you're in a terminal, not a chat window
|
|
200
|
+
- Show code changes as diffs or full file writes
|
|
201
|
+
- If you need to see the project structure, run: <run>find . -type f -name "*.py" | head -20</run>
|
|
202
|
+
- Never reveal API keys, tokens, or secrets
|
|
203
|
+
- You can chain multiple tool calls in one response"""
|
|
204
|
+
|
|
205
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
206
|
+
|
|
207
|
+
while True:
|
|
208
|
+
try:
|
|
209
|
+
# Prompt
|
|
210
|
+
user_input = input(f"\n{EMERALD}kash >{RESET} ").strip()
|
|
211
|
+
except (KeyboardInterrupt, EOFError):
|
|
212
|
+
print(f"\n{DIM}Goodbye.{RESET}")
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if not user_input:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# Commands
|
|
219
|
+
if user_input.startswith("/"):
|
|
220
|
+
cmd = user_input.split()[0].lower()
|
|
221
|
+
arg = user_input[len(cmd):].strip()
|
|
222
|
+
|
|
223
|
+
if cmd in ("/exit", "/quit", "/q"):
|
|
224
|
+
print(f"{DIM}Goodbye.{RESET}")
|
|
225
|
+
break
|
|
226
|
+
elif cmd == "/help":
|
|
227
|
+
print(f"""
|
|
228
|
+
{BOLD}Commands:{RESET}
|
|
229
|
+
/read FILE Read a file
|
|
230
|
+
/run CMD Run a shell command
|
|
231
|
+
/write F C Write content to file
|
|
232
|
+
/deploy FILE Deploy agent
|
|
233
|
+
/agents List your agents
|
|
234
|
+
/model MODEL Switch model
|
|
235
|
+
/clear Clear conversation
|
|
236
|
+
/exit Quit
|
|
237
|
+
|
|
238
|
+
{BOLD}Or just type naturally:{RESET}
|
|
239
|
+
"read the main.py file and explain it"
|
|
240
|
+
"create a script that scrapes product prices"
|
|
241
|
+
"what files are in this directory?"
|
|
242
|
+
"fix the bug on line 42 of server.py"
|
|
243
|
+
""")
|
|
244
|
+
continue
|
|
245
|
+
elif cmd == "/clear":
|
|
246
|
+
messages = [messages[0]] # Keep system prompt
|
|
247
|
+
print(f"{DIM}Conversation cleared.{RESET}")
|
|
248
|
+
continue
|
|
249
|
+
elif cmd == "/read" and arg:
|
|
250
|
+
try:
|
|
251
|
+
with open(os.path.expanduser(arg)) as f:
|
|
252
|
+
content = f.read()
|
|
253
|
+
print(f"\n{DIM}--- {arg} ---{RESET}")
|
|
254
|
+
print(content[:3000])
|
|
255
|
+
if len(content) > 3000:
|
|
256
|
+
print(f"{DIM}... ({len(content)} chars total){RESET}")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
259
|
+
continue
|
|
260
|
+
elif cmd == "/run" and arg:
|
|
261
|
+
try:
|
|
262
|
+
result = subprocess.run(arg, shell=True, capture_output=True, text=True, timeout=30)
|
|
263
|
+
print(result.stdout)
|
|
264
|
+
if result.stderr:
|
|
265
|
+
print(f"{YELLOW}{result.stderr}{RESET}")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
268
|
+
continue
|
|
269
|
+
elif cmd == "/agents":
|
|
270
|
+
cmd_list(argparse.Namespace())
|
|
271
|
+
continue
|
|
272
|
+
elif cmd == "/deploy" and arg:
|
|
273
|
+
cmd_deploy(argparse.Namespace(file=arg, name=None, model=None, memory=None))
|
|
274
|
+
continue
|
|
275
|
+
else:
|
|
276
|
+
print(f"{DIM}Unknown command. Type /help{RESET}")
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Send to AI
|
|
280
|
+
messages.append({"role": "user", "content": user_input})
|
|
281
|
+
|
|
282
|
+
print(f"\n{DIM}Thinking...{RESET}", end="", flush=True)
|
|
283
|
+
response = call_ai(messages, api_url, api_key)
|
|
284
|
+
print("\r" + " " * 20 + "\r", end="") # Clear "Thinking..."
|
|
285
|
+
|
|
286
|
+
# Process tool calls
|
|
287
|
+
response = process_tool_calls(response)
|
|
288
|
+
|
|
289
|
+
# Display response
|
|
290
|
+
print(f"\n{CYAN}kash:{RESET} {response}")
|
|
291
|
+
|
|
292
|
+
messages.append({"role": "assistant", "content": response})
|
|
293
|
+
|
|
294
|
+
# Keep conversation manageable
|
|
295
|
+
if len(messages) > 40:
|
|
296
|
+
messages = [messages[0]] + messages[-30:]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# --- Standard CLI commands ---
|
|
300
|
+
|
|
301
|
+
def cmd_login(args):
|
|
302
|
+
print(f"{BOLD}KashCloud Login{RESET}\n")
|
|
303
|
+
api_key = input(" API key (from dashboard): ").strip()
|
|
304
|
+
if not api_key.startswith("sk_"):
|
|
305
|
+
print(f"\n{RED}Invalid key format. Should start with sk_{RESET}")
|
|
306
|
+
sys.exit(1)
|
|
307
|
+
|
|
308
|
+
api_url = getattr(args, "api_url", None)
|
|
309
|
+
config = get_config()
|
|
310
|
+
config["api_key"] = api_key
|
|
311
|
+
if api_url:
|
|
312
|
+
config["api_url"] = api_url
|
|
313
|
+
else:
|
|
314
|
+
config["api_url"] = DEFAULT_API_URL
|
|
315
|
+
save_config(config)
|
|
316
|
+
|
|
317
|
+
# Verify the key
|
|
318
|
+
try:
|
|
319
|
+
resp = httpx.get(f"{config['api_url']}/v1/usage", headers={"Authorization": f"Bearer {api_key}"}, timeout=10)
|
|
320
|
+
if resp.status_code == 200:
|
|
321
|
+
print(f"\n{EMERALD} Authenticated successfully.{RESET}")
|
|
322
|
+
print(f" Run {BOLD}kashcloud{RESET} to start the interactive terminal.\n")
|
|
323
|
+
else:
|
|
324
|
+
print(f"\n{YELLOW} Key saved but could not verify (server returned {resp.status_code}).{RESET}")
|
|
325
|
+
except Exception:
|
|
326
|
+
print(f"\n{YELLOW} Key saved. Could not reach server to verify.{RESET}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def cmd_deploy(args):
|
|
330
|
+
file_path = args.file
|
|
331
|
+
if not os.path.exists(file_path):
|
|
332
|
+
print(f"{RED}Error: {file_path} not found{RESET}")
|
|
333
|
+
sys.exit(1)
|
|
334
|
+
|
|
335
|
+
name = args.name or os.path.basename(file_path).replace(".py", "")
|
|
336
|
+
filename = os.path.basename(file_path)
|
|
337
|
+
|
|
338
|
+
print(f"\n{DIM} Deploying {filename} as '{name}'...{RESET}")
|
|
339
|
+
|
|
340
|
+
req_path = os.path.join(os.path.dirname(file_path) or ".", "requirements.txt")
|
|
341
|
+
files = {"file": (filename, open(file_path, "rb"), "text/x-python")}
|
|
342
|
+
if os.path.exists(req_path):
|
|
343
|
+
files["requirements"] = ("requirements.txt", open(req_path, "rb"), "text/plain")
|
|
344
|
+
|
|
345
|
+
data = {"name": name}
|
|
346
|
+
if getattr(args, "model", None):
|
|
347
|
+
data["model"] = args.model
|
|
348
|
+
if getattr(args, "memory", None):
|
|
349
|
+
data["memory_mb"] = str(args.memory)
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
resp = httpx.post(
|
|
353
|
+
f"{get_api_url()}/v1/agents/deploy",
|
|
354
|
+
headers={"Authorization": f"Bearer {get_api_key()}"},
|
|
355
|
+
files=files, data=data, timeout=120.0,
|
|
356
|
+
)
|
|
357
|
+
if resp.status_code == 200:
|
|
358
|
+
d = resp.json()
|
|
359
|
+
print(f"\n{EMERALD} Agent deployed!{RESET}")
|
|
360
|
+
print(f" ID: {d['agent_id']}")
|
|
361
|
+
print(f" Status: {d['status']}")
|
|
362
|
+
print(f"\n Logs: kashcloud logs {d['agent_id']}")
|
|
363
|
+
print(f" Stop: kashcloud stop {d['agent_id']}")
|
|
364
|
+
else:
|
|
365
|
+
print(f"\n{RED} Deploy failed: {resp.text[:200]}{RESET}")
|
|
366
|
+
except Exception as e:
|
|
367
|
+
print(f"{RED} Error: {e}{RESET}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def cmd_status(args):
|
|
371
|
+
try:
|
|
372
|
+
resp = httpx.get(f"{get_api_url()}/v1/agents/{args.agent_id}/status", headers=headers())
|
|
373
|
+
if resp.status_code == 200:
|
|
374
|
+
d = resp.json()
|
|
375
|
+
status_color = EMERALD if d.get("status") == "running" else RED if d.get("status") == "crashed" else DIM
|
|
376
|
+
print(f"\n Agent: {d.get('name', args.agent_id)}")
|
|
377
|
+
print(f" ID: {d.get('agent_id', args.agent_id)}")
|
|
378
|
+
print(f" Status: {status_color}{d.get('status', 'unknown')}{RESET}")
|
|
379
|
+
print(f" Model: {d.get('model', 'default')}")
|
|
380
|
+
else:
|
|
381
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
382
|
+
except Exception as e:
|
|
383
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def cmd_logs(args):
|
|
387
|
+
try:
|
|
388
|
+
resp = httpx.get(
|
|
389
|
+
f"{get_api_url()}/v1/agents/{args.agent_id}/logs",
|
|
390
|
+
headers=headers(), params={"tail": getattr(args, "tail", 100)},
|
|
391
|
+
)
|
|
392
|
+
if resp.status_code == 200:
|
|
393
|
+
logs = resp.json().get("logs", [])
|
|
394
|
+
if not logs:
|
|
395
|
+
print(f"{DIM} No logs yet.{RESET}")
|
|
396
|
+
else:
|
|
397
|
+
for log in logs:
|
|
398
|
+
ts = log.get("created_at", "")
|
|
399
|
+
stream = log.get("stream", "stdout")
|
|
400
|
+
content = log.get("content", "")
|
|
401
|
+
color = RED if stream == "stderr" else ""
|
|
402
|
+
print(f"{DIM}[{ts}]{RESET} {color}{content}{RESET}")
|
|
403
|
+
else:
|
|
404
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
405
|
+
except Exception as e:
|
|
406
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def cmd_stop(args):
|
|
410
|
+
try:
|
|
411
|
+
resp = httpx.post(f"{get_api_url()}/v1/agents/{args.agent_id}/stop", headers=headers())
|
|
412
|
+
if resp.status_code == 200:
|
|
413
|
+
print(f"{EMERALD} Agent {args.agent_id} stopped.{RESET}")
|
|
414
|
+
else:
|
|
415
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
416
|
+
except Exception as e:
|
|
417
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cmd_restart(args):
|
|
421
|
+
try:
|
|
422
|
+
resp = httpx.post(f"{get_api_url()}/v1/agents/{args.agent_id}/restart", headers=headers())
|
|
423
|
+
if resp.status_code == 200:
|
|
424
|
+
print(f"{EMERALD} Agent {args.agent_id} restarted.{RESET}")
|
|
425
|
+
else:
|
|
426
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
427
|
+
except Exception as e:
|
|
428
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def cmd_list(args):
|
|
432
|
+
try:
|
|
433
|
+
resp = httpx.get(f"{get_api_url()}/v1/agents", headers=headers())
|
|
434
|
+
if resp.status_code == 200:
|
|
435
|
+
agents = resp.json().get("agents", [])
|
|
436
|
+
if not agents:
|
|
437
|
+
print(f"\n{DIM} No agents deployed.{RESET}")
|
|
438
|
+
print(f" Run: {EMERALD}kashcloud deploy <file.py>{RESET}\n")
|
|
439
|
+
return
|
|
440
|
+
print(f"\n {'ID':<20} {'Name':<20} {'Status':<12} {'Model':<30}")
|
|
441
|
+
print(f" {'-'*82}")
|
|
442
|
+
for a in agents:
|
|
443
|
+
sc = EMERALD if a["status"] == "running" else RED if a["status"] == "crashed" else DIM
|
|
444
|
+
print(f" {a['id']:<20} {a['name']:<20} {sc}{a['status']:<12}{RESET} {a.get('model', ''):<30}")
|
|
445
|
+
print()
|
|
446
|
+
else:
|
|
447
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
448
|
+
except Exception as e:
|
|
449
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def cmd_chat(args):
|
|
453
|
+
"""One-shot chat message."""
|
|
454
|
+
api_url = get_api_url()
|
|
455
|
+
api_key = get_api_key()
|
|
456
|
+
message = " ".join(args.message)
|
|
457
|
+
if not message:
|
|
458
|
+
print(f"{RED}No message provided.{RESET}")
|
|
459
|
+
sys.exit(1)
|
|
460
|
+
|
|
461
|
+
messages = [
|
|
462
|
+
{"role": "system", "content": f"You are Kash, an AI assistant. Current dir: {os.getcwd()}. Be concise."},
|
|
463
|
+
{"role": "user", "content": message},
|
|
464
|
+
]
|
|
465
|
+
response = call_ai(messages, api_url, api_key)
|
|
466
|
+
response = process_tool_calls(response)
|
|
467
|
+
print(response)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def main():
|
|
471
|
+
parser = argparse.ArgumentParser(
|
|
472
|
+
prog="kashcloud",
|
|
473
|
+
description="KashCloud — AI terminal + agent infrastructure.",
|
|
474
|
+
)
|
|
475
|
+
sub = parser.add_subparsers(dest="command")
|
|
476
|
+
|
|
477
|
+
# login
|
|
478
|
+
p_login = sub.add_parser("login", help="Authenticate with API key")
|
|
479
|
+
p_login.add_argument("--api-url", help="Custom API URL")
|
|
480
|
+
|
|
481
|
+
# chat (one-shot)
|
|
482
|
+
p_chat = sub.add_parser("chat", help="Send a one-shot message")
|
|
483
|
+
p_chat.add_argument("message", nargs="+", help="Your message")
|
|
484
|
+
|
|
485
|
+
# deploy
|
|
486
|
+
p_deploy = sub.add_parser("deploy", help="Deploy an agent")
|
|
487
|
+
p_deploy.add_argument("file", help="Python file to deploy")
|
|
488
|
+
p_deploy.add_argument("--name", "-n", help="Agent name")
|
|
489
|
+
p_deploy.add_argument("--model", "-m", help="AI model to use")
|
|
490
|
+
p_deploy.add_argument("--memory", type=int, help="Memory limit in MB")
|
|
491
|
+
|
|
492
|
+
# status
|
|
493
|
+
p_status = sub.add_parser("status", help="Agent status")
|
|
494
|
+
p_status.add_argument("agent_id")
|
|
495
|
+
|
|
496
|
+
# logs
|
|
497
|
+
p_logs = sub.add_parser("logs", help="View logs")
|
|
498
|
+
p_logs.add_argument("agent_id")
|
|
499
|
+
p_logs.add_argument("--tail", "-t", type=int, default=100)
|
|
500
|
+
|
|
501
|
+
# stop
|
|
502
|
+
p_stop = sub.add_parser("stop", help="Stop agent")
|
|
503
|
+
p_stop.add_argument("agent_id")
|
|
504
|
+
|
|
505
|
+
# restart
|
|
506
|
+
p_restart = sub.add_parser("restart", help="Restart agent")
|
|
507
|
+
p_restart.add_argument("agent_id")
|
|
508
|
+
|
|
509
|
+
# list
|
|
510
|
+
sub.add_parser("list", help="List agents")
|
|
511
|
+
|
|
512
|
+
args = parser.parse_args()
|
|
513
|
+
|
|
514
|
+
# No command = interactive REPL
|
|
515
|
+
if not args.command:
|
|
516
|
+
interactive_repl()
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
cmds = {
|
|
520
|
+
"login": cmd_login, "chat": cmd_chat, "deploy": cmd_deploy,
|
|
521
|
+
"status": cmd_status, "logs": cmd_logs, "stop": cmd_stop,
|
|
522
|
+
"restart": cmd_restart, "list": cmd_list,
|
|
523
|
+
}
|
|
524
|
+
cmds[args.command](args)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == "__main__":
|
|
528
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kashcloud"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "KashCloud CLI -- AI terminal + agent infrastructure. Like Claude Code, but yours."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "KashCloud", email = "hello@kashagent.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai",
|
|
17
|
+
"agents",
|
|
18
|
+
"cli",
|
|
19
|
+
"cloud",
|
|
20
|
+
"deploy",
|
|
21
|
+
"llm",
|
|
22
|
+
"terminal",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Environment :: Console",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Software Development :: Libraries",
|
|
36
|
+
"Topic :: Software Development :: Build Tools",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"httpx>=0.28.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
kashcloud = "kashcloud.cli:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://kashagent.com/developers"
|
|
47
|
+
Documentation = "https://kashagent.com/docs"
|
|
48
|
+
Repository = "https://github.com/pacoai-afk/kashagent"
|
|
49
|
+
Issues = "https://github.com/pacoai-afk/kashagent/issues"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/kashcloud"]
|
kashcloud-0.2.0/setup.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="kashcloud",
|
|
5
|
+
version="0.2.0",
|
|
6
|
+
description="KashCloud — AI terminal + agent infrastructure. Like Claude Code, but yours.",
|
|
7
|
+
long_description="Interactive AI terminal powered by Claude. Deploy persistent AI agents with one command. Model routing, memory, billing, scaling — all included.",
|
|
8
|
+
author="KashCloud",
|
|
9
|
+
author_email="hello@kashagent.com",
|
|
10
|
+
url="https://kashagent.com/developers",
|
|
11
|
+
py_modules=["main"],
|
|
12
|
+
install_requires=["httpx>=0.28.0"],
|
|
13
|
+
entry_points={"console_scripts": ["kashcloud=main:main"]},
|
|
14
|
+
python_requires=">=3.10",
|
|
15
|
+
classifiers=[
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
],
|
|
22
|
+
)
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""KashCloud CLI -- Claude Code-style interactive AI terminal.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
kashcloud Interactive AI terminal (like Claude Code)
|
|
6
|
+
kashcloud chat "your message" One-shot message
|
|
7
|
+
kashcloud deploy agent.py Deploy a Python agent
|
|
8
|
+
kashcloud list List all your agents
|
|
9
|
+
kashcloud logs <agent_id> View agent logs
|
|
10
|
+
kashcloud stop <agent_id> Stop an agent
|
|
11
|
+
kashcloud restart <agent_id> Restart an agent
|
|
12
|
+
kashcloud login Authenticate with API key
|
|
13
|
+
kashcloud status <agent_id> Check agent status
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import textwrap
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import httpx
|
|
27
|
+
except ImportError:
|
|
28
|
+
print("Missing dependency. Run: pip install httpx")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
from kashcloud import __version__
|
|
32
|
+
|
|
33
|
+
# --- Config ---
|
|
34
|
+
CONFIG_PATH = os.path.expanduser("~/.kashcloud/config.json")
|
|
35
|
+
DEFAULT_API_URL = "https://backend-production-bca0.up.railway.app"
|
|
36
|
+
VERSION = __version__
|
|
37
|
+
|
|
38
|
+
EMERALD = "\033[38;2;16;185;129m"
|
|
39
|
+
CYAN = "\033[38;2;100;220;255m"
|
|
40
|
+
DIM = "\033[2m"
|
|
41
|
+
BOLD = "\033[1m"
|
|
42
|
+
RESET = "\033[0m"
|
|
43
|
+
RED = "\033[31m"
|
|
44
|
+
YELLOW = "\033[33m"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_config() -> dict:
|
|
48
|
+
if os.path.exists(CONFIG_PATH):
|
|
49
|
+
with open(CONFIG_PATH) as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_config(config: dict):
|
|
55
|
+
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
|
56
|
+
with open(CONFIG_PATH, "w") as f:
|
|
57
|
+
json.dump(config, f, indent=2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_api_url() -> str:
|
|
61
|
+
return get_config().get("api_url", os.getenv("KASHCLOUD_API_URL", DEFAULT_API_URL))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_api_key() -> str:
|
|
65
|
+
key = get_config().get("api_key", os.getenv("KASHCLOUD_API_KEY", ""))
|
|
66
|
+
if not key:
|
|
67
|
+
print(f"{RED}Not authenticated.{RESET} Run: {EMERALD}kashcloud login{RESET}")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
return key
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def headers() -> dict:
|
|
73
|
+
return {"Authorization": f"Bearer {get_api_key()}", "Content-Type": "application/json"}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- Interactive REPL (Claude Code style) ---
|
|
77
|
+
|
|
78
|
+
def print_banner():
|
|
79
|
+
print(f"""
|
|
80
|
+
{EMERALD}+----------------------------------------------------------+
|
|
81
|
+
| {BOLD}KashCloud{RESET}{EMERALD} v{VERSION} |
|
|
82
|
+
| Interactive AI Terminal -- powered by Claude |
|
|
83
|
+
+----------------------------------------------------------+{RESET}
|
|
84
|
+
|
|
85
|
+
{DIM} Model: Claude Sonnet - Memory: Persistent - Tools: Enabled
|
|
86
|
+
Type a message, or use commands:
|
|
87
|
+
/help Show commands
|
|
88
|
+
/read FILE Read a file
|
|
89
|
+
/run CMD Run a shell command
|
|
90
|
+
/deploy F Deploy an agent
|
|
91
|
+
/agents List agents
|
|
92
|
+
/clear Clear history
|
|
93
|
+
/exit Quit
|
|
94
|
+
{RESET}""")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def call_ai(messages: list[dict], api_url: str, api_key: str) -> str:
|
|
98
|
+
"""Call the KashCloud chat completions API."""
|
|
99
|
+
try:
|
|
100
|
+
resp = httpx.post(
|
|
101
|
+
f"{api_url}/v1/chat/completions",
|
|
102
|
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
103
|
+
json={
|
|
104
|
+
"model": "anthropic/claude-sonnet-4",
|
|
105
|
+
"max_tokens": 4096,
|
|
106
|
+
"messages": messages,
|
|
107
|
+
},
|
|
108
|
+
timeout=120.0,
|
|
109
|
+
)
|
|
110
|
+
if resp.status_code == 200:
|
|
111
|
+
data = resp.json()
|
|
112
|
+
return data["choices"][0]["message"]["content"]
|
|
113
|
+
elif resp.status_code == 429:
|
|
114
|
+
return f"{RED}Rate limited. Wait a moment and try again.{RESET}"
|
|
115
|
+
else:
|
|
116
|
+
return f"{RED}API error ({resp.status_code}): {resp.text[:200]}{RESET}"
|
|
117
|
+
except httpx.TimeoutException:
|
|
118
|
+
return f"{RED}Request timed out. Try again.{RESET}"
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return f"{RED}Connection error: {e}{RESET}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def process_tool_calls(response: str) -> str:
|
|
124
|
+
"""Execute any tool calls in the AI response."""
|
|
125
|
+
import re as _re
|
|
126
|
+
|
|
127
|
+
# Execute <run> commands
|
|
128
|
+
for match in _re.finditer(r'<run>(.*?)</run>', response, _re.DOTALL):
|
|
129
|
+
cmd = match.group(1).strip()
|
|
130
|
+
print(f"\n{DIM} Running: {cmd}{RESET}")
|
|
131
|
+
try:
|
|
132
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30, cwd=os.getcwd())
|
|
133
|
+
output = result.stdout + (f"\nSTDERR: {result.stderr}" if result.stderr else "")
|
|
134
|
+
response = response.replace(match.group(0), f"\n```\n{output.strip()}\n```\n")
|
|
135
|
+
except subprocess.TimeoutExpired:
|
|
136
|
+
response = response.replace(match.group(0), "\n```\nCommand timed out (30s limit)\n```\n")
|
|
137
|
+
|
|
138
|
+
# Execute <read> file reads
|
|
139
|
+
for match in _re.finditer(r'<read>(.*?)</read>', response, _re.DOTALL):
|
|
140
|
+
path = match.group(1).strip()
|
|
141
|
+
print(f"\n{DIM} Reading: {path}{RESET}")
|
|
142
|
+
try:
|
|
143
|
+
with open(os.path.expanduser(path)) as f:
|
|
144
|
+
content = f.read()
|
|
145
|
+
if len(content) > 5000:
|
|
146
|
+
content = content[:5000] + f"\n... ({len(content)} chars total, truncated)"
|
|
147
|
+
response = response.replace(match.group(0), f"\n```\n{content}\n```\n")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
response = response.replace(match.group(0), f"\n```\nError reading {path}: {e}\n```\n")
|
|
150
|
+
|
|
151
|
+
# Execute <write> file writes
|
|
152
|
+
for match in _re.finditer(r'<write path="([^"]+)">(.*?)</write>', response, _re.DOTALL):
|
|
153
|
+
path = match.group(1).strip()
|
|
154
|
+
content = match.group(2)
|
|
155
|
+
print(f"\n{DIM} Writing: {path}{RESET}")
|
|
156
|
+
try:
|
|
157
|
+
os.makedirs(os.path.dirname(os.path.expanduser(path)) or ".", exist_ok=True)
|
|
158
|
+
with open(os.path.expanduser(path), "w") as f:
|
|
159
|
+
f.write(content)
|
|
160
|
+
response = response.replace(match.group(0), f"\n{EMERALD}Wrote {len(content)} chars to {path}{RESET}\n")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
response = response.replace(match.group(0), f"\n{RED}Error writing {path}: {e}{RESET}\n")
|
|
163
|
+
|
|
164
|
+
# Clean any remaining XML tags
|
|
165
|
+
response = _re.sub(r'</?(?:run|read|write)[^>]*>', '', response)
|
|
166
|
+
return response
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def interactive_repl():
|
|
170
|
+
"""Claude Code-style interactive terminal."""
|
|
171
|
+
api_url = get_api_url()
|
|
172
|
+
api_key = get_api_key()
|
|
173
|
+
|
|
174
|
+
print_banner()
|
|
175
|
+
|
|
176
|
+
cwd = os.getcwd()
|
|
177
|
+
system_prompt = f"""You are Kash, an AI coding assistant running in the KashCloud terminal. You are like Claude Code.
|
|
178
|
+
|
|
179
|
+
CURRENT DIRECTORY: {cwd}
|
|
180
|
+
PLATFORM: {sys.platform}
|
|
181
|
+
|
|
182
|
+
YOU HAVE REAL TOOLS. Use them by embedding XML tags in your response:
|
|
183
|
+
|
|
184
|
+
1. RUN SHELL COMMANDS:
|
|
185
|
+
<run>ls -la</run>
|
|
186
|
+
<run>git status</run>
|
|
187
|
+
<run>python3 script.py</run>
|
|
188
|
+
|
|
189
|
+
2. READ FILES:
|
|
190
|
+
<read>path/to/file.py</read>
|
|
191
|
+
|
|
192
|
+
3. WRITE/CREATE FILES:
|
|
193
|
+
<write path="path/to/file.py">
|
|
194
|
+
file content here
|
|
195
|
+
</write>
|
|
196
|
+
|
|
197
|
+
RULES:
|
|
198
|
+
- When asked to look at code, READ the file first
|
|
199
|
+
- When asked to modify code, READ it first, then WRITE the updated version
|
|
200
|
+
- When asked to run something, use <run>
|
|
201
|
+
- Be concise and direct -- you're in a terminal, not a chat window
|
|
202
|
+
- Show code changes as diffs or full file writes
|
|
203
|
+
- If you need to see the project structure, run: <run>find . -type f -name "*.py" | head -20</run>
|
|
204
|
+
- Never reveal API keys, tokens, or secrets
|
|
205
|
+
- You can chain multiple tool calls in one response"""
|
|
206
|
+
|
|
207
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
208
|
+
|
|
209
|
+
while True:
|
|
210
|
+
try:
|
|
211
|
+
# Prompt
|
|
212
|
+
user_input = input(f"\n{EMERALD}kash >{RESET} ").strip()
|
|
213
|
+
except (KeyboardInterrupt, EOFError):
|
|
214
|
+
print(f"\n{DIM}Goodbye.{RESET}")
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
if not user_input:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Commands
|
|
221
|
+
if user_input.startswith("/"):
|
|
222
|
+
cmd = user_input.split()[0].lower()
|
|
223
|
+
arg = user_input[len(cmd):].strip()
|
|
224
|
+
|
|
225
|
+
if cmd in ("/exit", "/quit", "/q"):
|
|
226
|
+
print(f"{DIM}Goodbye.{RESET}")
|
|
227
|
+
break
|
|
228
|
+
elif cmd == "/help":
|
|
229
|
+
print(f"""
|
|
230
|
+
{BOLD}Commands:{RESET}
|
|
231
|
+
/read FILE Read a file
|
|
232
|
+
/run CMD Run a shell command
|
|
233
|
+
/write F C Write content to file
|
|
234
|
+
/deploy FILE Deploy agent
|
|
235
|
+
/agents List your agents
|
|
236
|
+
/model MODEL Switch model
|
|
237
|
+
/clear Clear conversation
|
|
238
|
+
/exit Quit
|
|
239
|
+
|
|
240
|
+
{BOLD}Or just type naturally:{RESET}
|
|
241
|
+
"read the main.py file and explain it"
|
|
242
|
+
"create a script that scrapes product prices"
|
|
243
|
+
"what files are in this directory?"
|
|
244
|
+
"fix the bug on line 42 of server.py"
|
|
245
|
+
""")
|
|
246
|
+
continue
|
|
247
|
+
elif cmd == "/clear":
|
|
248
|
+
messages = [messages[0]] # Keep system prompt
|
|
249
|
+
print(f"{DIM}Conversation cleared.{RESET}")
|
|
250
|
+
continue
|
|
251
|
+
elif cmd == "/read" and arg:
|
|
252
|
+
try:
|
|
253
|
+
with open(os.path.expanduser(arg)) as f:
|
|
254
|
+
content = f.read()
|
|
255
|
+
print(f"\n{DIM}--- {arg} ---{RESET}")
|
|
256
|
+
print(content[:3000])
|
|
257
|
+
if len(content) > 3000:
|
|
258
|
+
print(f"{DIM}... ({len(content)} chars total){RESET}")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
261
|
+
continue
|
|
262
|
+
elif cmd == "/run" and arg:
|
|
263
|
+
try:
|
|
264
|
+
result = subprocess.run(arg, shell=True, capture_output=True, text=True, timeout=30)
|
|
265
|
+
print(result.stdout)
|
|
266
|
+
if result.stderr:
|
|
267
|
+
print(f"{YELLOW}{result.stderr}{RESET}")
|
|
268
|
+
except Exception as e:
|
|
269
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
270
|
+
continue
|
|
271
|
+
elif cmd == "/agents":
|
|
272
|
+
cmd_list(argparse.Namespace())
|
|
273
|
+
continue
|
|
274
|
+
elif cmd == "/deploy" and arg:
|
|
275
|
+
cmd_deploy(argparse.Namespace(file=arg, name=None, model=None, memory=None))
|
|
276
|
+
continue
|
|
277
|
+
else:
|
|
278
|
+
print(f"{DIM}Unknown command. Type /help{RESET}")
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Send to AI
|
|
282
|
+
messages.append({"role": "user", "content": user_input})
|
|
283
|
+
|
|
284
|
+
print(f"\n{DIM}Thinking...{RESET}", end="", flush=True)
|
|
285
|
+
response = call_ai(messages, api_url, api_key)
|
|
286
|
+
print("\r" + " " * 20 + "\r", end="") # Clear "Thinking..."
|
|
287
|
+
|
|
288
|
+
# Process tool calls
|
|
289
|
+
response = process_tool_calls(response)
|
|
290
|
+
|
|
291
|
+
# Display response
|
|
292
|
+
print(f"\n{CYAN}kash:{RESET} {response}")
|
|
293
|
+
|
|
294
|
+
messages.append({"role": "assistant", "content": response})
|
|
295
|
+
|
|
296
|
+
# Keep conversation manageable
|
|
297
|
+
if len(messages) > 40:
|
|
298
|
+
messages = [messages[0]] + messages[-30:]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# --- Standard CLI commands ---
|
|
302
|
+
|
|
303
|
+
def cmd_login(args):
|
|
304
|
+
print(f"{BOLD}KashCloud Login{RESET}\n")
|
|
305
|
+
api_key = input(" API key (from dashboard): ").strip()
|
|
306
|
+
if not api_key.startswith("sk_"):
|
|
307
|
+
print(f"\n{RED}Invalid key format. Should start with sk_{RESET}")
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
|
|
310
|
+
api_url = getattr(args, "api_url", None)
|
|
311
|
+
config = get_config()
|
|
312
|
+
config["api_key"] = api_key
|
|
313
|
+
if api_url:
|
|
314
|
+
config["api_url"] = api_url
|
|
315
|
+
else:
|
|
316
|
+
config["api_url"] = DEFAULT_API_URL
|
|
317
|
+
save_config(config)
|
|
318
|
+
|
|
319
|
+
# Verify the key
|
|
320
|
+
try:
|
|
321
|
+
resp = httpx.get(f"{config['api_url']}/v1/usage", headers={"Authorization": f"Bearer {api_key}"}, timeout=10)
|
|
322
|
+
if resp.status_code == 200:
|
|
323
|
+
print(f"\n{EMERALD} Authenticated successfully.{RESET}")
|
|
324
|
+
print(f" Run {BOLD}kashcloud{RESET} to start the interactive terminal.\n")
|
|
325
|
+
else:
|
|
326
|
+
print(f"\n{YELLOW} Key saved but could not verify (server returned {resp.status_code}).{RESET}")
|
|
327
|
+
except Exception:
|
|
328
|
+
print(f"\n{YELLOW} Key saved. Could not reach server to verify.{RESET}")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_deploy(args):
|
|
332
|
+
file_path = args.file
|
|
333
|
+
if not os.path.exists(file_path):
|
|
334
|
+
print(f"{RED}Error: {file_path} not found{RESET}")
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
|
|
337
|
+
name = args.name or os.path.basename(file_path).replace(".py", "")
|
|
338
|
+
filename = os.path.basename(file_path)
|
|
339
|
+
|
|
340
|
+
print(f"\n{DIM} Deploying {filename} as '{name}'...{RESET}")
|
|
341
|
+
|
|
342
|
+
req_path = os.path.join(os.path.dirname(file_path) or ".", "requirements.txt")
|
|
343
|
+
files = {"file": (filename, open(file_path, "rb"), "text/x-python")}
|
|
344
|
+
if os.path.exists(req_path):
|
|
345
|
+
files["requirements"] = ("requirements.txt", open(req_path, "rb"), "text/plain")
|
|
346
|
+
|
|
347
|
+
data = {"name": name}
|
|
348
|
+
if getattr(args, "model", None):
|
|
349
|
+
data["model"] = args.model
|
|
350
|
+
if getattr(args, "memory", None):
|
|
351
|
+
data["memory_mb"] = str(args.memory)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
resp = httpx.post(
|
|
355
|
+
f"{get_api_url()}/v1/agents/deploy",
|
|
356
|
+
headers={"Authorization": f"Bearer {get_api_key()}"},
|
|
357
|
+
files=files, data=data, timeout=120.0,
|
|
358
|
+
)
|
|
359
|
+
if resp.status_code == 200:
|
|
360
|
+
d = resp.json()
|
|
361
|
+
print(f"\n{EMERALD} Agent deployed!{RESET}")
|
|
362
|
+
print(f" ID: {d['agent_id']}")
|
|
363
|
+
print(f" Status: {d['status']}")
|
|
364
|
+
print(f"\n Logs: kashcloud logs {d['agent_id']}")
|
|
365
|
+
print(f" Stop: kashcloud stop {d['agent_id']}")
|
|
366
|
+
else:
|
|
367
|
+
print(f"\n{RED} Deploy failed: {resp.text[:200]}{RESET}")
|
|
368
|
+
except Exception as e:
|
|
369
|
+
print(f"{RED} Error: {e}{RESET}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def cmd_status(args):
|
|
373
|
+
try:
|
|
374
|
+
resp = httpx.get(f"{get_api_url()}/v1/agents/{args.agent_id}/status", headers=headers())
|
|
375
|
+
if resp.status_code == 200:
|
|
376
|
+
d = resp.json()
|
|
377
|
+
status_color = EMERALD if d.get("status") == "running" else RED if d.get("status") == "crashed" else DIM
|
|
378
|
+
print(f"\n Agent: {d.get('name', args.agent_id)}")
|
|
379
|
+
print(f" ID: {d.get('agent_id', args.agent_id)}")
|
|
380
|
+
print(f" Status: {status_color}{d.get('status', 'unknown')}{RESET}")
|
|
381
|
+
print(f" Model: {d.get('model', 'default')}")
|
|
382
|
+
else:
|
|
383
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
384
|
+
except Exception as e:
|
|
385
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def cmd_logs(args):
|
|
389
|
+
try:
|
|
390
|
+
resp = httpx.get(
|
|
391
|
+
f"{get_api_url()}/v1/agents/{args.agent_id}/logs",
|
|
392
|
+
headers=headers(), params={"tail": getattr(args, "tail", 100)},
|
|
393
|
+
)
|
|
394
|
+
if resp.status_code == 200:
|
|
395
|
+
logs = resp.json().get("logs", [])
|
|
396
|
+
if not logs:
|
|
397
|
+
print(f"{DIM} No logs yet.{RESET}")
|
|
398
|
+
else:
|
|
399
|
+
for log in logs:
|
|
400
|
+
ts = log.get("created_at", "")
|
|
401
|
+
stream = log.get("stream", "stdout")
|
|
402
|
+
content = log.get("content", "")
|
|
403
|
+
color = RED if stream == "stderr" else ""
|
|
404
|
+
print(f"{DIM}[{ts}]{RESET} {color}{content}{RESET}")
|
|
405
|
+
else:
|
|
406
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def cmd_stop(args):
|
|
412
|
+
try:
|
|
413
|
+
resp = httpx.post(f"{get_api_url()}/v1/agents/{args.agent_id}/stop", headers=headers())
|
|
414
|
+
if resp.status_code == 200:
|
|
415
|
+
print(f"{EMERALD} Agent {args.agent_id} stopped.{RESET}")
|
|
416
|
+
else:
|
|
417
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
418
|
+
except Exception as e:
|
|
419
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def cmd_restart(args):
|
|
423
|
+
try:
|
|
424
|
+
resp = httpx.post(f"{get_api_url()}/v1/agents/{args.agent_id}/restart", headers=headers())
|
|
425
|
+
if resp.status_code == 200:
|
|
426
|
+
print(f"{EMERALD} Agent {args.agent_id} restarted.{RESET}")
|
|
427
|
+
else:
|
|
428
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
429
|
+
except Exception as e:
|
|
430
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def cmd_list(args):
|
|
434
|
+
try:
|
|
435
|
+
resp = httpx.get(f"{get_api_url()}/v1/agents", headers=headers())
|
|
436
|
+
if resp.status_code == 200:
|
|
437
|
+
agents = resp.json().get("agents", [])
|
|
438
|
+
if not agents:
|
|
439
|
+
print(f"\n{DIM} No agents deployed.{RESET}")
|
|
440
|
+
print(f" Run: {EMERALD}kashcloud deploy <file.py>{RESET}\n")
|
|
441
|
+
return
|
|
442
|
+
print(f"\n {'ID':<20} {'Name':<20} {'Status':<12} {'Model':<30}")
|
|
443
|
+
print(f" {'-'*82}")
|
|
444
|
+
for a in agents:
|
|
445
|
+
sc = EMERALD if a["status"] == "running" else RED if a["status"] == "crashed" else DIM
|
|
446
|
+
print(f" {a['id']:<20} {a['name']:<20} {sc}{a['status']:<12}{RESET} {a.get('model', ''):<30}")
|
|
447
|
+
print()
|
|
448
|
+
else:
|
|
449
|
+
print(f"{RED}Error: {resp.text}{RESET}")
|
|
450
|
+
except Exception as e:
|
|
451
|
+
print(f"{RED}Error: {e}{RESET}")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def cmd_chat(args):
|
|
455
|
+
"""One-shot chat message."""
|
|
456
|
+
api_url = get_api_url()
|
|
457
|
+
api_key = get_api_key()
|
|
458
|
+
message = " ".join(args.message)
|
|
459
|
+
if not message:
|
|
460
|
+
print(f"{RED}No message provided.{RESET}")
|
|
461
|
+
sys.exit(1)
|
|
462
|
+
|
|
463
|
+
messages = [
|
|
464
|
+
{"role": "system", "content": f"You are Kash, an AI assistant. Current dir: {os.getcwd()}. Be concise."},
|
|
465
|
+
{"role": "user", "content": message},
|
|
466
|
+
]
|
|
467
|
+
response = call_ai(messages, api_url, api_key)
|
|
468
|
+
response = process_tool_calls(response)
|
|
469
|
+
print(response)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def main():
|
|
473
|
+
parser = argparse.ArgumentParser(
|
|
474
|
+
prog="kashcloud",
|
|
475
|
+
description="KashCloud -- AI terminal + agent infrastructure.",
|
|
476
|
+
)
|
|
477
|
+
parser.add_argument("--version", "-V", action="version", version=f"kashcloud {VERSION}")
|
|
478
|
+
|
|
479
|
+
sub = parser.add_subparsers(dest="command")
|
|
480
|
+
|
|
481
|
+
# login
|
|
482
|
+
p_login = sub.add_parser("login", help="Authenticate with API key")
|
|
483
|
+
p_login.add_argument("--api-url", help="Custom API URL")
|
|
484
|
+
|
|
485
|
+
# chat (one-shot)
|
|
486
|
+
p_chat = sub.add_parser("chat", help="Send a one-shot message")
|
|
487
|
+
p_chat.add_argument("message", nargs="+", help="Your message")
|
|
488
|
+
|
|
489
|
+
# deploy
|
|
490
|
+
p_deploy = sub.add_parser("deploy", help="Deploy an agent")
|
|
491
|
+
p_deploy.add_argument("file", help="Python file to deploy")
|
|
492
|
+
p_deploy.add_argument("--name", "-n", help="Agent name")
|
|
493
|
+
p_deploy.add_argument("--model", "-m", help="AI model to use")
|
|
494
|
+
p_deploy.add_argument("--memory", type=int, help="Memory limit in MB")
|
|
495
|
+
|
|
496
|
+
# status
|
|
497
|
+
p_status = sub.add_parser("status", help="Agent status")
|
|
498
|
+
p_status.add_argument("agent_id")
|
|
499
|
+
|
|
500
|
+
# logs
|
|
501
|
+
p_logs = sub.add_parser("logs", help="View logs")
|
|
502
|
+
p_logs.add_argument("agent_id")
|
|
503
|
+
p_logs.add_argument("--tail", "-t", type=int, default=100)
|
|
504
|
+
|
|
505
|
+
# stop
|
|
506
|
+
p_stop = sub.add_parser("stop", help="Stop agent")
|
|
507
|
+
p_stop.add_argument("agent_id")
|
|
508
|
+
|
|
509
|
+
# restart
|
|
510
|
+
p_restart = sub.add_parser("restart", help="Restart agent")
|
|
511
|
+
p_restart.add_argument("agent_id")
|
|
512
|
+
|
|
513
|
+
# list
|
|
514
|
+
sub.add_parser("list", help="List agents")
|
|
515
|
+
|
|
516
|
+
args = parser.parse_args()
|
|
517
|
+
|
|
518
|
+
# No command = interactive REPL
|
|
519
|
+
if not args.command:
|
|
520
|
+
interactive_repl()
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
cmds = {
|
|
524
|
+
"login": cmd_login, "chat": cmd_chat, "deploy": cmd_deploy,
|
|
525
|
+
"status": cmd_status, "logs": cmd_logs, "stop": cmd_stop,
|
|
526
|
+
"restart": cmd_restart, "list": cmd_list,
|
|
527
|
+
}
|
|
528
|
+
cmds[args.command](args)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
main()
|