klio 1.4.7 → 1.4.9

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,17 +86,21 @@ 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.
95
97
 
96
98
  - **Show house and sign distribution of the datetime column**: `klio [planet] --csv <file-path>`
97
99
  - **Show aspect type distribution between two planets:** `klio [planet1] [planet2] --csv <file-path> --a`
100
+ - **Filter CSV data by column value:** `klio [planet] --csv <file-path> --filter "column:value"` (e.g., `--filter "Item:coffee"`)
101
+ - **Creating a bar chart**: Create a bar chart and save the image to the downloads' folder. The image shows the aspect distribution of your csv datetime values: `klio moon sun --csv /home/user/Downloads/coffee.csv --filter "Item:cookie" --a --title "Eaten cookies during sun-moon aspects"`
98
102
 
99
- The command also returns a Chi-Square.
103
+ - The command also returns a Chi-Square.
100
104
 
101
105
  ### Adding different charts
102
106
 
@@ -107,7 +111,7 @@ The command also returns a Chi-Square.
107
111
  - `--people` - Lists all saved persons
108
112
  - `--delete-person <id>` - Deletes a person
109
113
 
110
- Then, instead using `--i` for the commands from above you can use `--wp <id>` i.e. `--wp john`
114
+ Then, instead using `--i` for the commands from above you can use `--wp <id>` or `--wp john`
111
115
 
112
116
  ### Advanced Features
113
117
 
@@ -117,11 +121,21 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
117
121
  - **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
122
  - **Transits**: `klio --tr` - Shows personal transits based on birth data. Combine with `--wp <id or --i>` and optional `--d <"DD.MM.YYYY HH:MM>`
119
123
  - **Transit Houses**: `klio --s --tra --i`
124
+
125
+
126
+
120
127
  ### Past and Future Aspects
121
128
 
122
129
  - **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
130
  - **Future aspects**: `klio --z <count> [planet1] [aspect-type] [planet2]` - Shows future aspects (Format: --z <count> planet1 aspectType planet2)
124
131
 
132
+ ### Reference
133
+ - **Transit frequency**: `[planet1] [aspect-type] [planet2] --o` - Calculates how often a transit occurs in the sky using astronomical synodic periods
134
+ - **Example**: `klio saturn c neptune --o` - Shows Saturn-Neptune conjunction frequency
135
+ - **Example**: `klio jupiter s pluto --o` - Shows Jupiter-Pluto square frequency
136
+ - **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
137
+ - **Output**: Shows frequency in readable format (e.g., "every 36 years") with approximate
138
+
125
139
  ### Planet Ingresses
126
140
 
127
141
  - **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 +146,7 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
132
146
  ### Wikidata Integration
133
147
 
134
148
  - **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
149
+ - **Available occupations for now**: authors, scientists, artists, musicians, politicians, actors, philosophers
136
150
  - **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
137
151
  - **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
152
  - **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.7",
3
+ "version": "1.4.9",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -1900,7 +1900,39 @@ function analyzeSignDistributionSignificance(signDistribution) {
1900
1900
  };
1901
1901
  }
1902
1902
 
