klio 1.4.2 → 1.4.4

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
@@ -90,8 +90,6 @@ It's possible to analyze Apple Health data. This happens on the device and no se
90
90
  - **Step analysis**: `klio [planet] --apple <filepath> --steps` - Analyzes step patterns of any planet
91
91
  - **Stress analysis**: `klio [planet] --apple <filepath> --stress` - Analyzes stress patterns of planet sign stress based on HRV
92
92
 
93
- > Maybe I'll add some more in the future.
94
-
95
93
  ### CSV Analysis
96
94
  It's possible to analyze a csv with a column of either ISO date time or unix time stamp.
97
95
 
@@ -100,9 +98,6 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
100
98
 
101
99
  The command also returns a Chi-Square.
102
100
 
103
- You can research a lot of fun things with this. You find a lot of free datasets on Kaggle.
104
-
105
-
106
101
  ### Adding different charts
107
102
 
108
103
  - **Use personal data**: `--i` - Uses birth data from setup for calculations
@@ -127,6 +122,13 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
127
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)
128
123
  - **Future aspects**: `klio --z <count> [planet1] [aspect-type] [planet2]` - Shows future aspects (Format: --z <count> planet1 aspectType planet2)
129
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
+
130
132
  ### AI Integration
131
133
 
132
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -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) - calculate based on planet and requested count
2548
+ // Mars: ~2 months per sign, so for multiple ingresses we need ~count * 60 days
2549
+ // Other fast planets (Moon, Mercury, Venus, Sun): 90 days is sufficient
2550
+ const fastPlanets = ['moon', 'mercury', 'venus', 'sun'];
2551
+ const isFastPlanet = fastPlanets.includes(planetName);
2552
+ const isMars = planetName === 'mars';
2553
+
2554
+ const maxDays = isVerySlowPlanet ? 365 * 90 :
2555
+ isMediumSlowPlanet ? 365 * 90 :
2556
+ isMars ? Math.min(365 * 5, 90 * count) : // Mars: up to 5 years or count * 3 months
2557
+ isFastPlanet ? 90 : // 30 days for fast planets (enough for multiple moon ingresses)
2558
+ 365 * 90; // Default for other planets
2559
+ const maxAttempts = maxDays * 24 / timeStepHours;
2560
+
2561
+ while (!foundIngress && attempts < maxAttempts) {
2562
+ attempts++;
2563
+ currentDate.setHours(currentDate.getHours() + timeStepHours);
2564
+
2565
+ const testPlanetData = getAstrologicalData(planetName, {
2566
+ year: currentDate.getFullYear(),
2567
+ month: currentDate.getMonth() + 1,
2568
+ day: currentDate.getDate(),
2569
+ hour: currentDate.getHours(),
2570
+ minute: currentDate.getMinutes()
2571
+ });
2572
+
2573
+ const testSignIndex = signs.indexOf(testPlanetData.sign);
2574
+
2575
+ if (testSignIndex === targetSignIndex) {
2576
+ // Found the ingress! Now refine to get more precise time
2577
+ ingressDate = findExactPlanetIngressTime(planetName, currentDate, targetSignIndex, timezoneOffsetMinutes);
2578
+ foundIngress = true;
2579
+ }
2580
+ }
2581
+
2582
+ if (foundIngress) {
2583
+ results.push({
2584
+ sign: signs[targetSignIndex],
2585
+ date: ingressDate,
2586
+ fromSign: signs[currentSignIndex]
2587
+ });
2588
+
2589
+ // Prepare for next ingress
2590
+ currentSignIndex = targetSignIndex;
2591
+ targetSignIndex = (currentSignIndex + 1) % 12;
2592
+ // Convert ingressDate object to Date object
2593
+ currentDate = new Date(ingressDate.year, ingressDate.month - 1, ingressDate.day, ingressDate.hour, ingressDate.minute);
2594
+ // For fast planets, start searching a few hours after the ingress to avoid missing the next one
2595
+ if (isFastPlanet) {
2596
+ currentDate.setHours(currentDate.getHours() + 6); // Start 6 hours after ingress
2597
+ }
2598
+ } else {
2599
+ const daysLimit = isVerySlowPlanet ? 365 * 90 :
2600
+ isMediumSlowPlanet ? 365 * 90 :
2601
+ isMars ? Math.min(365 * 5, 90 * count) :
2602
+ isFastPlanet ? 90 :
2603
+ 365 * 90;
2604
+ console.log(`Could not find ${planetName} ingress to ${signs[targetSignIndex]} within ${daysLimit} days`);
2605
+ if (isVerySlowPlanet) {
2606
+ console.log(`💡 Note: For very slow planets like ${planetName}, the ephemeris data may not extend far enough into the future.`);
2607
+ } else if (isMediumSlowPlanet) {
2608
+ console.log(`💡 Note: For medium-slow planets like ${planetName}, the ephemeris data may not extend far enough into the future.`);
2609
+ } else if (isMars) {
2610
+ console.log(`💡 Note: For Mars, the requested number of ingresses may be too high for the current search window.`);
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');
@@ -254,6 +254,7 @@ program
254
254
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
255
255
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
256
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')
257
258
  .option('--gui', 'Launches the web interface for command history (port 37421)')
258
259
  .option('--gui-port <port>', 'Specify custom port for GUI server')
259
260
  .description('Shows astrological data for a planet')
@@ -1012,6 +1013,105 @@ program
1012
1013
  return;
1013
1014
  }
1014
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
+
1015
1115
  // For other options, a planet is required (except for --a or --s without planet)
1016
1116
  if (!planetArg && !options.a && !options.s) {
1017
1117
  console.error('Error: Planet is required for this operation.');