klio 1.4.1 → 1.4.3

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
@@ -62,8 +90,6 @@ It's possible to analyze Apple Health data. This happens on the device and no se
62
90
  - **Step analysis**: `klio [planet] --apple <filepath> --steps` - Analyzes step patterns of any planet
63
91
  - **Stress analysis**: `klio [planet] --apple <filepath> --stress` - Analyzes stress patterns of planet sign stress based on HRV
64
92
 
65
- > Maybe I'll add some more in the future.
66
-
67
93
  ### CSV Analysis
68
94
  It's possible to analyze a csv with a column of either ISO date time or unix time stamp.
69
95
 
@@ -72,9 +98,6 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
72
98
 
73
99
  The command also returns a Chi-Square.
74
100
 
75
- You can research a lot of fun things with this. You find a lot of free datasets on Kaggle.
76
-
77
-
78
101
  ### Adding different charts
79
102
 
80
103
  - **Use personal data**: `--i` - Uses birth data from setup for calculations
@@ -99,6 +122,13 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
99
122
  - **Past aspects**: `klio --v <count> [planet1] [aspect-type] [planet2]` - Shows past aspects (Format: --v <count> planet1 aspectType planet2) Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)
100
123
  - **Future aspects**: `klio --z <count> [planet1] [aspect-type] [planet2]` - Shows future aspects (Format: --z <count> planet1 aspectType planet2)
101
124
 
125
+ ### Planet Ingresses
126
+
127
+ - **Planet ingress**: `klio [planet] --in [count]` - Shows when a planet enters a new zodiac sign. Optional count parameter for multiple ingresses (default: 1). Works with any planet (moon, mercury, venus, etc.)
128
+ - Example: `klio moon --in` - Shows next moon ingress
129
+ - Example: `klio saturn --in 2` - Shows next 2 Saturn ingresses (note: slow planets may have limited future data)
130
+ - Example: `klio mercury --in --d "15.03.2026"` - Shows next Mercury ingress from a specific date
131
+
102
132
  ### AI Integration
103
133
 
104
134
  - **AI model selection**: `--ai <model>` - Sets a specific AI model (e.g., "google/gemma-3n-e4b") for LM Studio ([lmstudio.ai](https://lmstudio.ai))
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.3",
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
  }
@@ -2491,6 +2491,223 @@ function analyzeHouseDistributionSignificance(houseDistribution) {
2491
2491
  };
2492
2492
  }
2493
2493
 