1903
- function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null) {
1903
+ // Helper function to parse filter criteria
1904
+ function parseFilterCriteria(filterString) {
1905
+ if (!filterString) return null;
1906
+
1907
+ const parts = filterString.split(':');
1908
+ if (parts.length !== 2) {
1909
+ console.warn('Invalid filter format. Expected "column:value"');
1910
+ return null;
1911
+ }
1912
+
1913
+ return {
1914
+ column: parts[0].trim(),
1915
+ value: parts[1].trim()
1916
+ };
1917
+ }
1918
+
1919
+ // Helper function to apply filter to records
1920
+ function applyFilter(records, filterCriteria) {
1921
+ if (!filterCriteria || !records || records.length === 0) return records;
1922
+
1923
+ const parsedCriteria = typeof filterCriteria === 'string'
1924
+ ? parseFilterCriteria(filterCriteria)
1925
+ : filterCriteria;
1926
+
1927
+ if (!parsedCriteria) return records;
1928
+
1929
+ return records.filter(record => {
1930
+ const recordValue = record[parsedCriteria.column];
1931
+ return recordValue && recordValue.toString() === parsedCriteria.value;
1932
+ });
1933
+ }
1934
+
1935
+ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
1904
1936
  return new Promise(async (resolve, reject) => {
1905
1937
  const results = [];
1906
1938
  let pendingOperations = 0;
@@ -1967,8 +1999,11 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
1967
1999
  skip_empty_lines: true
1968
2000
  });
1969
2001
 
2002
+ // Apply filter if specified
2003
+ const filteredRecords = filterCriteria ? applyFilter(records, filterCriteria) : records;
2004
+
1970
2005
  // Process each row
1971
- for (const data of records) {
2006
+ for (const data of filteredRecords) {
1972
2007
  // Look for a column with ISO-Datetime values or Unix-Timestamps
1973
2008
  const datetimeColumns = Object.keys(data).filter(key => {
1974
2009
  const value = data[key];
@@ -2107,6 +2142,20 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2107
2142
  fs.createReadStream(filePath)
2108
2143
  .pipe(csvParser())
2109
2144
  .on('data', (data) => {
2145
+ // Apply filter if specified
2146
+ if (filterCriteria) {
2147
+ const parsedCriteria = typeof filterCriteria === 'string'
2148
+ ? parseFilterCriteria(filterCriteria)
2149
+ : filterCriteria;
2150
+
2151
+ if (parsedCriteria) {
2152
+ const recordValue = data[parsedCriteria.column];
2153
+ if (!recordValue || recordValue.toString() !== parsedCriteria.value) {
2154
+ return; // Skip this record
2155
+ }
2156
+ }
2157
+ }
2158
+
2110
2159
  // Look for a column with ISO-Datetime values or Unix-Timestamps
2111
2160
  const datetimeColumns = Object.keys(data).filter(key => {
2112
2161
  const value = data[key];
@@ -2780,5 +2829,91 @@ module.exports = {
2780
2829
  showPlanetComboAspects,
2781
2830
  calculateNextPlanetIngress,
2782
2831
  calculateAstrologicalAngles,
2783
- longitudeToSignDegree
2832
+ longitudeToSignDegree,
2833
+ calculateTransitFrequency
2784
2834
  };
2835
+
2836
+ // Function to calculate how often a transit occurs between two planets
2837
+ function calculateTransitFrequency(planet1, planet2, aspectType) {
2838
+ // Use astronomical synodic period calculation for more accurate results
2839
+ const orbitalPeriods = {
2840
+ 'sun': 1, // Not applicable for conjunctions, but included for completeness
2841
+ 'moon': 0.0748, // 27.3 days
2842
+ 'mercury': 0.2408, // 87.97 days
2843
+ 'venus': 0.6152, // 224.7 days
2844
+ 'mars': 1.8808, // 686.98 days
2845
+ 'jupiter': 11.862, // 11.862 years
2846
+ 'saturn': 29.457, // 29.457 years
2847
+ 'uranus': 84.016, // 84.016 years
2848
+ 'neptune': 164.79, // 164.79 years
2849
+ 'pluto': 248.09 // 248.09 years
2850
+ };
2851
+
2852
+ // Get orbital periods for the two planets
2853
+ const period1 = orbitalPeriods[planet1];
2854
+ const period2 = orbitalPeriods[planet2];
2855
+
2856
+ if (!period1 || !period2) {
2857
+ return {
2858
+ frequency: 'Cannot calculate frequency: unknown orbital periods',
2859
+ averageInterval: null,
2860
+ aspects: []
2861
+ };
2862
+ }
2863
+
2864
+ // Calculate synodic period using the formula: 1/S = |1/P1 - 1/P2|
2865
+ // For conjunctions, we use the absolute difference
2866
+ const synodicPeriod = 1 / Math.abs(1/period1 - 1/period2);
2867
+
2868
+ // Convert to days
2869
+ const averageIntervalDays = synodicPeriod * 365.25;
2870
+
2871
+ // Format the frequency in a more readable way
2872
+ const formattedFrequency = formatFrequency(averageIntervalDays);
2873
+
2874
+ // Also calculate some actual aspects for reference
2875
+ const futureAspects = getFutureAspects(planet1, planet2, aspectType, 5);
2876
+
2877
+ return {
2878
+ frequency: formattedFrequency,
2879
+ frequencyShort: `Approximately every ${synodicPeriod.toFixed(1)} years`,
2880
+ averageInterval: averageIntervalDays,
2881
+ averageIntervalYears: synodicPeriod,
2882
+ intervals: [], // Not applicable for this calculation method
2883
+ aspects: futureAspects,
2884
+ note: `Note: Frequency calculated using astronomical synodic period (1/S = |1/${period1} - 1/${period2}| years)`
2885
+ };
2886
+ }
2887
+
2888
+ // Helper function to format frequency in a readable way
2889
+ function formatFrequency(days) {
2890
+ if (days < 30) {
2891
+ return `Approximately every ${Math.round(days)} days`;
2892
+ } else if (days < 90) {
2893
+ const months = days / 30;
2894
+ return `Approximately every ${Math.round(months)} months`;
2895
+ } else if (days < 365) {
2896
+ const months = days / 30;
2897
+ return `Approximately every ${Math.round(months)} months`;
2898
+ } else if (days < 730) {
2899
+ const years = days / 365.25;
2900
+ const months = (years - Math.floor(years)) * 12;
2901
+ if (months >= 9) {
2902
+ return `Approximately every ${Math.floor(years) + 1} years`;
2903
+ } else if (months >= 1) {
2904
+ return `Approximately every ${Math.floor(years)} years, ${Math.round(months)} months`;
2905
+ } else {
2906
+ return `Approximately every ${Math.floor(years)} years`;
2907
+ }
2908
+ } else {
2909
+ const years = days / 365.25;
2910
+ const months = (years - Math.floor(years)) * 12;
2911
+ if (months >= 9) {
2912
+ return `Approximately every ${Math.floor(years) + 1} years`;
2913
+ } else if (months >= 1) {
2914
+ return `Approximately every ${Math.floor(years)} years, ${Math.round(months)} months`;
2915
+ } else {
2916
+ return `Approximately every ${Math.round(years)} years`;
2917
+ }
2918
+ }
2919
+ }
package/src/cli/cli.js CHANGED
@@ -1,16 +1,26 @@
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');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
 
14
+ // Import chart generation and downloads folder utilities
15
+ let chartGenerator = null;
16
+ let downloadsFolder = null;
17
+ try {
18
+ chartGenerator = require('../utils/chartGenerator');
19
+ downloadsFolder = require('../utils/downloadsFolder');
20
+ } catch (error) {
21
+ console.debug('Chart generation utilities not available');
22
+ }
23
+
14
24
  // Wikidata service import
15
25
  let wikidataService = null;
16
26
  try {
@@ -232,6 +242,8 @@ program
232
242
  .option('--sleep', 'Analyzes sleep patterns with moon aspects')
233
243
  .option('--steps', 'Analyzes step patterns with moon signs')
234
244
  .option('--stress', 'Analyzes stress patterns with moon aspects based on HRV')
245
+ .option('--night', 'Analyzes late night sleep patterns (after 2am) and common aspects')
246
+ .option('--night-all', 'Analyzes all-nighter sleep patterns (04:00 and 06:00 starts)')
235
247
  .option('--setup', 'Configures location and birth data for personalized calculations')
236
248
  .option('--ai <model>', 'Sets a specific AI model (e.g. "google/gemma-3n-e4b")')
237
249
  .option('--system <prompt>', 'Sets a custom system prompt for all AI requests')
@@ -263,10 +275,13 @@ program
263
275
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
264
276
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
265
277
  .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
278
+ .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
279
+ .option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
266
280
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
267
281
  .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
268
282
  .option('--gui', 'Launches the web interface for command history (port 37421)')
269
283
  .option('--gui-port <port>', 'Specify custom port for GUI server')
284
+ .option('--o', 'Shows how often this transit occurs in the sky (frequency calculation)')
270
285
  .description('Shows astrological data for a planet')
271
286
  .action(async (planetArg, planet2Arg, options) => {
272
287
  // If planet2Arg is an object, it contains the options (commander behavior)
@@ -1492,8 +1507,9 @@ program
1492
1507
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
1493
1508
  const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
1494
1509
  const analyzeAspects = actualOptions.a || false;
1510
+ const filterCriteria = actualOptions.filter;
1495
1511
 
1496
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
1512
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
1497
1513
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
1498
1514
  if (results.length === 0) {
1499
1515
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -1611,6 +1627,60 @@ program
1611
1627
  // Also show total number of records and house system
1612
1628
  console.log(`\nIn Total: ${results.length}`);
1613
1629
  console.log(`House system: ${houseSystem.charAt(0).toUpperCase() + houseSystem.slice(1)}`);
1630
+
1631
+ // Generate chart image if --title option is provided
1632
+ if (actualOptions.title && chartGenerator && downloadsFolder) {
1633
+ try {
1634
+ // Get the downloads folder path
1635
+ const downloadsPath = downloadsFolder.getValidDownloadsFolder();
1636
+
1637
+ // Create a safe filename from the title
1638
+ const safeTitle = actualOptions.title
1639
+ .replace(/[^a-zA-Z0-9\s\-_.]/g, '')
1640
+ .substring(0, 50)
1641
+ .trim();
1642
+
1643
+ const timestamp = new Date().toISOString()
1644
+ .replace(/[:.]/g, '-')
1645
+ .substring(0, 19);
1646
+
1647
+ const filename = `${timestamp}_${safeTitle}.png`;
1648
+ const outputPath = path.join(downloadsPath, filename);
1649
+
1650
+ console.log(`\n📊 Generating chart image: ${filename}`);
1651
+ console.log(`💾 Saving to: ${outputPath}`);
1652
+
1653
+ // Calculate statistics for the chart
1654
+ let chartStats = null;
1655
+ if (analyzeAspects && aspectStatistics) {
1656
+ const { analyzeAspectDistributionSignificance } = require('../astrology/astrologyService');
1657
+ chartStats = analyzeAspectDistributionSignificance(aspectStatistics);
1658
+ } else if (houseDistribution) {
1659
+ const { analyzeHouseDistributionSignificance } = require('../astrology/astrologyService');
1660
+ chartStats = analyzeHouseDistributionSignificance(houseDistribution);
1661
+ }
1662
+
1663
+ // Generate the chart
1664
+ const chartData = {
1665
+ houseDistribution: houseDistribution || { counts: {}, total: results.length },
1666
+ aspectStatistics: aspectStatistics || { types: [], counts: {}, total: 0 },
1667
+ results: results,
1668
+ statistics: chartStats
1669
+ };
1670
+
1671
+ return chartGenerator.generateBarChart(
1672
+ chartData,
1673
+ actualOptions.title,
1674
+ outputPath,
1675
+ analyzeAspects // Show aspects if --a option was used
1676
+ ).then(imagePath => {
1677
+ console.log(`✅ Chart image successfully saved to: ${imagePath}`);
1678
+ console.log(`📁 You can find it in your Downloads folder`);
1679
+ });
1680
+ } catch (error) {
1681
+ console.error('❌ Error generating chart image:', error.message);
1682
+ }
1683
+ }
1614
1684
  }
1615
1685
  })
1616
1686
  .catch(error => {
@@ -1946,6 +2016,44 @@ program
1946
2016
  };
1947
2017
  await handleAIRequest(options, healthDataSummary);
1948
2018
  }
2019
+ } else if (options.night) {
2020
+ console.log(`Apple Health late night sleep analysis with ${pLabel} aspects`);
2021
+ console.log('========================================================\n');
2022
+ await analyzeLateNightAspects(planet, healthData);
2023
+
2024
+ // Handle AI request for night analysis
2025
+ if (options.p) {
2026
+ const healthDataSummary = {
2027
+ planet: planet,
2028
+ sign: 'Multiple',
2029
+ degreeInSign: 'Multiple',
2030
+ dignity: `Apple Health late night sleep analysis with ${planet} aspects`,
2031
+ element: 'Multiple',
2032
+ decan: 'Multiple',
2033
+ healthAnalysisType: 'night',
2034
+ planetName: planet
2035
+ };
2036
+ await handleAIRequest(options, healthDataSummary);
2037
+ }
2038
+ } else if (options.nightAll) {
2039
+ console.log(`Apple Health all-nighter sleep analysis with ${pLabel} aspects`);
2040
+ console.log('============================================================\n');
2041
+ await analyzeAllNighterAspects(planet, healthData);
2042
+
2043
+ // Handle AI request for all-nighter analysis
2044
+ if (options.p) {
2045
+ const healthDataSummary = {
2046
+ planet: planet,
2047
+ sign: 'Multiple',
2048
+ degreeInSign: 'Multiple',
2049
+ dignity: `Apple Health all-nighter sleep analysis with ${planet} aspects`,
2050
+ element: 'Multiple',
2051
+ decan: 'Multiple',
2052
+ healthAnalysisType: 'night-all',
2053
+ planetName: planet
2054
+ };
2055
+ await handleAIRequest(options, healthDataSummary);
2056
+ }
1949
2057
  } else if (hypothesis === 'stress-moon') {
1950
2058
  console.log(`Apple Health stress analysis with ${pLabel} aspects`);
1951
2059
  console.log('==========================================\n');
@@ -2394,13 +2502,83 @@ program
2394
2502
  return;
2395
2503
  }
2396
2504
 
2505
+ // Show transit frequency if --o option is specified
2506
+ if (options.o) {
2507
+ // We expect a specific format: planet1 aspectType planet2 --o
2508
+ // Example: "saturn k neptun --o" for Saturn conjunction Neptune
2509
+
2510
+ // Parse planet and aspect information from positional arguments
2511
+ // For the format: planet1 aspectType planet2 --o
2512
+ // planetArg = planet1, planet2Arg = aspectType, and we need to get planet2 from excess arguments
2513
+ const planet1 = planetArg ? planetArg.toLowerCase() : null;
2514
+ const aspectType = planet2Arg ? planet2Arg.toLowerCase() : null;
2515
+
2516
+ // Get the third argument (planet2) from excess arguments
2517
+ const excessArgs = program.args;
2518
+ const planet2 = excessArgs.length >= 3 ? excessArgs[2].toLowerCase() : null;
2519
+
2520
+ // Check if we have all required arguments
2521
+ if (!planet1 || !aspectType || !planet2) {
2522
+ console.error('Error: Transit frequency requires two planets and an aspect type.');
2523
+ console.error('Format: planet1 aspectType planet2 --o');
2524
+ console.error('Example: saturn c neptune --o');
2525
+ console.error('Aspect types: c=conjunction, o=opposition, s=square, t=trine, se=sextile, q=quincunx');
2526
+ process.exit(1);
2527
+ }
2528
+
2529
+ // Check if planets are valid
2530
+ if (!planets[planet1] || !planets[planet2]) {
2531
+ console.error('Error: Invalid planets.');
2532
+ console.error('Available planets:', Object.keys(planets).join(', '));
2533
+ process.exit(1);
2534
+ }
2535
+
2536
+ // Check if aspect type is valid
2537
+ const targetAngle = getAspectAngle(aspectType);
2538
+ if (targetAngle === null) {
2539
+ console.error('Error: Invalid aspect type.');
2540
+ console.error('Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)');
2541
+ process.exit(1);
2542
+ }
2543
+
2544
+ // Calculate transit frequency
2545
+ const frequencyData = calculateTransitFrequency(planet1, planet2, aspectType);
2546
+
2547
+ // Show results
2548
+ const aspectTypeFull = getAspectTypeFullName(aspectType);
2549
+ console.log(`Frequency: ${frequencyData.frequency}`);
2550
+ console.log(`(Average interval: ${frequencyData.averageInterval.toFixed(1)} days / ${frequencyData.averageIntervalYears.toFixed(1)} years)`);
2551
+
2552
+ // Show note if available
2553
+ if (frequencyData.note) {
2554
+ console.log(`${frequencyData.note}`);
2555
+ }
2556
+
2557
+ // Handle AI request for transit frequency
2558
+ if (options.p) {
2559
+ const frequencyDataForAI = {
2560
+ planet: `${planet1}-${planet2}`,
2561
+ sign: 'Multiple',
2562
+ degreeInSign: 'Multiple',
2563
+ dignity: `Transit frequency: ${frequencyData.frequency} for ${aspectTypeFull} between ${planet1} and ${planet2}`,
2564
+ element: 'Multiple',
2565
+ decan: 'Multiple',
2566
+ frequencyData: frequencyData
2567
+ };
2568
+ await handleAIRequest(options, frequencyDataForAI);
2569
+ }
2570
+
2571
+ return;
2572
+ }
2573
+
2397
2574
  // Analyze CSV file if --csv option is provided
2398
2575
  if (actualOptions.csv) {
2399
2576
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
2400
2577
  const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
2401
2578
  const analyzeAspects = actualOptions.a || false;
2579
+ const filterCriteria = actualOptions.filter;
2402
2580
 
2403
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
2581
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
2404
2582
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
2405
2583
  if (results.length === 0) {
2406
2584
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -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
  };
@@ -0,0 +1,129 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Generate a bar chart SVG from CSV analysis data
6
+ * @param {Object} data - CSV analysis data
7
+ * @param {string} title - Chart title
8
+ * @param {string} outputPath - Output file path
9
+ * @param {boolean} showAspects - Whether to show aspects or house distribution
10
+ * @returns {Promise<string>} Path to the generated image
11
+ */
12
+ async function generateBarChart(data, title, outputPath, showAspects = false) {
13
+ // SVG dimensions
14
+ const width = 1200;
15
+ const height = 800;
16
+ const padding = 60;
17
+ const chartWidth = width - 2 * padding;
18
+ const chartHeight = height - 2 * padding;
19
+ const titleHeight = 80;
20
+ const legendHeight = 100;
21
+
22
+ // Colors
23
+ const barColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
24
+
25
+ // Prepare data for charting
26
+ let chartData, labels, maxValue, dataType;
27
+
28
+ if (showAspects && data.aspectStatistics) {
29
+ // Aspect statistics chart
30
+ dataType = 'Aspects';
31
+ chartData = data.aspectStatistics.types.map((type, index) => ({
32
+ label: type,
33
+ value: data.aspectStatistics.counts[type] || 0,
34
+ color: barColors[index % barColors.length]
35
+ }));
36
+ maxValue = Math.max(...chartData.map(d => d.value), 10);
37
+ } else {
38
+ // House distribution chart (default)
39
+ dataType = 'House Distribution';
40
+ chartData = [];
41
+
42
+ // Ensure houseDistribution exists and has counts
43
+ const houseDist = data.houseDistribution || { counts: {} };
44
+
45
+ for (let i = 1; i <= 12; i++) {
46
+ chartData.push({
47
+ label: `House ${i}`,
48
+ value: houseDist.counts[i] || 0,
49
+ color: barColors[(i-1) % barColors.length]
50
+ });
51
+ }
52
+
53
+ maxValue = Math.max(...chartData.map(d => d.value), 10);
54
+ }
55
+
56
+ // Calculate bar dimensions
57
+ const barCount = chartData.length;
58
+ const barWidth = chartWidth / barCount / 1.5;
59
+ const maxBarHeight = chartHeight - titleHeight - legendHeight;
60
+ const yScale = maxBarHeight / maxValue;
61
+
62
+ // Generate SVG
63
+ const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
64
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
65
+ <svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
66
+ <!-- Background -->
67
+ <rect width="100%" height="100%" fill="#f8f9fa"/>
68
+
69
+ <!-- Title -->
70
+ <text x="${width/2}" y="${padding + titleHeight/2 + 10}" font-family="Arial" font-size="24" font-weight="bold" text-anchor="middle" fill="#333333">${title}</text>
71
+
72
+ <!-- Statistical Subtitle -->
73
+ ${data.statistics ? `
74
+ <text x="${width/2}" y="${padding + titleHeight/2 + 40}" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle" fill="#666666">
75
+ χ²(${data.statistics.degreesOfFreedom}) = ${data.statistics.chiSquare}, p ${data.statistics.isSignificant ? '&lt; 0.05' : '≥ 0.05'}, Cramer's V = ${data.statistics.effectSize}
76
+ </text>
77
+ ` : ''}
78
+
79
+ <!-- Y-axis grid lines -->
80
+ ${Array.from({length: 6}).map((_, i) => {
81
+ const y = height - padding - (i * maxBarHeight / 5);
82
+ return `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" stroke="#e0e0e0" stroke-width="1"/>`;
83
+ }).join('')}
84
+
85
+ <!-- Y-axis labels -->
86
+ ${Array.from({length: 6}).map((_, i) => {
87
+ const y = height - padding - (i * maxBarHeight / 5);
88
+ const value = Math.round(maxValue * (i / 5));
89
+ return `<text x="${padding - 10}" y="${y + 4}" font-family="Arial" font-size="14" text-anchor="end" fill="#666666">${value}</text>`;
90
+ }).join('')}
91
+
92
+ <!-- X-axis -->
93
+ <line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="#666666" stroke-width="2"/>
94
+
95
+ <!-- Bars -->
96
+ ${chartData.map((d, index) => {
97
+ const barHeight = d.value * yScale;
98
+ const x = padding + (index * (barWidth * 1.5)) + barWidth / 2;
99
+ const y = height - padding - barHeight;
100
+
101
+ return `
102
+ <rect x="${x - barWidth / 2}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${d.color}"/>
103
+ ${barHeight > 20 ? `<text x="${x}" y="${y + 20}" font-family="Arial" font-size="14" font-weight="bold" text-anchor="middle" fill="white">${d.value}</text>` : ''}
104
+ `;
105
+ }).join('')}
106
+
107
+ <!-- X-axis labels -->
108
+ ${chartData.map((d, index) => {
109
+ const x = padding + (index * (barWidth * 1.5)) + barWidth / 2;
110
+ const y = height - padding + 25;
111
+ return `<text x="${x}" y="${y}" font-family="Arial" font-size="14" text-anchor="middle" fill="#666666">${d.label}</text>`;
112
+ }).join('')}
113
+ </svg>`;
114
+
115
+ // Save the SVG file
116
+ return new Promise((resolve, reject) => {
117
+ fs.writeFile(outputPath, svgContent, 'utf8', (error) => {
118
+ if (error) {
119
+ reject(error);
120
+ } else {
121
+ resolve(outputPath);
122
+ }
123
+ });
124
+ });
125
+ }
126
+
127
+ module.exports = {
128
+ generateBarChart
129
+ };
@@ -0,0 +1,70 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+
4
+ /**
5
+ * Gets the default downloads folder path for the current operating system
6
+ * @returns {string} Path to the downloads folder
7
+ */
8
+ function getDownloadsFolder() {
9
+ const homeDir = os.homedir();
10
+
11
+ switch (process.platform) {
12
+ case 'win32':
13
+ // Windows: %USERPROFILE%\Downloads
14
+ return path.join(homeDir, 'Downloads');
15
+
16
+ case 'darwin':
17
+ // macOS: ~/Downloads
18
+ return path.join(homeDir, 'Downloads');
19
+
20
+ case 'linux':
21
+ // Linux: Check common locations
22
+ const xdgDownload = process.env.XDG_DOWNLOAD_DIR;
23
+ if (xdgDownload) {
24
+ return xdgDownload;
25
+ }
26
+ // Fallback to ~/Downloads
27
+ return path.join(homeDir, 'Downloads');
28
+
29
+ default:
30
+ // Fallback for other platforms
31
+ return path.join(homeDir, 'Downloads');
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Ensures the downloads folder exists and is writable
37
+ * @returns {string} Valid downloads folder path
38
+ */
39
+ function getValidDownloadsFolder() {
40
+ const fs = require('fs');
41
+ const downloadsFolder = getDownloadsFolder();
42
+
43
+ try {
44
+ // Check if folder exists
45
+ if (!fs.existsSync(downloadsFolder)) {
46
+ // Try to create it
47
+ fs.mkdirSync(downloadsFolder, { recursive: true });
48
+ }
49
+
50
+ // Check if folder is writable
51
+ const testFile = path.join(downloadsFolder, '.write_test_' + Date.now());
52
+ try {
53
+ fs.writeFileSync(testFile, 'test');
54
+ fs.unlinkSync(testFile);
55
+ return downloadsFolder;
56
+ } catch (error) {
57
+ // Fallback to current directory if downloads folder is not writable
58
+ console.warn(`⚠️ Downloads folder not writable: ${downloadsFolder}, using current directory`);
59
+ return process.cwd();
60
+ }
61
+ } catch (error) {
62
+ console.warn(`⚠️ Could not access downloads folder: ${error.message}, using current directory`);
63
+ return process.cwd();
64
+ }
65
+ }
66
+
67
+ module.exports = {
68
+ getDownloadsFolder,
69
+ getValidDownloadsFolder
70
+ };