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.
Files changed (183) hide show
  1. package/README.md +179 -0
  2. package/bin/rackmind.js +6 -0
  3. package/dist/ai/client.d.ts +71 -0
  4. package/dist/ai/client.d.ts.map +1 -0
  5. package/dist/ai/client.js +198 -0
  6. package/dist/ai/client.js.map +1 -0
  7. package/dist/ai/system-prompt.d.ts +20 -0
  8. package/dist/ai/system-prompt.d.ts.map +1 -0
  9. package/dist/ai/system-prompt.js +169 -0
  10. package/dist/ai/system-prompt.js.map +1 -0
  11. package/dist/ai/tool-executor.d.ts +27 -0
  12. package/dist/ai/tool-executor.d.ts.map +1 -0
  13. package/dist/ai/tool-executor.js +184 -0
  14. package/dist/ai/tool-executor.js.map +1 -0
  15. package/dist/ai/tools.d.ts +7 -0
  16. package/dist/ai/tools.d.ts.map +1 -0
  17. package/dist/ai/tools.js +203 -0
  18. package/dist/ai/tools.js.map +1 -0
  19. package/dist/commands/config.d.ts +21 -0
  20. package/dist/commands/config.d.ts.map +1 -0
  21. package/dist/commands/config.js +169 -0
  22. package/dist/commands/config.js.map +1 -0
  23. package/dist/commands/connect.d.ts +7 -0
  24. package/dist/commands/connect.d.ts.map +1 -0
  25. package/dist/commands/connect.js +117 -0
  26. package/dist/commands/connect.js.map +1 -0
  27. package/dist/commands/containers.d.ts +5 -0
  28. package/dist/commands/containers.d.ts.map +1 -0
  29. package/dist/commands/containers.js +83 -0
  30. package/dist/commands/containers.js.map +1 -0
  31. package/dist/commands/exec.d.ts +5 -0
  32. package/dist/commands/exec.d.ts.map +1 -0
  33. package/dist/commands/exec.js +133 -0
  34. package/dist/commands/exec.js.map +1 -0
  35. package/dist/commands/lifecycle.d.ts +7 -0
  36. package/dist/commands/lifecycle.d.ts.map +1 -0
  37. package/dist/commands/lifecycle.js +213 -0
  38. package/dist/commands/lifecycle.js.map +1 -0
  39. package/dist/commands/logs.d.ts +5 -0
  40. package/dist/commands/logs.d.ts.map +1 -0
  41. package/dist/commands/logs.js +117 -0
  42. package/dist/commands/logs.js.map +1 -0
  43. package/dist/commands/report.d.ts +6 -0
  44. package/dist/commands/report.d.ts.map +1 -0
  45. package/dist/commands/report.js +203 -0
  46. package/dist/commands/report.js.map +1 -0
  47. package/dist/commands/servers.d.ts +17 -0
  48. package/dist/commands/servers.d.ts.map +1 -0
  49. package/dist/commands/servers.js +116 -0
  50. package/dist/commands/servers.js.map +1 -0
  51. package/dist/commands/status.d.ts +5 -0
  52. package/dist/commands/status.d.ts.map +1 -0
  53. package/dist/commands/status.js +174 -0
  54. package/dist/commands/status.js.map +1 -0
  55. package/dist/commands/vms.d.ts +5 -0
  56. package/dist/commands/vms.d.ts.map +1 -0
  57. package/dist/commands/vms.js +83 -0
  58. package/dist/commands/vms.js.map +1 -0
  59. package/dist/config/crypto.d.ts +20 -0
  60. package/dist/config/crypto.d.ts.map +1 -0
  61. package/dist/config/crypto.js +78 -0
  62. package/dist/config/crypto.js.map +1 -0
  63. package/dist/config/index.d.ts +21 -0
  64. package/dist/config/index.d.ts.map +1 -0
  65. package/dist/config/index.js +158 -0
  66. package/dist/config/index.js.map +1 -0
  67. package/dist/config/types.d.ts +263 -0
  68. package/dist/config/types.d.ts.map +1 -0
  69. package/dist/config/types.js +83 -0
  70. package/dist/config/types.js.map +1 -0
  71. package/dist/globals.d.ts +22 -0
  72. package/dist/globals.d.ts.map +1 -0
  73. package/dist/globals.js +43 -0
  74. package/dist/globals.js.map +1 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +399 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/interactive/App.d.ts +3 -0
  80. package/dist/interactive/App.d.ts.map +1 -0
  81. package/dist/interactive/App.js +103 -0
  82. package/dist/interactive/App.js.map +1 -0
  83. package/dist/interactive/components/Header.d.ts +8 -0
  84. package/dist/interactive/components/Header.d.ts.map +1 -0
  85. package/dist/interactive/components/Header.js +20 -0
  86. package/dist/interactive/components/Header.js.map +1 -0
  87. package/dist/interactive/components/InputBar.d.ts +9 -0
  88. package/dist/interactive/components/InputBar.d.ts.map +1 -0
  89. package/dist/interactive/components/InputBar.js +89 -0
  90. package/dist/interactive/components/InputBar.js.map +1 -0
  91. package/dist/interactive/components/MessageList.d.ts +9 -0
  92. package/dist/interactive/components/MessageList.d.ts.map +1 -0
  93. package/dist/interactive/components/MessageList.js +28 -0
  94. package/dist/interactive/components/MessageList.js.map +1 -0
  95. package/dist/interactive/components/Spinner.d.ts +7 -0
  96. package/dist/interactive/components/Spinner.d.ts.map +1 -0
  97. package/dist/interactive/components/Spinner.js +19 -0
  98. package/dist/interactive/components/Spinner.js.map +1 -0
  99. package/dist/interactive/components/StatusBar.d.ts +10 -0
  100. package/dist/interactive/components/StatusBar.d.ts.map +1 -0
  101. package/dist/interactive/components/StatusBar.js +7 -0
  102. package/dist/interactive/components/StatusBar.js.map +1 -0
  103. package/dist/interactive/components/StreamingMessage.d.ts +7 -0
  104. package/dist/interactive/components/StreamingMessage.d.ts.map +1 -0
  105. package/dist/interactive/components/StreamingMessage.js +9 -0
  106. package/dist/interactive/components/StreamingMessage.js.map +1 -0
  107. package/dist/interactive/components/ToolIndicator.d.ts +7 -0
  108. package/dist/interactive/components/ToolIndicator.d.ts.map +1 -0
  109. package/dist/interactive/components/ToolIndicator.js +10 -0
  110. package/dist/interactive/components/ToolIndicator.js.map +1 -0
  111. package/dist/interactive/components/ToolOutput.d.ts +8 -0
  112. package/dist/interactive/components/ToolOutput.d.ts.map +1 -0
  113. package/dist/interactive/components/ToolOutput.js +9 -0
  114. package/dist/interactive/components/ToolOutput.js.map +1 -0
  115. package/dist/interactive/components/WelcomeScreen.d.ts +9 -0
  116. package/dist/interactive/components/WelcomeScreen.d.ts.map +1 -0
  117. package/dist/interactive/components/WelcomeScreen.js +8 -0
  118. package/dist/interactive/components/WelcomeScreen.js.map +1 -0
  119. package/dist/interactive/launch.d.ts +6 -0
  120. package/dist/interactive/launch.d.ts.map +1 -0
  121. package/dist/interactive/launch.js +30 -0
  122. package/dist/interactive/launch.js.map +1 -0
  123. package/dist/interactive/slashCommands.d.ts +13 -0
  124. package/dist/interactive/slashCommands.d.ts.map +1 -0
  125. package/dist/interactive/slashCommands.js +102 -0
  126. package/dist/interactive/slashCommands.js.map +1 -0
  127. package/dist/interactive/useRackMind.d.ts +30 -0
  128. package/dist/interactive/useRackMind.d.ts.map +1 -0
  129. package/dist/interactive/useRackMind.js +241 -0
  130. package/dist/interactive/useRackMind.js.map +1 -0
  131. package/dist/oneshot/run.d.ts +2 -0
  132. package/dist/oneshot/run.d.ts.map +1 -0
  133. package/dist/oneshot/run.js +195 -0
  134. package/dist/oneshot/run.js.map +1 -0
  135. package/dist/server/client.d.ts +89 -0
  136. package/dist/server/client.d.ts.map +1 -0
  137. package/dist/server/client.js +239 -0
  138. package/dist/server/client.js.map +1 -0
  139. package/dist/server/proxmox.d.ts +160 -0
  140. package/dist/server/proxmox.d.ts.map +1 -0
  141. package/dist/server/proxmox.js +219 -0
  142. package/dist/server/proxmox.js.map +1 -0
  143. package/dist/server/ssh.d.ts +80 -0
  144. package/dist/server/ssh.d.ts.map +1 -0
  145. package/dist/server/ssh.js +262 -0
  146. package/dist/server/ssh.js.map +1 -0
  147. package/dist/utils/ascii.d.ts +3 -0
  148. package/dist/utils/ascii.d.ts.map +1 -0
  149. package/dist/utils/ascii.js +16 -0
  150. package/dist/utils/ascii.js.map +1 -0
  151. package/dist/utils/format.d.ts +7 -0
  152. package/dist/utils/format.d.ts.map +1 -0
  153. package/dist/utils/format.js +20 -0
  154. package/dist/utils/format.js.map +1 -0
  155. package/dist/utils/history.d.ts +23 -0
  156. package/dist/utils/history.d.ts.map +1 -0
  157. package/dist/utils/history.js +107 -0
  158. package/dist/utils/history.js.map +1 -0
  159. package/dist/utils/logger.d.ts +9 -0
  160. package/dist/utils/logger.d.ts.map +1 -0
  161. package/dist/utils/logger.js +51 -0
  162. package/dist/utils/logger.js.map +1 -0
  163. package/dist/utils/markdown.d.ts +12 -0
  164. package/dist/utils/markdown.d.ts.map +1 -0
  165. package/dist/utils/markdown.js +76 -0
  166. package/dist/utils/markdown.js.map +1 -0
  167. package/dist/utils/prompt.d.ts +25 -0
  168. package/dist/utils/prompt.d.ts.map +1 -0
  169. package/dist/utils/prompt.js +133 -0
  170. package/dist/utils/prompt.js.map +1 -0
  171. package/dist/utils/retry.d.ts +37 -0
  172. package/dist/utils/retry.d.ts.map +1 -0
  173. package/dist/utils/retry.js +122 -0
  174. package/dist/utils/retry.js.map +1 -0
  175. package/dist/utils/shutdown.d.ts +22 -0
  176. package/dist/utils/shutdown.d.ts.map +1 -0
  177. package/dist/utils/shutdown.js +94 -0
  178. package/dist/utils/shutdown.js.map +1 -0
  179. package/dist/utils/table.d.ts +38 -0
  180. package/dist/utils/table.d.ts.map +1 -0
  181. package/dist/utils/table.js +150 -0
  182. package/dist/utils/table.js.map +1 -0
  183. 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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('../dist/index.js').catch((err) => {
4
+ console.error('Failed to start rackmind:', err.message);
5
+ process.exit(1);
6
+ });
@@ -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