klio 1.4.6 → 1.4.8

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
@@ -86,9 +86,11 @@ klio --gui --gui-port 8080
86
86
  ### Health Analysis
87
87
  It's possible to analyze Apple Health data. This happens on the device and no server is involved like everything from this CLI. It just parses the export XML.
88
88
 
89
- - **Apple Health Export**: `klio [planet] --apple <filepath> --sleep ` - Analyzes Apple Health Export XML file and shows the most frequent aspects during sleep deprivation (Top 10):
89
+ - **Sleep analysis**: `klio [planet] --apple <filepath> --sleep ` - Analyzes Apple Health Export XML file and shows the most frequent aspects during sleep deprivation (Top 10):
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
+ - **Late night analysis**: `klio [planet] --apple <filepath> --night` - Analyzes late night sleep patterns (after 2am) and shows the most frequent aspects during these times
93
+ - **All-nighter analysis**: `klio [planet] --apple <filepath> --night-all` - Analyzes all-nighter sleep patterns (starting at 04:00 or 06:00) and shows the most frequent aspects during these times
92
94
 
93
95
  ### CSV Analysis
94
96
  It's possible to analyze a csv with a column of either ISO date time or unix time stamp.
@@ -116,12 +118,22 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
116
118
  - **Element distribution**: `klio --el` - Shows element distribution of planets in a horizontal chart. combine with `--wp <id>` or `--i`
117
119
  - **Aspect figures**: `klio --af` - Shows active aspect figures like T-squares, Grand Trines, etc. Combine with `--wp <id>` or `--i` or `--d <"DD.MM.YYYY HH:MM>` or `--hs <house-system>`
118
120
  - **Transits**: `klio --tr` - Shows personal transits based on birth data. Combine with `--wp <id or --i>` and optional `--d <"DD.MM.YYYY HH:MM>`
121
+ - **Transit Houses**: `klio --s --tra --i`
122
+
123
+
119
124
 
120
125
  ### Past and Future Aspects
121
126
 
122
127
  - **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)
123
128
  - **Future aspects**: `klio --z <count> [planet1] [aspect-type] [planet2]` - Shows future aspects (Format: --z <count> planet1 aspectType planet2)
124
129
 
130
+ ### Reference
131
+ - **Transit frequency**: `[planet1] [aspect-type] [planet2] --o` - Calculates how often a transit occurs in the sky using astronomical synodic periods
132
+ - **Example**: `klio saturn c neptune --o` - Shows Saturn-Neptune conjunction frequency
133
+ - **Example**: `klio jupiter s pluto --o` - Shows Jupiter-Pluto square frequency
134
+ - **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
135
+ - **Output**: Shows frequency in readable format (e.g., "every 36 years") with approximate
136
+
125
137
  ### Planet Ingresses
126
138
 
127
139
  - **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.)
@@ -132,7 +144,7 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
132
144
  ### Wikidata Integration
133
145
 
134
146
  - **Search for people with specific aspects**: `[planet1] [aspect-type] [planet2] --wiki <occupation> [limit]` - Searches Wikidata for people with specific astrological aspects
135
- - **Available occupations**: authors, scientists, artists, musicians, politicians, actors, philosophers
147
+ - **Available occupations for now**: authors, scientists, artists, musicians, politicians, actors, philosophers
136
148
  - **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
137
149
  - **Example**: `klio moon c neptune --wiki authors 50` - Tries to find authors with Moon conjunct Neptune aspect with a limit of 50. Is faster but less common aspects are maybe not found with 50
138
150
  - **Example**: `klio saturn s pluto --wiki scientists 100` - Finds scientists with Saturn square Pluto aspect
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -2780,5 +2780,91 @@ module.exports = {
2780
2780
  showPlanetComboAspects,
2781
2781
  calculateNextPlanetIngress,
2782
2782
  calculateAstrologicalAngles,
2783
- longitudeToSignDegree
2783
+ longitudeToSignDegree,
2784
+ calculateTransitFrequency
2784
2785
  };
