rackmind-cli 0.1.2
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.
- package/README.md +179 -0
- package/bin/rackmind.js +6 -0
- package/dist/ai/client.d.ts +71 -0
- package/dist/ai/client.d.ts.map +1 -0
- package/dist/ai/client.js +198 -0
- package/dist/ai/client.js.map +1 -0
- package/dist/ai/system-prompt.d.ts +20 -0
- package/dist/ai/system-prompt.d.ts.map +1 -0
- package/dist/ai/system-prompt.js +169 -0
- package/dist/ai/system-prompt.js.map +1 -0
- package/dist/ai/tool-executor.d.ts +27 -0
- package/dist/ai/tool-executor.d.ts.map +1 -0
- package/dist/ai/tool-executor.js +184 -0
- package/dist/ai/tool-executor.js.map +1 -0
- package/dist/ai/tools.d.ts +7 -0
- package/dist/ai/tools.d.ts.map +1 -0
- package/dist/ai/tools.js +203 -0
- package/dist/ai/tools.js.map +1 -0
- package/dist/commands/config.d.ts +21 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +169 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/connect.d.ts +7 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +117 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/containers.d.ts +5 -0
- package/dist/commands/containers.d.ts.map +1 -0
- package/dist/commands/containers.js +83 -0
- package/dist/commands/containers.js.map +1 -0
- package/dist/commands/exec.d.ts +5 -0
- package/dist/commands/exec.d.ts.map +1 -0
- package/dist/commands/exec.js +133 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/lifecycle.d.ts +7 -0
- package/dist/commands/lifecycle.d.ts.map +1 -0
- package/dist/commands/lifecycle.js +213 -0
- package/dist/commands/lifecycle.js.map +1 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +117 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/report.d.ts +6 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +203 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/servers.d.ts +17 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +116 -0
- package/dist/commands/servers.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +174 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/vms.d.ts +5 -0
- package/dist/commands/vms.d.ts.map +1 -0
- package/dist/commands/vms.js +83 -0
- package/dist/commands/vms.js.map +1 -0
- package/dist/config/crypto.d.ts +20 -0
- package/dist/config/crypto.d.ts.map +1 -0
- package/dist/config/crypto.js +78 -0
- package/dist/config/crypto.js.map +1 -0
- package/dist/config/index.d.ts +21 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +158 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +263 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +83 -0
- package/dist/config/types.js.map +1 -0
- package/dist/globals.d.ts +22 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/globals.js +43 -0
- package/dist/globals.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +399 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive/App.d.ts +3 -0
- package/dist/interactive/App.d.ts.map +1 -0
- package/dist/interactive/App.js +103 -0
- package/dist/interactive/App.js.map +1 -0
- package/dist/interactive/components/Header.d.ts +8 -0
- package/dist/interactive/components/Header.d.ts.map +1 -0
- package/dist/interactive/components/Header.js +20 -0
- package/dist/interactive/components/Header.js.map +1 -0
- package/dist/interactive/components/InputBar.d.ts +9 -0
- package/dist/interactive/components/InputBar.d.ts.map +1 -0
- package/dist/interactive/components/InputBar.js +89 -0
- package/dist/interactive/components/InputBar.js.map +1 -0
- package/dist/interactive/components/MessageList.d.ts +9 -0
- package/dist/interactive/components/MessageList.d.ts.map +1 -0
- package/dist/interactive/components/MessageList.js +28 -0
- package/dist/interactive/components/MessageList.js.map +1 -0
- package/dist/interactive/components/Spinner.d.ts +7 -0
- package/dist/interactive/components/Spinner.d.ts.map +1 -0
- package/dist/interactive/components/Spinner.js +19 -0
- package/dist/interactive/components/Spinner.js.map +1 -0
- package/dist/interactive/components/StatusBar.d.ts +10 -0
- package/dist/interactive/components/StatusBar.d.ts.map +1 -0
- package/dist/interactive/components/StatusBar.js +7 -0
- package/dist/interactive/components/StatusBar.js.map +1 -0
- package/dist/interactive/components/StreamingMessage.d.ts +7 -0
- package/dist/interactive/components/StreamingMessage.d.ts.map +1 -0
- package/dist/interactive/components/StreamingMessage.js +9 -0
- package/dist/interactive/components/StreamingMessage.js.map +1 -0
- package/dist/interactive/components/ToolIndicator.d.ts +7 -0
- package/dist/interactive/components/ToolIndicator.d.ts.map +1 -0
- package/dist/interactive/components/ToolIndicator.js +10 -0
- package/dist/interactive/components/ToolIndicator.js.map +1 -0
- package/dist/interactive/components/ToolOutput.d.ts +8 -0
- package/dist/interactive/components/ToolOutput.d.ts.map +1 -0
- package/dist/interactive/components/ToolOutput.js +9 -0
- package/dist/interactive/components/ToolOutput.js.map +1 -0
- package/dist/interactive/components/WelcomeScreen.d.ts +9 -0
- package/dist/interactive/components/WelcomeScreen.d.ts.map +1 -0
- package/dist/interactive/components/WelcomeScreen.js +8 -0
- package/dist/interactive/components/WelcomeScreen.js.map +1 -0
- package/dist/interactive/launch.d.ts +6 -0
- package/dist/interactive/launch.d.ts.map +1 -0
- package/dist/interactive/launch.js +30 -0
- package/dist/interactive/launch.js.map +1 -0
- package/dist/interactive/slashCommands.d.ts +13 -0
- package/dist/interactive/slashCommands.d.ts.map +1 -0
- package/dist/interactive/slashCommands.js +102 -0
- package/dist/interactive/slashCommands.js.map +1 -0
- package/dist/interactive/useRackMind.d.ts +30 -0
- package/dist/interactive/useRackMind.d.ts.map +1 -0
- package/dist/interactive/useRackMind.js +241 -0
- package/dist/interactive/useRackMind.js.map +1 -0
- package/dist/oneshot/run.d.ts +2 -0
- package/dist/oneshot/run.d.ts.map +1 -0
- package/dist/oneshot/run.js +195 -0
- package/dist/oneshot/run.js.map +1 -0
- package/dist/server/client.d.ts +89 -0
- package/dist/server/client.d.ts.map +1 -0
- package/dist/server/client.js +239 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/proxmox.d.ts +160 -0
- package/dist/server/proxmox.d.ts.map +1 -0
- package/dist/server/proxmox.js +219 -0
- package/dist/server/proxmox.js.map +1 -0
- package/dist/server/ssh.d.ts +80 -0
- package/dist/server/ssh.d.ts.map +1 -0
- package/dist/server/ssh.js +262 -0
- package/dist/server/ssh.js.map +1 -0
- package/dist/utils/ascii.d.ts +3 -0
- package/dist/utils/ascii.d.ts.map +1 -0
- package/dist/utils/ascii.js +16 -0
- package/dist/utils/ascii.js.map +1 -0
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +20 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/history.d.ts +23 -0
- package/dist/utils/history.d.ts.map +1 -0
- package/dist/utils/history.js +107 -0
- package/dist/utils/history.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +51 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/markdown.d.ts +12 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/utils/markdown.js +76 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/prompt.d.ts +25 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +133 -0
- package/dist/utils/prompt.js.map +1 -0
- package/dist/utils/retry.d.ts +37 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +122 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/shutdown.d.ts +22 -0
- package/dist/utils/shutdown.d.ts.map +1 -0
- package/dist/utils/shutdown.js +94 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/dist/utils/table.d.ts +38 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/dist/utils/table.js +150 -0
- package/dist/utils/table.js.map +1 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# rackmind-cli
|
|
2
|
+
|
|
3
|
+
AI-powered infrastructure management for MSPs. A Claude Code-style terminal experience for managing Proxmox servers, LXC containers, and QEMU VMs through natural language.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g rackmind-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or link locally for development:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/rackmind-ai/rackmind-cli.git
|
|
15
|
+
cd rackmind-cli
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
npm link
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Add your first server
|
|
25
|
+
rackmind connect my-proxmox
|
|
26
|
+
|
|
27
|
+
# Set your Anthropic API key
|
|
28
|
+
rackmind config set-api-key
|
|
29
|
+
|
|
30
|
+
# Launch interactive REPL
|
|
31
|
+
rackmind
|
|
32
|
+
|
|
33
|
+
# Or ask a one-shot question
|
|
34
|
+
rackmind "what containers are running?"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Interactive Mode (REPL)
|
|
40
|
+
|
|
41
|
+
Launch with no arguments to enter the full interactive terminal:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rackmind
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Features:
|
|
48
|
+
|
|
49
|
+
- Natural language queries about your infrastructure
|
|
50
|
+
- AI-powered command execution with tool use
|
|
51
|
+
- Streaming responses with markdown rendering
|
|
52
|
+
- Slash commands: `/connect`, `/servers`, `/clear`, `/model`, `/help`, `/exit`
|
|
53
|
+
- Keyboard shortcuts: Up/Down for history, Ctrl+C to exit, Ctrl+L to clear
|
|
54
|
+
|
|
55
|
+
### One-shot Mode
|
|
56
|
+
|
|
57
|
+
Ask a single question and get a streamed response:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
rackmind "check disk usage on all containers"
|
|
61
|
+
rackmind ask "restart container 106"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Fully pipeable:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
rackmind "list running containers" --json | jq '.response'
|
|
68
|
+
rackmind "what is the uptime?" --quiet 2>/dev/null
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Subcommands
|
|
72
|
+
|
|
73
|
+
#### Server Management
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
rackmind connect <alias> # Connect to (or create) a server profile
|
|
77
|
+
rackmind servers list # List all server profiles
|
|
78
|
+
rackmind servers add <alias> # Add a new server profile
|
|
79
|
+
rackmind servers remove <alias> # Remove a server profile
|
|
80
|
+
rackmind servers switch <alias> # Switch the active server
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Infrastructure Status
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
rackmind status # Show config, connection status, server list
|
|
87
|
+
rackmind containers # List all LXC containers
|
|
88
|
+
rackmind vms # List all QEMU virtual machines
|
|
89
|
+
rackmind report # Generate a full health report
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### Guest Lifecycle
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
rackmind start <vmid> # Start a container or VM
|
|
96
|
+
rackmind stop <vmid> # Stop a container or VM (with confirmation)
|
|
97
|
+
rackmind restart <vmid> # Restart a container or VM (with confirmation)
|
|
98
|
+
rackmind stop <vmid> --force # Skip confirmation prompt
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Command Execution
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
rackmind exec "uptime" # Run on the Proxmox host
|
|
105
|
+
rackmind exec "apt update" --target=106 # Run inside LXC container 106
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Logs
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rackmind logs <vmid> # Tail logs for a container or VM
|
|
112
|
+
rackmind logs <vmid> -n 100 # Show last 100 lines
|
|
113
|
+
rackmind logs <vmid> -f # Follow log output
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Configuration
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
rackmind config # Show current configuration
|
|
120
|
+
rackmind config set <key> <value> # Set a preference
|
|
121
|
+
rackmind config reset # Reset preferences to defaults
|
|
122
|
+
rackmind config set-api-key # Set or update the Anthropic API key
|
|
123
|
+
rackmind config path # Print the config file path
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Available preference keys:
|
|
127
|
+
|
|
128
|
+
- `model` -- AI model (default: `claude-sonnet-4-5`)
|
|
129
|
+
- `theme` -- Color theme: `dark` or `light`
|
|
130
|
+
- `confirmDestructive` -- Require confirmation for destructive actions (`true`/`false`)
|
|
131
|
+
- `timestampMessages` -- Show timestamps on messages (`true`/`false`)
|
|
132
|
+
|
|
133
|
+
### Global Flags
|
|
134
|
+
|
|
135
|
+
These flags work with any command:
|
|
136
|
+
|
|
137
|
+
| Flag | Description |
|
|
138
|
+
| ------------------- | --------------------------------------------------------- |
|
|
139
|
+
| `--json` | Output in JSON format (implies `--quiet`) |
|
|
140
|
+
| `--quiet` | Suppress spinners, banners, and non-essential output |
|
|
141
|
+
| `--no-color` | Disable colored output (also respects `NO_COLOR` env var) |
|
|
142
|
+
| `--server <url>` | Override the RackMind server URL |
|
|
143
|
+
| `-i, --interactive` | Force interactive REPL mode |
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
Config is stored at `~/.config/rackmind/config.json` (XDG-compliant).
|
|
148
|
+
|
|
149
|
+
Credentials (API keys, passwords, token secrets) are encrypted using Node.js `crypto` with a machine-derived key. They are never stored in plaintext.
|
|
150
|
+
|
|
151
|
+
Config writes are atomic (write to temp file, then rename) to prevent corruption.
|
|
152
|
+
|
|
153
|
+
## Requirements
|
|
154
|
+
|
|
155
|
+
- Node.js 20+
|
|
156
|
+
- A Proxmox VE server with API token access
|
|
157
|
+
- An Anthropic API key (for AI features)
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm run dev # Run in dev mode (tsx)
|
|
163
|
+
npm run build # Compile TypeScript
|
|
164
|
+
npm run lint # Run ESLint
|
|
165
|
+
npm run format # Format with Prettier
|
|
166
|
+
npm run type-check # Type-check with tsc
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
All three quality gates must pass before every commit:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
tsc --noEmit
|
|
173
|
+
npx eslint .
|
|
174
|
+
npx prettier --check .
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
All rights reserved.
|
package/bin/rackmind.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import type { ServerClient } from '../server/client.js';
|
|
3
|
+
export interface AIClientOptions {
|
|
4
|
+
/** Anthropic API key */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Model to use (default: claude-sonnet-4-5) */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** Max tokens for the response */
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** API request timeout in milliseconds (default: 120000) */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface StreamCallbacks {
|
|
14
|
+
/** Called for each text chunk as it streams in */
|
|
15
|
+
onText: (text: string) => void;
|
|
16
|
+
/** Called when a tool is about to be executed */
|
|
17
|
+
onToolStart: (toolName: string, label: string) => void;
|
|
18
|
+
/** Called when a tool finishes executing */
|
|
19
|
+
onToolEnd: (toolName: string, label: string, isError: boolean) => void;
|
|
20
|
+
/** Called when an error occurs */
|
|
21
|
+
onError: (error: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface ConversationMessage {
|
|
24
|
+
role: 'user' | 'assistant';
|
|
25
|
+
content: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ToolExecution {
|
|
28
|
+
toolName: string;
|
|
29
|
+
label: string;
|
|
30
|
+
isError: boolean;
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MultiTurnCallbacks extends StreamCallbacks {
|
|
34
|
+
/** Called when a tool execution result is available (for display in REPL) */
|
|
35
|
+
onToolResult?: (execution: ToolExecution) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare class AIClient {
|
|
38
|
+
private readonly anthropic;
|
|
39
|
+
private readonly model;
|
|
40
|
+
private readonly maxTokens;
|
|
41
|
+
constructor(options: AIClientOptions);
|
|
42
|
+
/**
|
|
43
|
+
* Send a one-shot query to Claude with full server context and tool use.
|
|
44
|
+
*
|
|
45
|
+
* This method:
|
|
46
|
+
* 1. Fetches live server context from the Proxmox API
|
|
47
|
+
* 2. Builds a system prompt with the context
|
|
48
|
+
* 3. Sends the user's query to Claude with tool definitions
|
|
49
|
+
* 4. Streams the response, executing any tool calls in a loop
|
|
50
|
+
* 5. Returns when Claude produces a final text response (stop_reason = "end_turn")
|
|
51
|
+
*/
|
|
52
|
+
chat(query: string, serverClient: ServerClient, serverAlias: string, callbacks: StreamCallbacks): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Multi-turn conversation method for the interactive REPL.
|
|
55
|
+
*
|
|
56
|
+
* Unlike `chat()`, this accepts the full conversation history so far
|
|
57
|
+
* and appends the new user query. It returns the updated history
|
|
58
|
+
* (including the assistant's response) for the caller to persist.
|
|
59
|
+
*
|
|
60
|
+
* The caller is responsible for building up `history` across turns.
|
|
61
|
+
*/
|
|
62
|
+
chatMultiTurn(query: string, history: Anthropic.Messages.MessageParam[], serverClient: ServerClient, serverAlias: string, callbacks: MultiTurnCallbacks): Promise<Anthropic.Messages.MessageParam[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Create a message with retry logic for transient errors.
|
|
65
|
+
* Rate limits and overload errors trigger exponential backoff.
|
|
66
|
+
*/
|
|
67
|
+
private createWithRetry;
|
|
68
|
+
/** Get the current model name */
|
|
69
|
+
getModel(): string;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/ai/client.ts"],"names":[],"mappings":"AASA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAWxD,MAAM,WAAW,eAAe;IAC5B,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC5B,kDAAkD;IAClD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,iDAAiD;IACjD,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,4CAA4C;IAC5C,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACvE,kCAAkC;IAClC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AASD,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACvD,6EAA6E;IAC7E,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,IAAI,CAAC;CACrD;AAMD,qBAAa,QAAQ;IACjB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,EAAE,eAAe;IASpC;;;;;;;;;OASG;IACG,IAAI,CACN,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,eAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;IAwEhB;;;;;;;;OAQG;IACG,aAAa,CACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,EAC1C,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,kBAAkB,GAC9B,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;IAsF7C;;;OAGG;IACH,OAAO,CAAC,eAAe;IAiCvB,iCAAiC;IACjC,QAAQ,IAAI,MAAM;CAGrB"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// AI client — Claude API integration with tool use loop
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Wraps @anthropic-ai/sdk to handle:
|
|
5
|
+
// 1. Sending messages with system prompt + tools
|
|
6
|
+
// 2. Streaming text responses to a callback
|
|
7
|
+
// 3. Executing tool calls and feeding results back until a final text response
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
import { getToolDefinitions } from './tools.js';
|
|
11
|
+
import { fetchServerContext, buildSystemPrompt } from './system-prompt.js';
|
|
12
|
+
import { executeTool } from './tool-executor.js';
|
|
13
|
+
import { logger } from '../utils/logger.js';
|
|
14
|
+
import { withRetry, isTransientError, isRateLimitError } from '../utils/retry.js';
|
|
15
|
+
// Max iterations for the tool use loop to prevent infinite loops
|
|
16
|
+
const MAX_TOOL_ITERATIONS = 20;
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// AI Client
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export class AIClient {
|
|
21
|
+
anthropic;
|
|
22
|
+
model;
|
|
23
|
+
maxTokens;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.anthropic = new Anthropic({
|
|
26
|
+
apiKey: options.apiKey,
|
|
27
|
+
timeout: options.timeoutMs ?? 120_000, // 2 minutes default
|
|
28
|
+
});
|
|
29
|
+
this.model = options.model ?? 'claude-sonnet-4-5';
|
|
30
|
+
this.maxTokens = options.maxTokens ?? 4096;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Send a one-shot query to Claude with full server context and tool use.
|
|
34
|
+
*
|
|
35
|
+
* This method:
|
|
36
|
+
* 1. Fetches live server context from the Proxmox API
|
|
37
|
+
* 2. Builds a system prompt with the context
|
|
38
|
+
* 3. Sends the user's query to Claude with tool definitions
|
|
39
|
+
* 4. Streams the response, executing any tool calls in a loop
|
|
40
|
+
* 5. Returns when Claude produces a final text response (stop_reason = "end_turn")
|
|
41
|
+
*/
|
|
42
|
+
async chat(query, serverClient, serverAlias, callbacks) {
|
|
43
|
+
// Fetch live server context
|
|
44
|
+
const context = await fetchServerContext(serverClient);
|
|
45
|
+
const systemPrompt = buildSystemPrompt(context, serverAlias);
|
|
46
|
+
const tools = getToolDefinitions();
|
|
47
|
+
// Build initial messages
|
|
48
|
+
const messages = [{ role: 'user', content: query }];
|
|
49
|
+
// Tool use loop — keep going until Claude gives a final text response
|
|
50
|
+
for (let iteration = 0; iteration < MAX_TOOL_ITERATIONS; iteration++) {
|
|
51
|
+
logger.debug('AI request iteration', { iteration, messageCount: messages.length });
|
|
52
|
+
const response = await this.createWithRetry(systemPrompt, tools, messages, callbacks);
|
|
53
|
+
// Process the response content blocks
|
|
54
|
+
const assistantContent = [];
|
|
55
|
+
const toolUseBlocks = [];
|
|
56
|
+
for (const block of response.content) {
|
|
57
|
+
assistantContent.push(block);
|
|
58
|
+
if (block.type === 'text') {
|
|
59
|
+
callbacks.onText(block.text);
|
|
60
|
+
}
|
|
61
|
+
else if (block.type === 'tool_use') {
|
|
62
|
+
toolUseBlocks.push(block);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Add the assistant's response to the conversation
|
|
66
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
67
|
+
// If Claude is done (no more tool calls), we're finished
|
|
68
|
+
if (response.stop_reason === 'end_turn' || toolUseBlocks.length === 0) {
|
|
69
|
+
logger.debug('AI conversation complete', {
|
|
70
|
+
iterations: iteration + 1,
|
|
71
|
+
stopReason: response.stop_reason,
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Execute all tool calls and collect results
|
|
76
|
+
const toolResults = [];
|
|
77
|
+
for (const toolBlock of toolUseBlocks) {
|
|
78
|
+
const toolInput = toolBlock.input;
|
|
79
|
+
callbacks.onToolStart(toolBlock.name, toolBlock.name);
|
|
80
|
+
const result = await executeTool(toolBlock.name, toolInput, serverClient);
|
|
81
|
+
callbacks.onToolEnd(toolBlock.name, result.label, result.isError);
|
|
82
|
+
toolResults.push({
|
|
83
|
+
type: 'tool_result',
|
|
84
|
+
tool_use_id: toolBlock.id,
|
|
85
|
+
content: result.content,
|
|
86
|
+
is_error: result.isError,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Add tool results to the conversation and loop back
|
|
90
|
+
messages.push({ role: 'user', content: toolResults });
|
|
91
|
+
}
|
|
92
|
+
callbacks.onError('Reached maximum tool iterations. The AI may be stuck in a loop.');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Multi-turn conversation method for the interactive REPL.
|
|
96
|
+
*
|
|
97
|
+
* Unlike `chat()`, this accepts the full conversation history so far
|
|
98
|
+
* and appends the new user query. It returns the updated history
|
|
99
|
+
* (including the assistant's response) for the caller to persist.
|
|
100
|
+
*
|
|
101
|
+
* The caller is responsible for building up `history` across turns.
|
|
102
|
+
*/
|
|
103
|
+
async chatMultiTurn(query, history, serverClient, serverAlias, callbacks) {
|
|
104
|
+
// Fetch live server context
|
|
105
|
+
const context = await fetchServerContext(serverClient);
|
|
106
|
+
const systemPrompt = buildSystemPrompt(context, serverAlias);
|
|
107
|
+
const tools = getToolDefinitions();
|
|
108
|
+
// Clone history and append the new user message
|
|
109
|
+
const messages = [
|
|
110
|
+
...history,
|
|
111
|
+
{ role: 'user', content: query },
|
|
112
|
+
];
|
|
113
|
+
// Tool use loop
|
|
114
|
+
for (let iteration = 0; iteration < MAX_TOOL_ITERATIONS; iteration++) {
|
|
115
|
+
logger.debug('AI multi-turn request iteration', {
|
|
116
|
+
iteration,
|
|
117
|
+
messageCount: messages.length,
|
|
118
|
+
});
|
|
119
|
+
const response = await this.createWithRetry(systemPrompt, tools, messages, callbacks);
|
|
120
|
+
// Process content blocks
|
|
121
|
+
const assistantContent = [];
|
|
122
|
+
const toolUseBlocks = [];
|
|
123
|
+
for (const block of response.content) {
|
|
124
|
+
assistantContent.push(block);
|
|
125
|
+
if (block.type === 'text') {
|
|
126
|
+
callbacks.onText(block.text);
|
|
127
|
+
}
|
|
128
|
+
else if (block.type === 'tool_use') {
|
|
129
|
+
toolUseBlocks.push(block);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
133
|
+
// If done, return the full updated history
|
|
134
|
+
if (response.stop_reason === 'end_turn' || toolUseBlocks.length === 0) {
|
|
135
|
+
logger.debug('AI multi-turn conversation turn complete', {
|
|
136
|
+
iterations: iteration + 1,
|
|
137
|
+
stopReason: response.stop_reason,
|
|
138
|
+
});
|
|
139
|
+
return messages;
|
|
140
|
+
}
|
|
141
|
+
// Execute tool calls
|
|
142
|
+
const toolResults = [];
|
|
143
|
+
for (const toolBlock of toolUseBlocks) {
|
|
144
|
+
const toolInput = toolBlock.input;
|
|
145
|
+
callbacks.onToolStart(toolBlock.name, toolBlock.name);
|
|
146
|
+
const result = await executeTool(toolBlock.name, toolInput, serverClient);
|
|
147
|
+
callbacks.onToolEnd(toolBlock.name, result.label, result.isError);
|
|
148
|
+
if (callbacks.onToolResult) {
|
|
149
|
+
callbacks.onToolResult({
|
|
150
|
+
toolName: toolBlock.name,
|
|
151
|
+
label: result.label,
|
|
152
|
+
isError: result.isError,
|
|
153
|
+
content: result.content,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
toolResults.push({
|
|
157
|
+
type: 'tool_result',
|
|
158
|
+
tool_use_id: toolBlock.id,
|
|
159
|
+
content: result.content,
|
|
160
|
+
is_error: result.isError,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
messages.push({ role: 'user', content: toolResults });
|
|
164
|
+
}
|
|
165
|
+
callbacks.onError('Reached maximum tool iterations. The AI may be stuck in a loop.');
|
|
166
|
+
return messages;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create a message with retry logic for transient errors.
|
|
170
|
+
* Rate limits and overload errors trigger exponential backoff.
|
|
171
|
+
*/
|
|
172
|
+
createWithRetry(system, tools, messages, callbacks) {
|
|
173
|
+
return withRetry(() => this.anthropic.messages.create({
|
|
174
|
+
model: this.model,
|
|
175
|
+
max_tokens: this.maxTokens,
|
|
176
|
+
system,
|
|
177
|
+
tools,
|
|
178
|
+
messages,
|
|
179
|
+
}), {
|
|
180
|
+
maxAttempts: 3,
|
|
181
|
+
initialDelayMs: 2000,
|
|
182
|
+
maxDelayMs: 30_000,
|
|
183
|
+
isRetryable: isTransientError,
|
|
184
|
+
onRetry: (attempt, error, delayMs) => {
|
|
185
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
186
|
+
const isRateLimit = isRateLimitError(error);
|
|
187
|
+
const label = isRateLimit ? 'Rate limited' : 'API error';
|
|
188
|
+
callbacks.onError(`${label}, retrying in ${Math.round(delayMs / 1000)}s (attempt ${attempt + 1}/3)...`);
|
|
189
|
+
logger.warn('API call retry', { attempt, delayMs, error: msg });
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** Get the current model name */
|
|
194
|
+
getModel() {
|
|
195
|
+
return this.model;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ai/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ServerClient } from '../server/client.js';
|
|
2
|
+
export interface ServerContext {
|
|
3
|
+
nodeName: string;
|
|
4
|
+
nodeInfo: string;
|
|
5
|
+
containers: string;
|
|
6
|
+
vms: string;
|
|
7
|
+
containerCount: number;
|
|
8
|
+
vmCount: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fetch live server context from the Proxmox API.
|
|
12
|
+
* Gracefully handles partial failures — if containers can't be listed,
|
|
13
|
+
* the prompt still includes node info and VMs.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchServerContext(client: ServerClient): Promise<ServerContext>;
|
|
16
|
+
/**
|
|
17
|
+
* Build the full system prompt with live server context.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSystemPrompt(context: ServerContext, serverAlias: string): string;
|
|
20
|
+
//# sourceMappingURL=system-prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/ai/system-prompt.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAqExD,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CA8DrF;AAMD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CA8BrF"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// System prompt construction — builds context-rich prompts for Claude
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Fetches live server state (containers, VMs, node info) from the Proxmox API
|
|
5
|
+
// and injects it into the system prompt so Claude has full situational awareness.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Formatting helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function formatBytes(bytes) {
|
|
12
|
+
if (bytes === 0)
|
|
13
|
+
return '0 B';
|
|
14
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
15
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
16
|
+
const value = bytes / Math.pow(1024, i);
|
|
17
|
+
return `${value.toFixed(1)} ${units[i]}`;
|
|
18
|
+
}
|
|
19
|
+
function formatUptime(seconds) {
|
|
20
|
+
if (seconds === 0)
|
|
21
|
+
return 'not running';
|
|
22
|
+
const days = Math.floor(seconds / 86400);
|
|
23
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
24
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (days > 0)
|
|
27
|
+
parts.push(`${days}d`);
|
|
28
|
+
if (hours > 0)
|
|
29
|
+
parts.push(`${hours}h`);
|
|
30
|
+
if (minutes > 0)
|
|
31
|
+
parts.push(`${minutes}m`);
|
|
32
|
+
return parts.join(' ') || '< 1m';
|
|
33
|
+
}
|
|
34
|
+
function formatContainer(ct) {
|
|
35
|
+
const memPercent = ct.maxmem > 0 ? ((ct.mem / ct.maxmem) * 100).toFixed(1) : '0';
|
|
36
|
+
const diskPercent = ct.maxdisk > 0 ? ((ct.disk / ct.maxdisk) * 100).toFixed(1) : '0';
|
|
37
|
+
return [
|
|
38
|
+
` - CT ${ct.vmid} "${ct.name}": ${ct.status}`,
|
|
39
|
+
` CPUs: ${ct.cpus} | Memory: ${formatBytes(ct.mem)}/${formatBytes(ct.maxmem)} (${memPercent}%)`,
|
|
40
|
+
` Disk: ${formatBytes(ct.disk)}/${formatBytes(ct.maxdisk)} (${diskPercent}%)`,
|
|
41
|
+
` Network: in=${formatBytes(ct.netin)} out=${formatBytes(ct.netout)}`,
|
|
42
|
+
` Uptime: ${formatUptime(ct.uptime)}`,
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
function formatVM(vm) {
|
|
46
|
+
const memPercent = vm.maxmem > 0 ? ((vm.mem / vm.maxmem) * 100).toFixed(1) : '0';
|
|
47
|
+
const diskPercent = vm.maxdisk > 0 ? ((vm.disk / vm.maxdisk) * 100).toFixed(1) : '0';
|
|
48
|
+
return [
|
|
49
|
+
` - VM ${vm.vmid} "${vm.name}": ${vm.status}`,
|
|
50
|
+
` CPUs: ${vm.cpus} | Memory: ${formatBytes(vm.mem)}/${formatBytes(vm.maxmem)} (${memPercent}%)`,
|
|
51
|
+
` Disk: ${formatBytes(vm.disk)}/${formatBytes(vm.maxdisk)} (${diskPercent}%)`,
|
|
52
|
+
` Network: in=${formatBytes(vm.netin)} out=${formatBytes(vm.netout)}`,
|
|
53
|
+
` Uptime: ${formatUptime(vm.uptime)}`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
function formatNode(node) {
|
|
57
|
+
const cpuPercent = (node.cpu * 100).toFixed(1);
|
|
58
|
+
const memPercent = node.maxmem > 0 ? ((node.mem / node.maxmem) * 100).toFixed(1) : '0';
|
|
59
|
+
const diskPercent = node.maxdisk > 0 ? ((node.disk / node.maxdisk) * 100).toFixed(1) : '0';
|
|
60
|
+
return [
|
|
61
|
+
`Node: ${node.node} (${node.status})`,
|
|
62
|
+
` CPU: ${cpuPercent}% of ${node.maxcpu} cores`,
|
|
63
|
+
` Memory: ${formatBytes(node.mem)}/${formatBytes(node.maxmem)} (${memPercent}%)`,
|
|
64
|
+
` Disk: ${formatBytes(node.disk)}/${formatBytes(node.maxdisk)} (${diskPercent}%)`,
|
|
65
|
+
` Uptime: ${formatUptime(node.uptime)}`,
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fetch live server context from the Proxmox API.
|
|
70
|
+
* Gracefully handles partial failures — if containers can't be listed,
|
|
71
|
+
* the prompt still includes node info and VMs.
|
|
72
|
+
*/
|
|
73
|
+
export async function fetchServerContext(client) {
|
|
74
|
+
const nodeName = await client.getNodeName();
|
|
75
|
+
let nodeInfo = 'Node status unavailable';
|
|
76
|
+
let containers = 'Container list unavailable';
|
|
77
|
+
let vms = 'VM list unavailable';
|
|
78
|
+
let containerCount = 0;
|
|
79
|
+
let vmCount = 0;
|
|
80
|
+
// Fetch all in parallel
|
|
81
|
+
const [nodeResult, ctResult, vmResult] = await Promise.allSettled([
|
|
82
|
+
client.getNodeStatus(),
|
|
83
|
+
client.listContainers(),
|
|
84
|
+
client.listVMs(),
|
|
85
|
+
]);
|
|
86
|
+
if (nodeResult.status === 'fulfilled') {
|
|
87
|
+
nodeInfo = formatNode(nodeResult.value);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
logger.warn('Failed to fetch node status', {
|
|
91
|
+
error: nodeResult.reason instanceof Error
|
|
92
|
+
? nodeResult.reason.message
|
|
93
|
+
: String(nodeResult.reason),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (ctResult.status === 'fulfilled') {
|
|
97
|
+
const cts = ctResult.value;
|
|
98
|
+
containerCount = cts.length;
|
|
99
|
+
if (cts.length === 0) {
|
|
100
|
+
containers = 'No LXC containers found';
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
containers = cts.map(formatContainer).join('\n');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
logger.warn('Failed to fetch containers', {
|
|
108
|
+
error: ctResult.reason instanceof Error
|
|
109
|
+
? ctResult.reason.message
|
|
110
|
+
: String(ctResult.reason),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (vmResult.status === 'fulfilled') {
|
|
114
|
+
const vmList = vmResult.value;
|
|
115
|
+
vmCount = vmList.length;
|
|
116
|
+
if (vmList.length === 0) {
|
|
117
|
+
vms = 'No QEMU VMs found';
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
vms = vmList.map(formatVM).join('\n');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
logger.warn('Failed to fetch VMs', {
|
|
125
|
+
error: vmResult.reason instanceof Error
|
|
126
|
+
? vmResult.reason.message
|
|
127
|
+
: String(vmResult.reason),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { nodeName, nodeInfo, containers, vms, containerCount, vmCount };
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// System prompt builder
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
/**
|
|
136
|
+
* Build the full system prompt with live server context.
|
|
137
|
+
*/
|
|
138
|
+
export function buildSystemPrompt(context, serverAlias) {
|
|
139
|
+
return `You are RackMind, an AI assistant specialized in Proxmox server management and Linux system administration. You help MSPs (Managed Service Providers) and IT professionals manage their infrastructure through natural language.
|
|
140
|
+
|
|
141
|
+
## Connected Server
|
|
142
|
+
Server profile: "${serverAlias}"
|
|
143
|
+
${context.nodeInfo}
|
|
144
|
+
|
|
145
|
+
## LXC Containers (${context.containerCount} total)
|
|
146
|
+
${context.containers}
|
|
147
|
+
|
|
148
|
+
## QEMU Virtual Machines (${context.vmCount} total)
|
|
149
|
+
${context.vms}
|
|
150
|
+
|
|
151
|
+
## Your Capabilities
|
|
152
|
+
You have access to tools that let you:
|
|
153
|
+
- Execute shell commands on the Proxmox host via SSH
|
|
154
|
+
- Execute commands inside LXC containers via pct exec
|
|
155
|
+
- Query container, VM, and node status from the Proxmox API
|
|
156
|
+
- Start, stop, and restart containers and VMs
|
|
157
|
+
- List and refresh container/VM inventories
|
|
158
|
+
|
|
159
|
+
## Guidelines
|
|
160
|
+
1. When the user asks about server state, use the context above first. Only call tools if you need fresher data or details not included above.
|
|
161
|
+
2. When the user asks you to run a command, use the execute_command or execute_command_in_container tool. Show the command you're running and explain the output.
|
|
162
|
+
3. For DESTRUCTIVE actions (stop, restart, delete, format, rm -rf, etc.), always explain what will happen and ask for confirmation before executing. Never proceed without explicit user consent.
|
|
163
|
+
4. Format output clearly. Use bullet points, tables, or structured text for readability.
|
|
164
|
+
5. If a command fails, explain what went wrong and suggest fixes.
|
|
165
|
+
6. When discussing resource usage, include both absolute values and percentages.
|
|
166
|
+
7. Be concise but thorough. MSPs are busy — get to the point but don't skip important details.
|
|
167
|
+
8. If you don't know something or can't do something, say so clearly rather than guessing.`;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=system-prompt.js.map
|