klio 1.4.1 → 1.4.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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  A command-line tool for astrological calculations, health analysis, and personalized astrology insights.
4
4
 
5
+ **Features:**
6
+ - Command-line interface for astrological calculations
7
+ - Web-based GUI for easy exploration
8
+ - Health data analysis from Apple Health exports
9
+ - CSV data analysis with statistical testing
10
+ - Personalized astrology with multiple chart support
11
+ - AI integration for astrological insights
12
+
5
13
  ## Prerequisites
6
14
 
7
15
  ### For Linux Users
@@ -28,6 +36,26 @@ npm install -g klio
28
36
  klio [options]
29
37
  ```
30
38
 
39
+ ### Web Interface
40
+
41
+ AstroCLI includes a web-based GUI that you can launch with the `--gui` flag:
42
+
43
+ ```bash
44
+ klio --gui
45
+ ```
46
+
47
+ This will start a web server on port 37421 (or a different port if specified) and open a browser interface where you can:
48
+
49
+ - **Run commands** directly from the browser
50
+ - **View command output** in a formatted display
51
+ - **Browse all available commands** with examples and documentation
52
+ - **Access command history** to reuse previous commands
53
+
54
+ **Custom Port:**
55
+ ```bash
56
+ klio --gui --gui-port 8080
57
+ ```
58
+
31
59
  ### Configuration
32
60
 
33
61
  - **Show status**: `--status` - Shows the stored configuration data
package/commands.db ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -23,8 +23,11 @@
23
23
  "commander": "^14.0.3",
24
24
  "csv": "^6.4.1",
25
25
  "csv-parser": "^3.0.0",
26
+ "express": "^4.19.2",
26
27
  "fast-xml-parser": "^5.3.4",
27
28
  "moment-timezone": "^0.6.0",
29
+ "node-fetch": "^3.3.2",
30
+ "sqlite3": "^5.1.7",
28
31
  "swisseph": "^0.5.17"
29
32
  }
30
33
  }
package/src/cli/cli.js CHANGED
@@ -11,6 +11,15 @@ const swisseph = require('swisseph');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
 
14
+ // GUI Server import
15
+ let guiServer = null;
16
+ try {
17
+ guiServer = require('../gui/server');
18
+ } catch (error) {
19
+ // GUI server not available (optional feature)
20
+ console.debug('GUI server not available');
21
+ }
22
+
14
23
  const program = new Command();
15
24
 
16
25
  // Helper function to check if person data should be used (including new options)
@@ -245,11 +254,39 @@ program
245
254
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
246
255
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
247
256
  .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
257
+ .option('--gui', 'Launches the web interface for command history (port 37421)')
258
+ .option('--gui-port <port>', 'Specify custom port for GUI server')
248
259
  .description('Shows astrological data for a planet')
249
260
  .action(async (planetArg, planet2Arg, options) => {
250
261
  // If planet2Arg is an object, it contains the options (commander behavior)
251
262
  let planet2 = typeof planet2Arg === 'string' ? planet2Arg : null;
252
263
  let actualOptions = typeof planet2Arg === 'object' ? planet2Arg : options;
264
+
265
+ // Handle GUI launch if --gui flag is specified
266
+ if (options.gui) {
267
+ if (!guiServer) {
268
+ console.error('āŒ GUI server is not available. Please install required dependencies.');
269
+ console.error('šŸ’” Run: npm install express sqlite3');
270
+ process.exit(1);
271
+ }
272
+
273
+ try {
274
+ await guiServer.initialize();
275
+ const port = options.guiPort ? parseInt(options.guiPort) : 37421;
276
+ await guiServer.start(port);
277
+
278
+ console.log(`\n🌐 GUI Server started successfully!`);
279
+ console.log(`šŸ“Š Open your browser and navigate to: http://localhost:${guiServer.getPort()}`);
280
+ console.log(`šŸ’” Press Ctrl+C to stop the server and return to CLI`);
281
+
282
+ // Keep the process running
283
+ await new Promise(() => {});
284
+ return;
285
+ } catch (err) {
286
+ console.error('āŒ Failed to start GUI server:', err.message);
287
+ process.exit(1);
288
+ }
289
+ }
253
290
 