2494
+ // Function to calculate the next planet ingress (when planet enters a new sign)
2495
+ function calculateNextPlanetIngress(planetName, startDate = null, count = 1) {
2496
+ // Use current date if no start date is provided
2497
+ const startDateObj = startDate ? new Date(startDate.year, startDate.month - 1, startDate.day, startDate.hour, startDate.minute) : new Date();
2498
+
2499
+ const results = [];
2500
+ let currentDate = new Date(startDateObj);
2501
+
2502
+ // Calculate timezone offset for the start date
2503
+ const config = loadConfig();
2504
+ let timezone = 'Europe/Zurich'; // Default
2505
+
2506
+ if (config && config.currentLocation && config.currentLocation.timezone) {
2507
+ timezone = config.currentLocation.timezone;
2508
+ }
2509
+
2510
+ const timezoneOffsetMinutes = getTimezoneOffset({
2511
+ year: currentDate.getFullYear(),
2512
+ month: currentDate.getMonth() + 1,
2513
+ day: currentDate.getDate(),
2514
+ hour: currentDate.getHours(),
2515
+ minute: currentDate.getMinutes()
2516
+ }, timezone);
2517
+
2518
+ // Get current planet position
2519
+ const currentPlanetData = getAstrologicalData(planetName, {
2520
+ year: currentDate.getFullYear(),
2521
+ month: currentDate.getMonth() + 1,
2522
+ day: currentDate.getDate(),
2523
+ hour: currentDate.getHours(),
2524
+ minute: currentDate.getMinutes()
2525
+ });
2526
+
2527
+ let currentSignIndex = signs.indexOf(currentPlanetData.sign);
2528
+ let targetSignIndex = (currentSignIndex + 1) % 12; // Next sign
2529
+
2530
+ for (let i = 0; i < count; i++) {
2531
+ // Find the next ingress by checking positions in 1-hour increments
2532
+ let foundIngress = false;
2533
+ let ingressDate = null;
2534
+ let attempts = 0;
2535
+
2536
+ // Dynamic search strategy based on planet speed
2537
+ const verySlowPlanets = ['saturn', 'uranus', 'neptune', 'pluto'];
2538
+ const mediumSlowPlanets = ['jupiter'];
2539
+ const isVerySlowPlanet = verySlowPlanets.includes(planetName);
2540
+ const isMediumSlowPlanet = mediumSlowPlanets.includes(planetName);
2541
+
2542
+ // Determine time step based on planet speed
2543
+ const timeStepHours = isVerySlowPlanet ? 24 :
2544
+ isMediumSlowPlanet ? 6 :
2545
+ 1; // Daily for very slow, 6-hourly for medium, hourly for fast
2546
+
2547
+ // Maximum search window (in days)
2548
+ // For very slow planets, we need to search much further ahead
2549
+ // Saturn: ~2.5 years per sign, so for multiple ingresses we need ~5-10 years
2550
+ // Jupiter: ~1 year per sign, so for multiple ingresses we need ~3-5 years
2551
+ // Uranus: ~7 years per sign, so we need ~15-20 years
2552
+ // Neptune: ~13.75 years per sign, so we need ~25-30 years
2553
+ // Pluto: ~20.7 years per sign, so we need ~35-40 years
2554
+ // For fast planets (Moon, Mercury, Venus, Mars, Sun), we need a reasonable window per ingress
2555
+ const fastPlanets = ['moon', 'mercury', 'venus', 'mars', 'sun'];
2556
+ const isFastPlanet = fastPlanets.includes(planetName);
2557
+
2558
+ const maxDays = isVerySlowPlanet ? 365 * 90 :
2559
+ isMediumSlowPlanet ? 365 * 90 :
2560
+ isFastPlanet ? 90 : // 30 days for fast planets (enough for multiple moon ingresses)
2561
+ 365 * 90; // Default for other planets
2562
+ const maxAttempts = maxDays * 24 / timeStepHours;
2563
+
2564
+ while (!foundIngress && attempts < maxAttempts) {
2565
+ attempts++;
2566
+ currentDate.setHours(currentDate.getHours() + timeStepHours);
2567
+
2568
+ const testPlanetData = getAstrologicalData(planetName, {
2569
+ year: currentDate.getFullYear(),
2570
+ month: currentDate.getMonth() + 1,
2571
+ day: currentDate.getDate(),
2572
+ hour: currentDate.getHours(),
2573
+ minute: currentDate.getMinutes()
2574
+ });
2575
+
2576
+ const testSignIndex = signs.indexOf(testPlanetData.sign);
2577
+
2578
+ if (testSignIndex === targetSignIndex) {
2579
+ // Found the ingress! Now refine to get more precise time
2580
+ ingressDate = findExactPlanetIngressTime(planetName, currentDate, targetSignIndex, timezoneOffsetMinutes);
2581
+ foundIngress = true;
2582
+ }
2583
+ }
2584
+
2585
+ if (foundIngress) {
2586
+ results.push({
2587
+ sign: signs[targetSignIndex],
2588
+ date: ingressDate,
2589
+ fromSign: signs[currentSignIndex]
2590
+ });
2591
+
2592
+ // Prepare for next ingress
2593
+ currentSignIndex = targetSignIndex;
2594
+ targetSignIndex = (currentSignIndex + 1) % 12;
2595
+ // Convert ingressDate object to Date object
2596
+ currentDate = new Date(ingressDate.year, ingressDate.month - 1, ingressDate.day, ingressDate.hour, ingressDate.minute);
2597
+ // For fast planets, start searching a few hours after the ingress to avoid missing the next one
2598
+ if (isFastPlanet) {
2599
+ currentDate.setHours(currentDate.getHours() + 6); // Start 6 hours after ingress
2600
+ }
2601
+ } else {
2602
+ const daysLimit = isVerySlowPlanet ? 365 * 40 :
2603
+ isMediumSlowPlanet ? 365 * 5 :
2604
+ isFastPlanet ? 30 :
2605
+ 60;
2606
+ console.log(`Could not find ${planetName} ingress to ${signs[targetSignIndex]} within ${daysLimit} days`);
2607
+ if (isVerySlowPlanet) {
2608
+ console.log(`💡 Note: For very slow planets like ${planetName}, the ephemeris data may not extend far enough into the future.`);
2609
+ } else if (isMediumSlowPlanet) {
2610
+ console.log(`💡 Note: For medium-slow planets like ${planetName}, the ephemeris data may not extend far enough into the future.`);
2611
+ } else {
2612
+ console.log(`💡 Note: For ${planetName}, the requested number of ingresses may be too high.`);
2613
+ }
2614
+ console.log(`💡 Try requesting fewer ingresses or use a shorter time frame with the --d option.`);
2615
+ // Continue to next iteration instead of breaking the entire loop
2616
+ continue;
2617
+ }
2618
+ }
2619
+
2620
+ return results;
2621
+ }
2622
+
2623
+ // Helper function to find exact planet ingress time using binary search
2624
+ function findExactPlanetIngressTime(planetName, approximateDate, targetSignIndex, timezoneOffsetMinutes) {
2625
+ const targetLongitude = targetSignIndex * 30; // Each sign is 30 degrees
2626
+
2627
+ // Convert approximate date to Julian Day
2628
+ const approxJulianDay = calculateJulianDayUTC({
2629
+ year: approximateDate.getFullYear(),
2630
+ month: approximateDate.getMonth() + 1,
2631
+ day: approximateDate.getDate(),
2632
+ hour: approximateDate.getHours(),
2633
+ minute: approximateDate.getMinutes()
2634
+ }, timezoneOffsetMinutes);
2635
+
2636
+ // Binary search parameters
2637
+ let low = approxJulianDay - 0.1; // 2.4 hours before
2638
+ let high = approxJulianDay + 0.1; // 2.4 hours after
2639
+ let precision = 0.0001; // About 1.44 minutes precision
2640
+
2641
+ // Perform binary search
2642
+ for (let i = 0; i < 50; i++) {
2643
+ const mid = (low + high) / 2;
2644
+ const result = swisseph.swe_calc_ut(mid, planets[planetName], swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED);
2645
+
2646
+ if (result.error) {
2647
+ // Fallback to Moshier
2648
+ const moshierResult = swisseph.swe_calc_ut(mid, planets[planetName], swisseph.SEFLG_MOSEPH | swisseph.SEFLG_SPEED);
2649
+ if (moshierResult.error) {
2650
+ console.error(`Error calculating ${planetName} position:`, moshierResult.error);
2651
+ break;
2652
+ }
2653
+ result.longitude = moshierResult.longitude;
2654
+ }
2655
+
2656
+ if (result.longitude >= targetLongitude) {
2657
+ high = mid;
2658
+ } else {
2659
+ low = mid;
2660
+ }
2661
+
2662
+ if (high - low < precision) {
2663
+ break;
2664
+ }
2665
+ }
2666
+
2667
+ // Convert Julian Day back to date components
2668
+ const finalJulianDay = (low + high) / 2;
2669
+ return convertJulianDayToDate(finalJulianDay, timezoneOffsetMinutes);
2670
+ }
2671
+
2672
+ // Helper function to convert Julian Day to date components
2673
+ function convertJulianDayToDate(julianDay, timezoneOffsetMinutes) {
2674
+ // This is a simplified conversion - for more accuracy, use a proper algorithm
2675
+ // We'll use the Swiss Ephemeris function if available
2676
+ try {
2677
+ const result = swisseph.swe_revjul(julianDay, swisseph.SE_GREG_CAL);
2678
+
2679
+ // Extract minutes from the decimal part of the hour
2680
+ const hour = Math.floor(result.hour);
2681
+ const minute = Math.round((result.hour - hour) * 60);
2682
+
2683
+ // Adjust for timezone
2684
+ const utcDate = new Date(Date.UTC(result.year, result.month - 1, result.day, hour, minute));
2685
+ utcDate.setMinutes(utcDate.getMinutes() + timezoneOffsetMinutes);
2686
+
2687
+ return {
2688
+ year: utcDate.getFullYear(),
2689
+ month: utcDate.getMonth() + 1,
2690
+ day: utcDate.getDate(),
2691
+ hour: utcDate.getHours(),
2692
+ minute: utcDate.getMinutes()
2693
+ };
2694
+ } catch (error) {
2695
+ console.error('Error converting Julian Day to date:', error);
2696
+
2697
+ // Fallback: return approximate date
2698
+ const baseDate = new Date();
2699
+ baseDate.setTime((julianDay - 2440587.5) * 86400000); // Convert Julian Day to milliseconds
2700
+
2701
+ return {
2702
+ year: baseDate.getFullYear(),
2703
+ month: baseDate.getMonth() + 1,
2704
+ day: baseDate.getDate(),
2705
+ hour: baseDate.getHours(),
2706
+ minute: baseDate.getMinutes()
2707
+ };
2708
+ }
2709
+ }
2710
+
2494
2711
 