2786
+
2787
+ // Function to calculate how often a transit occurs between two planets
2788
+ function calculateTransitFrequency(planet1, planet2, aspectType) {
2789
+ // Use astronomical synodic period calculation for more accurate results
2790
+ const orbitalPeriods = {
2791
+ 'sun': 1, // Not applicable for conjunctions, but included for completeness
2792
+ 'moon': 0.0748, // 27.3 days
2793
+ 'mercury': 0.2408, // 87.97 days
2794
+ 'venus': 0.6152, // 224.7 days
2795
+ 'mars': 1.8808, // 686.98 days
2796
+ 'jupiter': 11.862, // 11.862 years
2797
+ 'saturn': 29.457, // 29.457 years
2798
+ 'uranus': 84.016, // 84.016 years
2799
+ 'neptune': 164.79, // 164.79 years
2800
+ 'pluto': 248.09 // 248.09 years
2801
+ };
2802
+
2803
+ // Get orbital periods for the two planets
2804
+ const period1 = orbitalPeriods[planet1];
2805
+ const period2 = orbitalPeriods[planet2];
2806
+
2807
+ if (!period1 || !period2) {
2808
+ return {
2809
+ frequency: 'Cannot calculate frequency: unknown orbital periods',
2810
+ averageInterval: null,
2811
+ aspects: []
2812
+ };
2813
+ }
2814
+
2815
+ // Calculate synodic period using the formula: 1/S = |1/P1 - 1/P2|
2816
+ // For conjunctions, we use the absolute difference
2817
+ const synodicPeriod = 1 / Math.abs(1/period1 - 1/period2);
2818
+
2819
+ // Convert to days
2820
+ const averageIntervalDays = synodicPeriod * 365.25;
2821
+
2822
+ // Format the frequency in a more readable way
2823
+ const formattedFrequency = formatFrequency(averageIntervalDays);
2824
+
2825
+ // Also calculate some actual aspects for reference
2826
+ const futureAspects = getFutureAspects(planet1, planet2, aspectType, 5);
2827
+
2828
+ return {
2829
+ frequency: formattedFrequency,
2830
+ frequencyShort: `Approximately every ${synodicPeriod.toFixed(1)} years`,
2831
+ averageInterval: averageIntervalDays,
2832
+ averageIntervalYears: synodicPeriod,
2833
+ intervals: [], // Not applicable for this calculation method
2834
+ aspects: futureAspects,
2835
+ note: `Note: Frequency calculated using astronomical synodic period (1/S = |1/${period1} - 1/${period2}| years)`
2836
+ };
2837
+ }
2838
+
2839
+ // Helper function to format frequency in a readable way
2840
+ function formatFrequency(days) {
2841
+ if (days < 30) {
2842
+ return `Approximately every ${Math.round(days)} days`;
2843
+ } else if (days < 90) {
2844
+ const months = days / 30;
2845
+ return `Approximately every ${Math.round(months)} months`;
2846
+ } else if (days < 365) {
2847
+ const months = days / 30;
2848
+ return `Approximately every ${Math.round(months)} months`;
2849
+ } else if (days < 730) {
2850
+ const years = days / 365.25;
2851
+ const months = (years - Math.floor(years)) * 12;
2852
+ if (months >= 9) {
2853
+ return `Approximately every ${Math.floor(years) + 1} years`;
2854
+ } else if (months >= 1) {
2855
+ return `Approximately every ${Math.floor(years)} years, ${Math.round(months)} months`;
2856
+ } else {
2857
+ return `Approximately every ${Math.floor(years)} years`;
2858
+ }
2859
+ } else {
2860
+ const years = days / 365.25;
2861
+ const months = (years - Math.floor(years)) * 12;
2862
+ if (months >= 9) {
2863
+ return `Approximately every ${Math.floor(years) + 1} years`;
2864
+ } else if (months >= 1) {
2865
+ return `Approximately every ${Math.floor(years)} years, ${Math.round(months)} months`;
2866
+ } else {
2867
+ return `Approximately every ${Math.round(years)} years`;
2868
+ }
2869
+ }
2870
+ }
package/src/cli/cli.js CHANGED
@@ -1,10 +1,10 @@
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, calculateNextPlanetIngress, calculateAstrologicalAngles, longitudeToSignDegree } = 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, calculateAstrologicalAngles, longitudeToSignDegree, calculateTransitFrequency } = 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
- const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep } = require('../health/healthAnalysis');
7
+ const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep, analyzeLateNightAspects, analyzeAllNighterAspects } = require('../health/healthAnalysis');
8
8
  const { analyzeHouseDistribution, filterFilesByHouse } = require('../health/fileAnalysis');