254
291
  // Show critical planets if --c option is specified (no planet required)
255
292
  if (actualOptions.c) {
@@ -0,0 +1,67 @@
1
+ const commandDatabase = require('./database');
2
+
3
+ class CommandLogger {
4
+ constructor() {
5
+ this.isInitialized = false;
6
+ }
7
+
8
+ async initialize() {
9
+ try {
10
+ await commandDatabase.initialize();
11
+ this.isInitialized = true;
12
+ } catch (err) {
13
+ console.error('Failed to initialize command logger:', err);
14
+ this.isInitialized = false;
15
+ }
16
+ }
17
+
18
+ async logCommand(command, arguments = '', category = 'general', tags = '') {
19
+ if (!this.isInitialized) {
20
+ await this.initialize();
21
+ }
22
+
23
+ if (!this.isInitialized) {
24
+ return false;
25
+ }
26
+
27
+ try {
28
+ // Capture the command result by temporarily overriding console.log
29
+ const originalLog = console.log;
30
+ let capturedOutput = '';
31
+
32
+ console.log = (...args) => {
33
+ capturedOutput += args.join(' ') + '\n';
34
+ originalLog(...args);
35
+ };
36
+
37
+ // Execute the command (this would be called from CLI)
38
+ // For now, we'll just save the command structure
39
+
40
+ const commandId = await commandDatabase.saveCommand(
41
+ command,
42
+ arguments,
43
+ '', // Result will be captured later
44
+ tags,
45
+ category
46
+ );
47
+
48
+ // Restore original console.log
49
+ console.log = originalLog;
50
+
51
+ return commandId;
52
+ } catch (err) {
53
+ console.error('Error logging command:', err);
54
+ return false;
55
+ }
56
+ }
57
+
58
+ async close() {
59
+ try {
60
+ await commandDatabase.close();
61
+ } catch (err) {
62
+ console.error('Error closing command logger:', err);
63
+ }
64
+ }
65
+ }
66
+
67
+ module.exports = new CommandLogger();
@@ -0,0 +1,135 @@
1
+ const sqlite3 = require('sqlite3').verbose();
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ class CommandDatabase {
6
+ constructor() {
7
+ this.dbPath = path.join(__dirname, '..', '..', 'commands.db');
8
+ this.db = null;
9
+ }
10
+
11
+ async initialize() {
12
+ // Create directory if it doesn't exist
13
+ const dbDir = path.dirname(this.dbPath);
14
+ if (!fs.existsSync(dbDir)) {
15
+ fs.mkdirSync(dbDir, { recursive: true });
16
+ }
17
+
18
+ this.db = new sqlite3.Database(this.dbPath);
19
+
20
+ return new Promise((resolve, reject) => {
21
+ this.db.serialize(() => {
22
+ this.db.run(`
23
+ CREATE TABLE IF NOT EXISTS commands (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
26
+ command TEXT NOT NULL,
27
+ arguments TEXT,
28
+ result TEXT,
29
+ tags TEXT,
30
+ category TEXT
31
+ )
32
+ `);
33
+
34
+ // Create indexes for better search performance
35
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_command ON commands(command)');
36
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_timestamp ON commands(timestamp)');
37
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_tags ON commands(tags)');
38
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_category ON commands(category)');
39
+ });
40
+
41
+ resolve();
42
+ });
43
+ }
44
+
45
+ async saveCommand(command, args = '', result = '', tags = '', category = '') {
46
+ return new Promise((resolve, reject) => {
47
+ const stmt = this.db.prepare(
48
+ 'INSERT INTO commands (command, arguments, result, tags, category) VALUES (?, ?, ?, ?, ?)'
49
+ );
50
+ stmt.run([command, args, result, tags, category], function(err) {
51
+ if (err) {
52
+ reject(err);
53
+ } else {
54
+ resolve(this.lastID);
55
+ }
56
+ });
57
+ stmt.finalize();
58
+ });
59
+ }
60
+
61
+ async getAllCommands() {
62
+ return new Promise((resolve, reject) => {
63
+ this.db.all('SELECT * FROM commands ORDER BY timestamp DESC', (err, rows) => {
64
+ if (err) {
65
+ reject(err);
66
+ } else {
67
+ resolve(rows);
68
+ }
69
+ });
70
+ });
71
+ }
72
+
73
+ async searchCommands(query) {
74
+ return new Promise((resolve, reject) => {
75
+ const searchQuery = `%${query}%`;
76
+ this.db.all(`
77
+ SELECT * FROM commands
78
+ WHERE command LIKE ?
79
+ OR arguments LIKE ?
80
+ OR result LIKE ?
81
+ OR tags LIKE ?
82
+ OR category LIKE ?
83
+ ORDER BY timestamp DESC
84
+ `, [searchQuery, searchQuery, searchQuery, searchQuery, searchQuery], (err, rows) => {
85
+ if (err) {
86
+ reject(err);
87
+ } else {
88
+ resolve(rows);
89
+ }
90
+ });
91
+ });
92
+ }
93
+
94
+ async getCommandById(id) {
95
+ return new Promise((resolve, reject) => {
96
+ this.db.get('SELECT * FROM commands WHERE id = ?', [id], (err, row) => {
97
+ if (err) {
98
+ reject(err);
99
+ } else {
100
+ resolve(row);
101
+ }
102
+ });
103
+ });
104
+ }
105
+
106
+ async getCommandsByCategory(category) {
107
+ return new Promise((resolve, reject) => {
108
+ this.db.all('SELECT * FROM commands WHERE category = ? ORDER BY timestamp DESC', [category], (err, rows) => {
109
+ if (err) {
110
+ reject(err);
111
+ } else {
112
+ resolve(rows);
113
+ }
114
+ });
115
+ });
116
+ }
117
+
118
+ async close() {
119
+ return new Promise((resolve, reject) => {
120
+ if (this.db) {
121
+ this.db.close((err) => {
122
+ if (err) {
123
+ reject(err);
124
+ } else {
125
+ resolve();
126
+ }
127
+ });
128
+ } else {
129
+ resolve();
130
+ }
131
+ });
132
+ }
133
+ }
134
+
135
+ module.exports = new CommandDatabase();
@@ -0,0 +1,381 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Klio GUI</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ }
12
+ .command-input {
13
+ font-family: 'Courier New', monospace;
14
+ }
15
+ .output-area {
16
+ font-family: 'Courier New', monospace;
17
+ white-space: pre-wrap;
18
+ }
19
+ .nav-link {
20
+ transition: all 0.2s ease;
21
+ }
22
+ .nav-link:hover {
23
+ background-color: rgba(59, 130, 246, 0.1);
24
+ }
25
+ .nav-link.active {
26
+ background-color: rgba(59, 130, 246, 0.2);
27
+ border-left: 3px solid #3b82f6;
28
+ }
29
+ </style>
30
+ </head>
31
+ <body class="bg-gray-50 min-h-screen">
32
+ <div class="flex min-h-screen">
33
+ <!-- Sidebar Navigation -->
34
+ <div class="w-64 bg-white border-r border-gray-200 flex flex-col">
35
+ <div class="p-4 border-b border-gray-200">
36
+ <h1 class="text-xl font-bold text-gray-800">Klio GUI</h1>
37
+ <p class="text-sm text-gray-600">Web Interface</p>
38
+ </div>
39
+
40
+ <nav class="flex-1 p-4">
41
+ <ul class="space-y-2">
42
+ <li>
43
+ <a href="#" onclick="showPage('runner')" id="nav-runner" class="nav-link flex items-center p-3 rounded-lg text-gray-700 active">
44
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
46
+ </svg>
47
+ Command Runner
48
+ </a>
49
+ </li>
50
+ <li>
51
+ <a href="#" onclick="showPage('commands')" id="nav-commands" class="nav-link flex items-center p-3 rounded-lg text-gray-700">
52
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
54
+ </svg>
55
+ Commands Reference
56
+ </a>
57
+ </li>
58
+ <li>
59
+ <a href="#" onclick="showPage('history')" id="nav-history" class="nav-link flex items-center p-3 rounded-lg text-gray-700">
60
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
62
+ </svg>
63
+ Command History
64
+ </a>
65
+ </li>
66
+ </ul>
67
+ </nav>
68
+
69
+ <div class="p-4 border-t border-gray-200">
70
+ <div class="text-xs text-gray-500">
71
+ <p>Klio GUI v0.1</p>
72
+ <p>GUI Interface</p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Main Content -->
78
+ <div class="flex-1 flex flex-col">
79
+ <!-- Header -->
80
+ <header class="bg-white border-b border-gray-200 p-4">
81
+ <div class="flex items-center justify-between">
82
+ <h1 class="text-lg font-semibold text-gray-800" id="page-title">Command Runner</h1>
83
+ </div>
84
+ </header>
85
+
86
+ <!-- Page Content -->
87
+ <main class="flex-1 overflow-y-auto p-6">
88
+ <!-- Command Runner Page -->
89
+ <div id="runner-page">
90
+ <div class="bg-white shadow-sm rounded-lg p-6 mb-6">
91
+ <div class="mb-4">
92
+ <label for="commandInput" class="block text-sm font-medium text-gray-700 mb-2">Command</label>
93
+ <div class="flex gap-2">
94
+ <input
95
+ type="text"
96
+ id="commandInput"
97
+ placeholder="e.g., moon --s"
98
+ class="flex-1 command-input px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
99
+ >
100
+ <button
101
+ onclick="runCommand()"
102
+ class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
103
+ >
104
+ Run
105
+ </button>
106
+ </div>
107
+ </div>
108
+ <div class="text-sm text-gray-600">
109
+ <p class="mb-2">Examples:</p>
110
+ <div class="flex flex-wrap gap-4 text-xs">
111
+ <button onclick="commandInput.value = '--v 3 saturn neptune'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">moon --s</button>
112
+ <button onclick="commandInput.value = '--rx --i'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--rx</button>
113
+ <button onclick="commandInput.value = '--c'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--c</button>
114
+ <button onclick="commandInput.value = 'mars venus --k'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">mars venus --k</button>
115
+ <button onclick="commandInput.value = '--el'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
116
+ <button onclick="commandInput.value = '--s'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
117
+ <button onclick="commandInput.value = '--a'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
118
+
119
+
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <div class="bg-white shadow-sm rounded-lg p-6">
125
+ <h2 class="text-lg font-semibold text-gray-800 mb-4">Output</h2>
126
+ <div id="outputArea" class="output-area bg-gray-100 p-4 rounded border min-h-[300px]">
127
+ Command output will appear here...
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Commands Reference Page -->
133
+ <div id="commands-page" class="hidden">
134
+ <div class="bg-white shadow-sm rounded-lg p-6">
135
+ <h2 class="text-lg font-semibold text-gray-800 mb-4">Available Commands</h2>
136
+ <div class="prose max-w-none text-sm">
137
+ <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Basic Astrological Data</h3>
138
+ <ul class="list-disc pl-6 space-y-2">
139
+ <li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet]</code> - Displays astrological data for a specific planet</li>
140
+ <li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet] --hs koch</code> - Shows planet data with house and house system</li>
141
+ <li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet] [planet2] --k</code> - Shows aspects between two planets</li>
142
+ <li><code class="bg-gray-100 px-2 py-1 rounded">klio --a</code> - Shows all current aspects</li>
143
+ <li><code class="bg-gray-100 px-2 py-1 rounded">klio --s</code> - Shows all planet positions</li>
144
+ <li><code class="bg-gray-100 px-2 py-1 rounded">klio --s --hs placidus</code> - Different house systems</li>
145
+ </ul>
146
+
147
+ <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">House Systems</h3>
148
+ <ul class="list-disc pl-6 space-y-2">
149
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--hs &lt;system&gt;</code> - Shows planet + house position (Placidus, Koch, Porphyry, Regiomontanus, Campanus, Equal, WholeSign, Gauquelin, Vehlow, Topocentric, Alcabitius, Morinus)</li>
150
+ </ul>
151
+
152
+ <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Date and Time</h3>
153
+ <ul class="list-disc pl-6 space-y-2">
154
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--d &lt;date&gt;</code> - Use a specific date (Format: DD.MM.YYYY or "DD.MM.YYYY HH:MM")</li>
155
+ </ul>
156
+
157
+ <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Advanced Features</h3>
158
+ <ul class="list-disc pl-6 space-y-2">
159
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--rx</code> - Shows all retrograde or stationary planets</li>
160
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--c</code> - Shows all planets on critical degrees</li>
161
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--el</code> - Shows element distribution of planets</li>
162
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--af</code> - Shows active aspect figures</li>
163
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--tr</code> - Shows personal transits based on birth data</li>
164
+ </ul>
165
+
166
+ <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Past and Future Aspects</h3>
167
+ <ul class="list-disc pl-6 space-y-2">
168
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--v &lt;count&gt; [planet1] [aspect-type] [planet2]</code> - Shows past aspects</li>
169
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--z &lt;count&gt; [planet1] [aspect-type] [planet2]</code> - Shows future aspects</li>
170
+ </ul>
171
+
172
+ <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Configuration</h3>
173
+ <ul class="list-disc pl-6 space-y-2">
174
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--setup</code> - Setup for a default chart</li>
175
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--status</code> - Shows the stored configuration data</li>
176
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--i</code> - Uses birth data from setup</li>
177
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--person &lt;id&gt; &lt;data&gt;</code> - Creates or updates any person</li>
178
+ <li><code class="bg-gray-100 px-2 py-1 rounded">--people</code> - Lists all saved persons</li>
179
+ </ul>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- Command History Page -->
185
+ <div id="history-page" class="hidden">
186
+ <div class="bg-white shadow-sm rounded-lg p-6">
187
+ <h2 class="text-lg font-semibold text-gray-800 mb-4">Command History</h2>
188
+ <div id="historyContainer" class="space-y-2 max-h-[500px] overflow-y-auto pr-2"></div>
189
+ </div>
190
+ </div>
191
+ </main>
192
+ </div>
193
+ </div>
194
+
195
+ <div id="toast" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-md shadow-lg opacity-0 transform translate-y-4 transition-all duration-300"></div>
196
+
197
+ <script>
198
+ let commandHistory = [];
199
+ const maxHistory = 50;
200
+
201
+ // DOM elements
202
+ const commandInput = document.getElementById('commandInput');
203
+ const historyContainer = document.getElementById('historyContainer');
204
+ const outputArea = document.getElementById('outputArea');
205
+ const toastElement = document.getElementById('toast');
206
+
207
+ // Initialize the app
208
+ function init() {
209
+ loadHistory();
210
+
211
+ // Add event listener for Enter key
212
+ commandInput.addEventListener('keypress', (e) => {
213
+ if (e.key === 'Enter') {
214
+ runCommand();
215
+ }
216
+ });
217
+
218
+ // Focus on command input
219
+ commandInput.focus();
220
+ }
221
+
222
+ // Page navigation function
223
+ function showPage(pageName) {
224
+ // Hide all pages
225
+ document.getElementById('runner-page').classList.add('hidden');
226
+ document.getElementById('commands-page').classList.add('hidden');
227
+ document.getElementById('history-page').classList.add('hidden');
228
+
229
+ // Remove active state from all nav items
230
+ document.getElementById('nav-runner').classList.remove('active');
231
+ document.getElementById('nav-commands').classList.remove('active');
232
+ document.getElementById('nav-history').classList.remove('active');
233
+
234
+ // Show selected page
235
+ if (pageName === 'runner') {
236
+ document.getElementById('runner-page').classList.remove('hidden');
237
+ document.getElementById('nav-runner').classList.add('active');
238
+ document.getElementById('page-title').textContent = 'Command Runner';
239
+ } else if (pageName === 'commands') {
240
+ document.getElementById('commands-page').classList.remove('hidden');
241
+ document.getElementById('nav-commands').classList.add('active');
242
+ document.getElementById('page-title').textContent = 'Commands Reference';
243
+ } else if (pageName === 'history') {
244
+ document.getElementById('history-page').classList.remove('hidden');
245
+ document.getElementById('nav-history').classList.add('active');
246
+ document.getElementById('page-title').textContent = 'Command History';
247
+ }
248
+ }
249
+
250
+ // Load history from localStorage
251
+ function loadHistory() {
252
+ try {
253
+ const savedHistory = localStorage.getItem('astrocliHistory');
254
+ if (savedHistory) {
255
+ commandHistory = JSON.parse(savedHistory);
256
+ renderHistory();
257
+ }
258
+ } catch (error) {
259
+ console.error('Error loading history:', error);
260
+ }
261
+ }
262
+
263
+ // Save history to localStorage
264
+ function saveHistory() {
265
+ try {
266
+ localStorage.setItem('astrocliHistory', JSON.stringify(commandHistory));
267
+ } catch (error) {
268
+ console.error('Error saving history:', error);
269
+ }
270
+ }
271
+
272
+ // Add command to history
273
+ function addToHistory(command) {
274
+ // Remove duplicate if exists
275
+ commandHistory = commandHistory.filter(cmd => cmd !== command);
276
+
277
+ // Add new command to beginning
278
+ commandHistory.unshift(command);
279
+
280
+ // Limit history size
281
+ if (commandHistory.length > maxHistory) {
282
+ commandHistory = commandHistory.slice(0, maxHistory);
283
+ }
284
+
285
+ saveHistory();
286
+ renderHistory();
287
+ }
288
+
289
+ // Render history
290
+ function renderHistory() {
291
+ if (commandHistory.length === 0) {
292
+ historyContainer.innerHTML = '<div class="text-gray-500 text-sm">No command history yet</div>';
293
+ return;
294
+ }
295
+
296
+ let html = '<div class="space-y-1">';
297
+ commandHistory.forEach((cmd, index) => {
298
+ html += `<div
299
+ class="command-input px-3 py-2 bg-gray-100 rounded cursor-pointer hover:bg-gray-200 text-sm truncate"
300
+ title="${cmd}"
301
+ onclick="commandInput.value = '${cmd.replace(/'/g, "\\'")}'; showPage('runner'); commandInput.focus()"
302
+ >${cmd}</div>`;
303
+ });
304
+ html += '</div>';
305
+
306
+ historyContainer.innerHTML = html;
307
+ }
308
+
309
+ // Run command
310
+ async function runCommand() {
311
+ const command = commandInput.value.trim();
312
+
313
+ if (!command) {
314
+ showToast('Please enter a command');
315
+ return;
316
+ }
317
+
318
+ try {
319
+ // Show loading state
320
+ outputArea.textContent = 'Running command...\n\n';
321
+
322
+ // Add to history
323
+ addToHistory(command);
324
+
325
+ // Clear input for next command
326
+ commandInput.value = '';
327
+
328
+ // Run the command via API
329
+ const response = await fetch('/api/run-command', {
330
+ method: 'POST',
331
+ headers: {
332
+ 'Content-Type': 'application/json'
333
+ },
334
+ body: JSON.stringify({ command })
335
+ });
336
+
337
+ const result = await response.json();
338
+
339
+ if (response.ok) {
340
+ // Display the result with better formatting
341
+ if (result.output) {
342
+ const cleanedOutput = result.output
343
+ .replace(/\n{3,}/g, '\n\n')
344
+ .trim();
345
+ outputArea.textContent = cleanedOutput;
346
+ } else {
347
+ outputArea.textContent = 'Command executed successfully';
348
+ }
349
+ } else {
350
+ let errorMessage = 'Error: ' + result.error;
351
+ if (result.details) {
352
+ errorMessage += '\n' + result.details;
353
+ }
354
+ outputArea.textContent = errorMessage;
355
+ showToast('Command failed');
356
+ }
357
+
358
+ } catch (error) {
359
+ console.error('Error running command:', error);
360
+ outputArea.textContent = 'Error: ' + error.message;
361
+ showToast('Command failed');
362
+ }
363
+ }
364
+
365
+ // Show toast message
366
+ function showToast(message) {
367
+ toastElement.textContent = message;
368
+ toastElement.classList.remove('opacity-0', 'translate-y-4');
369
+ toastElement.classList.add('opacity-100', 'translate-y-0');
370
+
371
+ setTimeout(() => {
372
+ toastElement.classList.add('opacity-0', 'translate-y-4');
373
+ toastElement.classList.remove('opacity-100', 'translate-y-0');
374
+ }, 3000);
375
+ }
376
+
377
+ // Initialize the app when DOM is loaded
378
+ document.addEventListener('DOMContentLoaded', init);
379
+ </script>
380
+ </body>
381
+ </html>
@@ -0,0 +1,134 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const commandDatabase = require('../database');
4
+ const { exec } = require('child_process');
5
+ const path = require('path');
6
+
7
+ // Initialize database
8
+ commandDatabase.initialize().catch(err => {
9
+ console.error('Failed to initialize database:', err);
10
+ });
11
+
12
+ // Get all commands
13
+ router.get('/commands', async (req, res) => {
14
+ try {
15
+ const commands = await commandDatabase.getAllCommands();
16
+ res.json(commands);
17
+ } catch (err) {
18
+ console.error('Error fetching commands:', err);
19
+ res.status(500).json({ error: 'Failed to fetch commands' });
20
+ }
21
+ });
22
+
23
+ // Search commands
24
+ router.get('/commands/search', async (req, res) => {
25
+ const query = req.query.q || '';
26
+
27
+ try {
28
+ const results = await commandDatabase.searchCommands(query);
29
+ res.json(results);
30
+ } catch (err) {
31
+ console.error('Error searching commands:', err);
32
+ res.status(500).json({ error: 'Failed to search commands' });
33
+ }
34
+ });
35
+
36
+ // Get specific command by ID
37
+ router.get('/commands/:id', async (req, res) => {
38
+ try {
39
+ const command = await commandDatabase.getCommandById(req.params.id);
40
+ if (command) {
41
+ res.json(command);
42
+ } else {
43
+ res.status(404).json({ error: 'Command not found' });
44
+ }
45
+ } catch (err) {
46
+ console.error('Error fetching command:', err);
47
+ res.status(500).json({ error: 'Failed to fetch command' });
48
+ }
49
+ });
50
+
51
+ // Get commands by category
52
+ router.get('/commands/category/:category', async (req, res) => {
53
+ try {
54
+ const commands = await commandDatabase.getCommandsByCategory(req.params.category);
55
+ res.json(commands);
56
+ } catch (err) {
57
+ console.error('Error fetching commands by category:', err);
58
+ res.status(500).json({ error: 'Failed to fetch commands by category' });
59
+ }
60
+ });
61
+
62
+ // Save a new command
63
+ router.post('/commands', async (req, res) => {
64
+ try {
65
+ const { command, args, result, tags, category } = req.body;
66
+
67
+ if (!command) {
68
+ return res.status(400).json({ error: 'Command is required' });
69
+ }
70
+
71
+ const commandId = await commandDatabase.saveCommand(command, args, result, tags, category);
72
+ res.status(201).json({ id: commandId, message: 'Command saved successfully' });
73
+ } catch (err) {
74
+ console.error('Error saving command:', err);
75
+ res.status(500).json({ error: 'Failed to save command' });
76
+ }
77
+ });
78
+
79
+ // Health check endpoint
80
+ router.get('/health', (req, res) => {
81
+ res.json({ status: 'healthy', timestamp: new Date().toISOString() });
82
+ });
83
+
84
+ // Run command endpoint
85
+ router.post('/run-command', async (req, res) => {
86
+ try {
87
+ const { command } = req.body;
88
+
89
+ if (!command) {
90
+ return res.status(400).json({ error: 'Command is required' });
91
+ }
92
+
93
+ // Execute the command using child_process
94
+ const commandToRun = command.startsWith('klio ') ? command : `klio ${command}`;
95
+ const mainJsPath = path.join(__dirname, '..', '..', '..', 'src', 'main.js');
96
+
97
+ exec(`node ${mainJsPath} ${command}`, {
98
+ cwd: path.join(__dirname, '..', '..', '..'),
99
+ maxBuffer: 1024 * 1024 * 10 // 10MB buffer
100
+ }, (error, stdout, stderr) => {
101
+ if (error) {
102
+ console.error('Command execution error:', error);
103
+ console.error('Stderr:', stderr);
104
+ return res.status(500).json({
105
+ error: 'Command execution failed',
106
+ details: stderr || error.message,
107
+ command: commandToRun
108
+ });
109
+ }
110
+
111
+ // Save command to history
112
+ commandDatabase.saveCommand(
113
+ command,
114
+ '',
115
+ stdout,
116
+ '',
117
+ 'executed'
118
+ ).catch(err => {
119
+ console.error('Failed to save command to history:', err);
120
+ });
121
+
122
+ res.json({
123
+ output: stdout,
124
+ command: command
125
+ });
126
+ });
127
+
128
+ } catch (err) {
129
+ console.error('Error in run-command endpoint:', err);
130
+ res.status(500).json({ error: 'Failed to execute command' });
131
+ }
132
+ });
133
+
134
+ module.exports = router;
@@ -0,0 +1,82 @@
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const apiRouter = require('./routes/api');
4
+ const commandDatabase = require('./database');
5
+
6
+ class GUIServer {
7
+ constructor() {
8
+ this.app = express();
9
+ this.server = null;
10
+ this.port = 37421; // Less common port
11
+ }
12
+
13
+ async initialize() {
14
+ // Initialize database
15
+ await commandDatabase.initialize();
16
+
17
+ // Middleware
18
+ this.app.use(express.json());
19
+ this.app.use(express.urlencoded({ extended: true }));
20
+
21
+ // API routes
22
+ this.app.use('/api', apiRouter);
23
+
24
+ // Serve static files from public directory
25
+ this.app.use(express.static(path.join(__dirname, 'public')));
26
+
27
+ // Serve index.html for all other routes (for SPA behavior)
28
+ this.app.get('*', (req, res) => {
29
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
30
+ });
31
+
32
+ // Error handling middleware
33
+ this.app.use((err, req, res, next) => {
34
+ console.error('Server error:', err);
35
+ res.status(500).json({ error: 'Internal server error' });
36
+ });
37
+ }
38
+
39
+ start(port = this.port) {
40
+ return new Promise((resolve, reject) => {
41
+ this.server = this.app.listen(port, () => {
42
+ console.log(`🌐 GUI Server running on http://localhost:${port}`);
43
+ console.log(`šŸ“Š Command history available at http://localhost:${port}`);
44
+ console.log('šŸ’” Press Ctrl+C to stop the server');
45
+ resolve();
46
+ });
47
+
48
+ this.server.on('error', (err) => {
49
+ if (err.code === 'EADDRINUSE') {
50
+ console.error(`Port ${port} is already in use. Trying port ${port + 1}...`);
51
+ this.start(port + 1).then(resolve).catch(reject);
52
+ } else {
53
+ reject(err);
54
+ }
55
+ });
56
+ });
57
+ }
58
+
59
+ async stop() {
60
+ return new Promise((resolve, reject) => {
61
+ if (this.server) {
62
+ this.server.close(async () => {
63
+ await commandDatabase.close();
64
+ console.log('GUI Server stopped');
65
+ resolve();
66
+ });
67
+ } else {
68
+ resolve();
69
+ }
70
+ });
71
+ }
72
+
73
+ getPort() {
74
+ if (this.server) {
75
+ const address = this.server.address();
76
+ return address && address.port;
77
+ }
78
+ return this.port;
79
+ }
80
+ }
81
+
82
+ module.exports = new GUIServer();