2495
2712
  module.exports = {
2496
2713
  calculateHouses,
@@ -2527,5 +2744,6 @@ module.exports = {
2527
2744
  analyzeSignDistributionSignificance,
2528
2745
  calculateAspectStatistics,
2529
2746
  calculatePlanetComboAspects,
2530
- showPlanetComboAspects
2747
+ showPlanetComboAspects,
2748
+ calculateNextPlanetIngress
2531
2749
  };
package/src/cli/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { Command } = require('commander');
2
2
  const { planets, signs } = require('../astrology/astrologyConstants');
3
3
  const { showRetrogradePlanets } = require('../astrology/retrogradeService');
4
- const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode } = require('../astrology/astrologyService');
4
+ const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress } = require('../astrology/astrologyService');
5
5
  const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson } = require('../config/configService');
6
6
  const { parseAppleHealthXML } = require('../health/healthService');
7
7
  const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep } = require('../health/healthAnalysis');
@@ -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,40 @@ 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('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
258
+ .option('--gui', 'Launches the web interface for command history (port 37421)')
259
+ .option('--gui-port <port>', 'Specify custom port for GUI server')
248
260
  .description('Shows astrological data for a planet')
249
261
  .action(async (planetArg, planet2Arg, options) => {
250
262
  // If planet2Arg is an object, it contains the options (commander behavior)
251
263
  let planet2 = typeof planet2Arg === 'string' ? planet2Arg : null;
252
264
  let actualOptions = typeof planet2Arg === 'object' ? planet2Arg : options;
265
+
266
+ // Handle GUI launch if --gui flag is specified
267
+ if (options.gui) {
268
+ if (!guiServer) {
269
+ console.error('❌ GUI server is not available. Please install required dependencies.');
270
+ console.error('💡 Run: npm install express sqlite3');
271
+ process.exit(1);
272
+ }
273
+
274
+ try {
275
+ await guiServer.initialize();
276
+ const port = options.guiPort ? parseInt(options.guiPort) : 37421;
277
+ await guiServer.start(port);
278
+
279
+ console.log(`\n🌐 GUI Server started successfully!`);
280
+ console.log(`📊 Open your browser and navigate to: http://localhost:${guiServer.getPort()}`);
281
+ console.log(`💡 Press Ctrl+C to stop the server and return to CLI`);
282
+
283
+ // Keep the process running
284
+ await new Promise(() => {});
285
+ return;
286
+ } catch (err) {
287
+ console.error('❌ Failed to start GUI server:', err.message);
288
+ process.exit(1);
289
+ }
290
+ }
253
291
 
254
292
  // Show critical planets if --c option is specified (no planet required)
255
293
  if (actualOptions.c) {
@@ -975,6 +1013,105 @@ program
975
1013
  return;
976
1014
  }
977
1015
 
1016
+ // Show next planet ingress if --in option is specified
1017
+ if (options.in) {
1018
+ // Determine the planet to use (default to moon if no planet specified)
1019
+ const planetName = planetArg ? planetArg.toLowerCase() : 'moon';
1020
+
1021
+ // Validate planet name
1022
+ if (planets[planetName] === undefined) {
1023
+ console.error(`Error: Invalid planet '${planetName}'. Available planets: ${Object.keys(planets).join(', ')}`);
1024
+ process.exit(1);
1025
+ }
1026
+
1027
+ // Parse the count parameter (default to 1 if not specified)
1028
+ const count = options.in && typeof options.in === 'string' && options.in.trim() ? parseInt(options.in) : 1;
1029
+
1030
+ // Use custom date if specified
1031
+ let customDate = null;
1032
+ if (options.d) {
1033
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
1034
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
1035
+ const match = options.d.match(dateRegex);
1036
+
1037
+ if (match) {
1038
+ const day = parseInt(match[1], 10);
1039
+ const month = parseInt(match[2], 10);
1040
+ const year = parseInt(match[3], 10);
1041
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
1042
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
1043
+
1044
+ // Check if the date is valid
1045
+ const date = new Date(year, month - 1, day, hour, minute);
1046
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
1047
+ customDate = {
1048
+ day: day,
1049
+ month: month,
1050
+ year: year,
1051
+ hour: hour,
1052
+ minute: minute
1053
+ };
1054
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
1055
+ } else {
1056
+ console.error('Invalid date:', options.d);
1057
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
1058
+ process.exit(1);
1059
+ }
1060
+ } else {
1061
+ console.error('Invalid date:', options.d);
1062
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
1063
+ process.exit(1);
1064
+ }
1065
+ }
1066
+
1067
+ // Calculate next planet ingress(es)
1068
+ const ingresses = calculateNextPlanetIngress(planetName, customDate, count);
1069
+
1070
+ if (ingresses.length === 0) {
1071
+ console.log(`No ${planetName} ingresses found.`);
1072
+ return;
1073
+ }
1074
+
1075
+ const planetNameCapitalized = planetName.charAt(0).toUpperCase() + planetName.slice(1);
1076
+ console.log(`Next ${planetNameCapitalized} Ingress(es):`);
1077
+ console.log('================================================================================');
1078
+ console.log('| # | Date & Time | From Sign | To Sign |');
1079
+ console.log('================================================================================');
1080
+
1081
+ ingresses.forEach((ingress, index) => {
1082
+ const dateStr = `${ingress.date.day.toString().padStart(2, '0')}.${ingress.date.month.toString().padStart(2, '0')}.${ingress.date.year} ${ingress.date.hour.toString().padStart(2, '0')}:${ingress.date.minute.toString().padStart(2, '0')}`;
1083
+ const fromSign = ingress.fromSign.padEnd(12, ' ');
1084
+ const toSign = ingress.sign.padEnd(12, ' ');
1085
+
1086
+ console.log(`| ${(index + 1).toString().padEnd(2)} | ${dateStr.padEnd(21)} | ${fromSign} | ${toSign} |`);
1087
+ });
1088
+
1089
+ console.log('================================================================================');
1090
+
1091
+ if (count > 1) {
1092
+ console.log(`\nShowing ${ingresses.length} ${planetName} ingresses starting from ${customDate ? 'the specified date' : 'now'}.`);
1093
+ } else {
1094
+ console.log(`\nShowing the next ${planetName} ingress from ${customDate ? 'the specified date' : 'now'}.`);
1095
+ }
1096
+
1097
+ // Handle AI request
1098
+ if (options.p) {
1099
+ const aiData = {
1100
+ planet: planetName,
1101
+ sign: 'Multiple',
1102
+ degreeInSign: 'Multiple',
1103
+ dignity: `Next ${planetName} ingress: ${ingresses[0].sign} at ${ingresses[0].date.day}.${ingresses[0].date.month}.${ingresses[0].date.year} ${ingresses[0].date.hour}:${ingresses[0].date.minute.toString().padStart(2, '0')}`,
1104
+ element: 'Multiple',
1105
+ decan: 'Multiple',
1106
+ planetIngresses: ingresses
1107
+ };
1108
+
1109
+ await handleAIRequest(options, aiData);
1110
+ }
1111
+
1112
+ return;
1113
+ }
1114
+
978
1115
  // For other options, a planet is required (except for --a or --s without planet)
979
1116
  if (!planetArg && !options.a && !options.s) {
980
1117
  console.error('Error: Planet is required for this operation.');
@@ -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();