9
9
  const { getFileCreationDate, parseDateToComponents } = require('../utils/fileUtils');
10
10
  const swisseph = require('swisseph');
@@ -232,6 +232,8 @@ program
232
232
  .option('--sleep', 'Analyzes sleep patterns with moon aspects')
233
233
  .option('--steps', 'Analyzes step patterns with moon signs')
234
234
  .option('--stress', 'Analyzes stress patterns with moon aspects based on HRV')
235
+ .option('--night', 'Analyzes late night sleep patterns (after 2am) and common aspects')
236
+ .option('--night-all', 'Analyzes all-nighter sleep patterns (04:00 and 06:00 starts)')
235
237
  .option('--setup', 'Configures location and birth data for personalized calculations')
236
238
  .option('--ai <model>', 'Sets a specific AI model (e.g. "google/gemma-3n-e4b")')
237
239
  .option('--system <prompt>', 'Sets a custom system prompt for all AI requests')
@@ -257,7 +259,7 @@ program
257
259
  .option('--p <prompt>', 'Asks a question to the AI model (e.g. "Which careers suit this position?")')
258
260
  .option('--el', 'Shows the element distribution of planets in a horizontal chart')
259
261
  .option('--af', 'Shows active aspect figures like T-squares, Grand Trines, etc.')
260
- .option('--transite', 'Shows personal transits based on birth data')
262
+ .option('--tra', 'Shows personal transits based on birth data')
261
263
  .option('--transit-aspekte', 'Shows personal transit aspects (Transit → Radix)')
262
264
  .option('--tr', 'Shows personal transit aspects (Transit → Radix) (short form)')
263
265
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
@@ -267,6 +269,7 @@ program
267
269
  .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
268
270
  .option('--gui', 'Launches the web interface for command history (port 37421)')
269
271
  .option('--gui-port <port>', 'Specify custom port for GUI server')
272
+ .option('--o', 'Shows how often this transit occurs in the sky (frequency calculation)')
270
273
  .description('Shows astrological data for a planet')
271
274
  .action(async (planetArg, planet2Arg, options) => {
272
275
  // If planet2Arg is an object, it contains the options (commander behavior)
@@ -516,8 +519,8 @@ program
516
519
  return;
517
520
  }
518
521
 
519
- // Show personal transits if --transite option is specified (no planet required)
520
- if (options.transite) {
522
+ // Show personal transits if --tra option is specified (no planet required)
523
+ if (options.tra) {
521
524
  // Person data is required for transits
522
525
  const personDataToUse = getPersonDataToUse(options);
523
526
  if (!personDataToUse.data) {
@@ -604,10 +607,7 @@ program
604
607
  }
605
608
 
606
609
  console.log('================================================================================');
607
- console.log('\nNote: The houses are based on your birth ASC (House System: Koch).');
608
- console.log('Example: If Jupiter is in Cancer and you have Scorpio ASC, this shows in which');
609
- console.log('of your birth houses the transit Jupiter is located.');
610
-
610
+
611
611
  // Send question to AI model if --p option is specified
612
612
  if (options.p) {
613
613
  // Create a data object for AI with transit information
@@ -1949,6 +1949,44 @@ program
1949
1949
  };
1950
1950
  await handleAIRequest(options, healthDataSummary);
1951
1951
  }
1952
+ } else if (options.night) {
1953
+ console.log(`Apple Health late night sleep analysis with ${pLabel} aspects`);
1954
+ console.log('========================================================\n');
1955
+ await analyzeLateNightAspects(planet, healthData);
1956
+
1957
+ // Handle AI request for night analysis
1958
+ if (options.p) {
1959
+ const healthDataSummary = {
1960
+ planet: planet,
1961
+ sign: 'Multiple',
1962
+ degreeInSign: 'Multiple',
1963
+ dignity: `Apple Health late night sleep analysis with ${planet} aspects`,
1964
+ element: 'Multiple',
1965
+ decan: 'Multiple',
1966
+ healthAnalysisType: 'night',
1967
+ planetName: planet
1968
+ };
1969
+ await handleAIRequest(options, healthDataSummary);
1970
+ }
1971
+ } else if (options.nightAll) {
1972
+ console.log(`Apple Health all-nighter sleep analysis with ${pLabel} aspects`);
1973
+ console.log('============================================================\n');
1974
+ await analyzeAllNighterAspects(planet, healthData);
1975
+
1976
+ // Handle AI request for all-nighter analysis
1977
+ if (options.p) {
1978
+ const healthDataSummary = {
1979
+ planet: planet,
1980
+ sign: 'Multiple',
1981
+ degreeInSign: 'Multiple',
1982
+ dignity: `Apple Health all-nighter sleep analysis with ${planet} aspects`,
1983
+ element: 'Multiple',
1984
+ decan: 'Multiple',
1985
+ healthAnalysisType: 'night-all',
1986
+ planetName: planet
1987
+ };
1988
+ await handleAIRequest(options, healthDataSummary);
1989
+ }
1952
1990
  } else if (hypothesis === 'stress-moon') {
1953
1991
  console.log(`Apple Health stress analysis with ${pLabel} aspects`);
1954
1992
  console.log('==========================================\n');
@@ -2397,6 +2435,75 @@ program
2397
2435
  return;
2398
2436
  }
2399
2437
 
2438
+ // Show transit frequency if --o option is specified
2439
+ if (options.o) {
2440
+ // We expect a specific format: planet1 aspectType planet2 --o
2441
+ // Example: "saturn k neptun --o" for Saturn conjunction Neptune
2442
+
2443
+ // Parse planet and aspect information from positional arguments
2444
+ // For the format: planet1 aspectType planet2 --o
2445
+ // planetArg = planet1, planet2Arg = aspectType, and we need to get planet2 from excess arguments
2446
+ const planet1 = planetArg ? planetArg.toLowerCase() : null;
2447
+ const aspectType = planet2Arg ? planet2Arg.toLowerCase() : null;
2448
+
2449
+ // Get the third argument (planet2) from excess arguments
2450
+ const excessArgs = program.args;
2451
+ const planet2 = excessArgs.length >= 3 ? excessArgs[2].toLowerCase() : null;
2452
+
2453
+ // Check if we have all required arguments
2454
+ if (!planet1 || !aspectType || !planet2) {
2455
+ console.error('Error: Transit frequency requires two planets and an aspect type.');
2456
+ console.error('Format: planet1 aspectType planet2 --o');
2457
+ console.error('Example: saturn c neptune --o');
2458
+ console.error('Aspect types: c=conjunction, o=opposition, s=square, t=trine, se=sextile, q=quincunx');
2459
+ process.exit(1);
2460
+ }
2461
+
2462
+ // Check if planets are valid
2463
+ if (!planets[planet1] || !planets[planet2]) {
2464
+ console.error('Error: Invalid planets.');
2465
+ console.error('Available planets:', Object.keys(planets).join(', '));
2466
+ process.exit(1);
2467
+ }
2468
+
2469
+ // Check if aspect type is valid
2470
+ const targetAngle = getAspectAngle(aspectType);
2471
+ if (targetAngle === null) {
2472
+ console.error('Error: Invalid aspect type.');
2473
+ console.error('Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)');
2474
+ process.exit(1);
2475
+ }
2476
+
2477
+ // Calculate transit frequency
2478
+ const frequencyData = calculateTransitFrequency(planet1, planet2, aspectType);
2479
+
2480
+ // Show results
2481
+ const aspectTypeFull = getAspectTypeFullName(aspectType);
2482
+ console.log(`Frequency: ${frequencyData.frequency}`);
2483
+ console.log(`(Average interval: ${frequencyData.averageInterval.toFixed(1)} days / ${frequencyData.averageIntervalYears.toFixed(1)} years)`);
2484
+
2485
+ // Show note if available
2486
+ if (frequencyData.note) {
2487
+ console.log(`${frequencyData.note}`);
2488
+ }
2489
+
2490
+ // Handle AI request for transit frequency
2491
+ if (options.p) {
2492
+ const frequencyDataForAI = {
2493
+ planet: `${planet1}-${planet2}`,
2494
+ sign: 'Multiple',
2495
+ degreeInSign: 'Multiple',
2496
+ dignity: `Transit frequency: ${frequencyData.frequency} for ${aspectTypeFull} between ${planet1} and ${planet2}`,
2497
+ element: 'Multiple',
2498
+ decan: 'Multiple',
2499
+ frequencyData: frequencyData
2500
+ };
2501
+ await handleAIRequest(options, frequencyDataForAI);
2502
+ }
2503
+
2504
+ return;
2505
+ }
2506
+
2400
2507
  // Analyze CSV file if --csv option is provided
2401
2508
  if (actualOptions.csv) {
2402
2509
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
@@ -1,6 +1,219 @@
1
1
  const { getAstrologicalData, calculatePlanetAspects } = require('../astrology/astrologyService');
2
2
  const { extractStepData, extractHRVData, extractSleepData, extractSleepGoal } = require('./healthService');
3
3
 
4
+ // Function to analyze all-nighter sleep patterns (04:00 and 06:00 starts)
5
+ async function analyzeAllNighterAspects(planetName, healthData) {
6
+ const sleepRecords = extractSleepData(healthData);
7
+
8
+ if (sleepRecords.length === 0) {
9
+ console.log('No sleep data found to analyze.');
10
+ return;
11
+ }
12
+
13
+ // Filter sleep periods that start at typical all-nighter hours (04:00 and 06:00)
14
+ const allNighterHours = [4, 6];
15
+ const allNighterSleepRecords = sleepRecords.filter(record => {
16
+ const startHour = record.startDate.getHours();
17
+ return allNighterHours.includes(startHour);
18
+ });
19
+
20
+ console.log(`All-nighter sleep periods (starting at 04:00 or 06:00): ${allNighterSleepRecords.length} of ${sleepRecords.length} total sleep periods`);
21
+
22
+ if (allNighterSleepRecords.length === 0) {
23
+ console.log('No all-nighter sleep periods found (starting at 04:00 or 06:00).');
24
+ return;
25
+ }
26
+
27
+ // Analyze planet aspects for each all-nighter sleep period
28
+ const aspectStats = {};
29
+
30
+ for (const night of allNighterSleepRecords) {
31
+ const midSleepTime = new Date(night.startDate.getTime() +
32
+ (night.endDate.getTime() - night.startDate.getTime()) / 2);
33
+ const dateComponents = {
34
+ year: midSleepTime.getFullYear(),
35
+ month: midSleepTime.getMonth() + 1,
36
+ day: midSleepTime.getDate(),
37
+ hour: midSleepTime.getHours(),
38
+ minute: midSleepTime.getMinutes()
39
+ };
40
+ const planetAspects = calculatePlanetAspects(planetName, dateComponents);
41
+
42
+ // Count aspects
43
+ for (const aspect of planetAspects) {
44
+ const aspectKey = `${aspect.type} with ${aspect.planet}`;
45
+ aspectStats[aspectKey] = (aspectStats[aspectKey] || 0) + 1;
46
+ }
47
+ }
48
+
49
+ // Show statistics (Top 10 planet aspects)
50
+ console.log(`\nMost frequent ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects during all-nighters (Top 10):`);
51
+ console.log('Aspect | Frequency | Percent | Active today');
52
+ console.log('-------|-----------|--------|------------');
53
+
54
+ const totalAllNighters = allNighterSleepRecords.length;
55
+ const sortedAspects = Object.entries(aspectStats)
56
+ .sort((a, b) => b[1] - a[1])
57
+ .slice(0, 10); // Limit to Top 10
58
+
59
+ // Calculate current planet aspects for comparison
60
+ const currentDate = new Date();
61
+ const currentDateComponents = {
62
+ year: currentDate.getFullYear(),
63
+ month: currentDate.getMonth() + 1,
64
+ day: currentDate.getDate(),
65
+ hour: currentDate.getHours(),
66
+ minute: currentDate.getMinutes()
67
+ };
68
+ const currentPlanetAspects = calculatePlanetAspects(planetName, currentDateComponents);
69
+ const currentAspectSet = new Set(currentPlanetAspects.map(a => `${a.type} with ${a.planet}`));
70
+
71
+ for (const [aspect, count] of sortedAspects) {
72
+ const percentage = totalAllNighters > 0 ? ((count / totalAllNighters) * 100).toFixed(1) : 0;
73
+ const isActiveToday = currentAspectSet.has(aspect) ? '✓' : '✗';
74
+ console.log(`${aspect.padEnd(25)} | ${count.toString().padStart(2)} | ${percentage}% | ${isActiveToday}`);
75
+ }
76
+
77
+ if (sortedAspects.length === 0) {
78
+ console.log(`No ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects found in all-nighter sleep periods.`);
79
+ }
80
+
81
+ // Show all-nighter statistics
82
+ console.log(`\nAll-nighter sleep statistics:`);
83
+ const totalAllNighterDuration = allNighterSleepRecords.reduce((sum, record) => sum + record.duration, 0);
84
+ const avgAllNighterDuration = totalAllNighterDuration / allNighterSleepRecords.length;
85
+
86
+ console.log(`Average all-nighter sleep duration: ${avgAllNighterDuration.toFixed(2)} hours`);
87
+ console.log(`Total all-nighter sleep periods: ${allNighterSleepRecords.length}`);
88
+
89
+ // Show breakdown by all-nighter start hours
90
+ const hourCounts = {};
91
+ allNighterSleepRecords.forEach(record => {
92
+ const hour = record.startDate.getHours();
93
+ hourCounts[hour] = (hourCounts[hour] || 0) + 1;
94
+ });
95
+
96
+ console.log(`All-nighter start hour breakdown:`);
97
+ allNighterHours.forEach(hour => {
98
+ if (hourCounts[hour]) {
99
+ console.log(` ${hour}:00 - ${hourCounts[hour]} times`);
100
+ }
101
+ });
102
+ }
103
+
104
+ // Function to analyze late night sleep patterns (after 2am) and common aspects
105
+ async function analyzeLateNightAspects(planetName, healthData) {
106
+ const sleepRecords = extractSleepData(healthData);
107
+
108
+ if (sleepRecords.length === 0) {
109
+ console.log('No sleep data found to analyze.');
110
+ return;
111
+ }
112
+
113
+ // Filter sleep periods that start after 2am (went to bed later than 2am)
114
+ const lateNightSleepRecords = sleepRecords.filter(record => {
115
+ const startHour = record.startDate.getHours();
116
+ return startHour >= 2; // Started after 2am
117
+ });
118
+
119
+ console.log(`Late night sleep periods (starting after 2am): ${lateNightSleepRecords.length} of ${sleepRecords.length} total sleep periods`);
120
+
121
+ if (lateNightSleepRecords.length === 0) {
122
+ console.log('No late night sleep periods found (starting after 2am).');
123
+ return;
124
+ }
125
+
126
+ // Analyze planet aspects for each late night sleep period
127
+ const aspectStats = {};
128
+
129
+ for (const night of lateNightSleepRecords) {
130
+ const midSleepTime = new Date(night.startDate.getTime() +
131
+ (night.endDate.getTime() - night.startDate.getTime()) / 2);
132
+ const dateComponents = {
133
+ year: midSleepTime.getFullYear(),
134
+ month: midSleepTime.getMonth() + 1,
135
+ day: midSleepTime.getDate(),
136
+ hour: midSleepTime.getHours(),
137
+ minute: midSleepTime.getMinutes()
138
+ };
139
+ const planetAspects = calculatePlanetAspects(planetName, dateComponents);
140
+
141
+ // Count aspects
142
+ for (const aspect of planetAspects) {
143
+ const aspectKey = `${aspect.type} with ${aspect.planet}`;
144
+ aspectStats[aspectKey] = (aspectStats[aspectKey] || 0) + 1;
145
+ }
146
+ }
147
+
148
+ // Show statistics (Top 10 planet aspects)
149
+ console.log(`\nMost frequent ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects during late night sleep (Top 10):`);
150
+ console.log('Aspect | Frequency | Percent | Active today');
151
+ console.log('-------|-----------|--------|------------');
152
+
153
+ const totalLateNights = lateNightSleepRecords.length;
154
+ const sortedAspects = Object.entries(aspectStats)
155
+ .sort((a, b) => b[1] - a[1])
156
+ .slice(0, 10); // Limit to Top 10
157
+
158
+ // Calculate current planet aspects for comparison
159
+ const currentDate = new Date();
160
+ const currentDateComponents = {
161
+ year: currentDate.getFullYear(),
162
+ month: currentDate.getMonth() + 1,
163
+ day: currentDate.getDate(),
164
+ hour: currentDate.getHours(),
165
+ minute: currentDate.getMinutes()
166
+ };
167
+ const currentPlanetAspects = calculatePlanetAspects(planetName, currentDateComponents);
168
+ const currentAspectSet = new Set(currentPlanetAspects.map(a => `${a.type} with ${a.planet}`));
169
+
170
+ for (const [aspect, count] of sortedAspects) {
171
+ const percentage = totalLateNights > 0 ? ((count / totalLateNights) * 100).toFixed(1) : 0;
172
+ const isActiveToday = currentAspectSet.has(aspect) ? '✓' : '✗';
173
+ console.log(`${aspect.padEnd(25)} | ${count.toString().padStart(2)} | ${percentage}% | ${isActiveToday}`);
174
+ }
175
+
176
+ if (sortedAspects.length === 0) {
177
+ console.log(`No ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects found in late night sleep periods.`);
178
+ }
179
+
180
+ // Show some statistics about the late night sleep patterns
181
+ console.log(`\nLate night sleep statistics:`);
182
+ const totalLateNightDuration = lateNightSleepRecords.reduce((sum, record) => sum + record.duration, 0);
183
+ const avgLateNightDuration = totalLateNightDuration / lateNightSleepRecords.length;
184
+
185
+ console.log(`Average late night sleep duration: ${avgLateNightDuration.toFixed(2)} hours`);
186
+ console.log(`Total late night sleep periods: ${lateNightSleepRecords.length}`);
187
+
188
+ // Show the most common late night sleep start times
189
+ const startHours = lateNightSleepRecords.map(record => record.startDate.getHours());
190
+ const hourCounts = {};
191
+ startHours.forEach(hour => {
192
+ hourCounts[hour] = (hourCounts[hour] || 0) + 1;
193
+ });
194
+
195
+ const sortedHours = Object.entries(hourCounts).sort((a, b) => b[1] - a[1]);
196
+ console.log(`Most common late night sleep start hours:`);
197
+
198
+ // Show top 3 most common hours
199
+ sortedHours.slice(0, 3).forEach(([hour, count]) => {
200
+ console.log(` ${hour}:00 - ${count} times`);
201
+ });
202
+
203
+ // Check for all-nighter patterns (04:00 and 06:00)
204
+ const allNighterHours = [4, 6];
205
+ const allNighterFound = allNighterHours.some(hour => hourCounts[hour] && hourCounts[hour] > 0);
206
+
207
+ if (allNighterFound) {
208
+ console.log(`\nAll-nighter patterns detected:`);
209
+ allNighterHours.forEach(hour => {
210
+ if (hourCounts[hour] && hourCounts[hour] > 0) {
211
+ console.log(` ${hour}:00 - ${hourCounts[hour]} times (potential all-nighter)`);
212
+ }
213
+ });
214
+ }
215
+ }
216
+
4
217
  // Function to analyze steps by planet sign
5
218
  async function analyzeStepsByPlanetSign(planetName, healthData, timeLimitDays = null) {
6
219
  let stepRecords = extractStepData(healthData);
@@ -421,5 +634,7 @@ async function analyzePlanetAspectsForSleep(planetName, healthData, sleepGoal =
421
634
  module.exports = {
422
635
  analyzeStepsByPlanetSign,
423
636
  analyzeStressByPlanetAspects,
424
- analyzePlanetAspectsForSleep
637
+ analyzePlanetAspectsForSleep,
638
+ analyzeLateNightAspects,
639
+ analyzeAllNighterAspects
425
640
  };