klio 1.4.4 → 1.4.5

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
@@ -129,6 +129,15 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
129
129
  - Example: `klio saturn --in 2` - Shows next 2 Saturn ingresses (note: slow planets may have limited future data)
130
130
  - Example: `klio mercury --in --d "15.03.2026"` - Shows next Mercury ingress from a specific date
131
131
 
132
+ ### Wikidata Integration
133
+
134
+ - **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
136
+ - **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
137
+ - **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
+ - **Example**: `klio saturn s pluto --wiki scientists 100` - Finds scientists with Saturn square Pluto aspect
139
+ - **Example**: `klio sun t moon --wiki artists 200` - Finds artists with Sun trine Moon aspect
140
+
132
141
  ### AI Integration
133
142
 
134
143
  - **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.4",
3
+ "version": "1.4.5",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
package/src/cli/cli.js CHANGED
@@ -11,6 +11,15 @@ const swisseph = require('swisseph');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
 
14
+ // Wikidata service import
15
+ let wikidataService = null;
16
+ try {
17
+ wikidataService = require('../wikidata/wikidataService');
18
+ } catch (error) {
19
+ // Wikidata service not available
20
+ console.debug('Wikidata service not available');
21
+ }
22
+
14
23
  // GUI Server import
15
24
  let guiServer = null;
16
25
  try {
@@ -255,6 +264,7 @@ program
255
264
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
256
265
  .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
257
266
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
267
+ .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
258
268
  .option('--gui', 'Launches the web interface for command history (port 37421)')
259
269
  .option('--gui-port <port>', 'Specify custom port for GUI server')
260
270
  .description('Shows astrological data for a planet')
@@ -1112,6 +1122,111 @@ program
1112
1122
  return;
1113
1123
  }
1114
1124
 
1125
+ // Handle Wikidata people search if --wiki option is specified
1126
+ if (options.wiki) {
1127
+ // For the wiki command, the format is: planet1 aspectType planet2 --wiki <occupation> [limit]
1128
+ // But the argument parsing is: planetArg=planet1, planet2=aspectType, and options.wiki=planet2
1129
+ // We need to extract the occupation and planet2 from the remaining arguments
1130
+
1131
+ if (!planetArg || !planet2 || !options.wiki) {
1132
+ console.error('Error: Wikidata search requires format: planet1 aspectType planet2 --wiki <occupation> [limit]');
1133
+ console.error('Example: klio saturn conjunction neptune --wiki authors');
1134
+ console.error('Available occupations:', Object.keys(wikidataService.PREDEFINED_OCCUPATIONS || {}).join(', '));
1135
+ process.exit(1);
1136
+ }
1137
+
1138
+ const planet1 = planetArg.toLowerCase();
1139
+ const aspectType = planet2.toLowerCase();
1140
+
1141
+ // The occupation is in options.wiki, but we need to find the actual planet2
1142
+ // Let's look at the original arguments to parse this correctly
1143
+ const args = process.argv.slice(2); // Skip 'node' and script name
1144
+
1145
+ // Find the position of --wiki
1146
+ const wikiIndex = args.findIndex(arg => arg === '--wiki');
1147
+ if (wikiIndex === -1 || wikiIndex + 1 >= args.length) {
1148
+ console.error('Error: Missing occupation after --wiki flag');
1149
+ process.exit(1);
1150
+ }
1151
+
1152
+ const occupation = args[wikiIndex + 1].toLowerCase();
1153
+
1154
+ // The planet2 should be the argument before --wiki
1155
+ const planet2Name = args[wikiIndex - 1].toLowerCase();
1156
+
1157
+ // Validate planets
1158
+ if (planets[planet1] === undefined) {
1159
+ console.error(`Error: Invalid planet '${planet1}'. Available planets: ${Object.keys(planets).join(', ')}`);
1160
+ process.exit(1);
1161
+ }
1162
+
1163
+ if (planets[planet2Name] === undefined) {
1164
+ console.error(`Error: Invalid planet '${planet2Name}'. Available planets: ${Object.keys(planets).join(', ')}`);
1165
+ process.exit(1);
1166
+ }
1167
+
1168
+ // Validate aspect type (including shorthands)
1169
+ const validAspects = ['conjunction', 'opposition', 'square', 'trine', 'sextile', 'quincunx', 'c', 'con', 'opp', 'sq', 'tri', 'sex', 'qui'];
1170
+ if (!validAspects.includes(aspectType)) {
1171
+ console.error(`Error: Invalid aspect type '${aspectType}'. Available aspects: ${['conjunction', 'opposition', 'square', 'trine', 'sextile', 'quincunx'].join(', ')}`);
1172
+ console.error('Shorthands: c/con, opp, sq, tri, sex, qui');
1173
+ process.exit(1);
1174
+ }
1175
+
1176
+ // Parse limit if provided (look for arguments after occupation)
1177
+ let limit = 500;
1178
+ if (wikiIndex + 2 < args.length) {
1179
+ const limitArg = args[wikiIndex + 2];
1180
+ if (!isNaN(limitArg) && parseInt(limitArg) > 0) {
1181
+ limit = parseInt(limitArg);
1182
+ }
1183
+ }
1184
+
1185
+ console.log(`🔍 Searching Wikidata for ${occupation} with ${planet1} ${aspectType} ${planet2Name} aspect...`);
1186
+ console.log(`📊 Limit: ${limit} people`);
1187
+
1188
+ if (!wikidataService) {
1189
+ console.error('❌ Wikidata service is not available.');
1190
+ process.exit(1);
1191
+ }
1192
+
1193
+ try {
1194
+ const results = await wikidataService.findPeopleWithAspect(occupation, planet1, aspectType, planet2Name, limit);
1195
+
1196
+ if (results.length === 0) {
1197
+ console.log('No results found.');
1198
+ return;
1199
+ }
1200
+
1201
+ // Display results in a table
1202
+ console.log('\n📋 Results (sorted by orb, closest aspects first):');
1203
+ console.log('================================================================================');
1204
+ console.log('| # | Name | Birth Date | Aspect Type | Orb | Wikidata Link |');
1205
+ console.log('================================================================================');
1206
+
1207
+ results.forEach((result, index) => {
1208
+ const name = result.name.padEnd(28, ' ').substring(0, 28);
1209
+ const birthDate = result.birthDate ? result.birthDate.substring(0, 10) : 'Unknown';
1210
+ const aspectTypeDisplay = result.aspect.type.padEnd(11, ' ');
1211
+ const orb = result.aspect.orb.padEnd(4, ' ');
1212
+ const linkUrl = result.linkUrl || 'N/A';
1213
+
1214
+ console.log(`| ${(index + 1).toString().padEnd(2)} | ${name} | ${birthDate} | ${aspectTypeDisplay} | ${orb}° | ${linkUrl} |`);
1215
+ });
1216
+
1217
+ console.log('================================================================================');
1218
+ console.log(`\n📊 Found ${results.length} ${occupation} with ${planet1} ${aspectType} ${planet2Name} aspect.`);
1219
+ console.log('🔗 Wikipedia search links are provided for each person.');
1220
+ console.log('🎯 Results are sorted by orb (closest aspects first).');
1221
+
1222
+ } catch (error) {
1223
+ console.error('❌ Error searching Wikidata:', error.message);
1224
+ process.exit(1);
1225
+ }
1226
+
1227
+ return;
1228
+ }
1229
+
1115
1230
  // For other options, a planet is required (except for --a or --s without planet)
1116
1231
  if (!planetArg && !options.a && !options.s) {
1117
1232
  console.error('Error: Planet is required for this operation.');
@@ -0,0 +1,228 @@
1
+ // Wikidata Service for fetching author data and checking astrological aspects
2
+ const axios = require('axios');
3
+ const { calculatePlanetComboAspects } = require('../astrology/astrologyService');
4
+
5
+ // Wikidata SPARQL endpoint
6
+ const WIKIDATA_SPARQL_ENDPOINT = 'https://query.wikidata.org/sparql';
7
+
8
+ // Predefined occupations with their Wikidata Q IDs
9
+ const PREDEFINED_OCCUPATIONS = {
10
+ 'authors': 'wd:Q36180', // Writer
11
+ 'scientists': 'wd:Q901', // Scientist
12
+ 'artists': 'wd:Q1028181', // Visual artist
13
+ 'musicians': 'wd:Q639669', // Musician
14
+ 'politicians': 'wd:Q82955', // Politician
15
+ 'actors': 'wd:Q33999', // Actor
16
+ 'philosophers': 'wd:Q4964182' // Philosopher
17
+ };
18
+
19
+ // Function to fetch people by occupation from Wikidata
20
+ async function fetchPeopleByOccupation(occupation, limit = 500) {
21
+ try {
22
+ // Get the Wikidata Q ID for the occupation
23
+ const occupationQid = PREDEFINED_OCCUPATIONS[occupation.toLowerCase()];
24
+
25
+ if (!occupationQid) {
26
+ console.error(`Error: Unknown occupation '${occupation}'. Available occupations: ${Object.keys(PREDEFINED_OCCUPATIONS).join(', ')}`);
27
+ return [];
28
+ }
29
+
30
+ const query = `
31
+ SELECT DISTINCT ?person ?personLabel ?birthDate ?wikipediaLink WHERE {
32
+ ?person wdt:P31 wd:Q5; # Instance of human
33
+ wdt:P106 ${occupationQid}. # Specific occupation
34
+
35
+ # Make birth date optional
36
+ OPTIONAL { ?person wdt:P569 ?birthDate. }
37
+
38
+ # Get English Wikipedia article link
39
+ OPTIONAL { ?wikipediaArticle schema:about ?person ;
40
+ schema:isPartOf <https://en.wikipedia.org/> ;
41
+ schema:url ?wikipediaLink . }
42
+
43
+ # Get label
44
+ SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
45
+ }
46
+ LIMIT ${limit}
47
+ `;
48
+
49
+ const response = await axios.get(WIKIDATA_SPARQL_ENDPOINT, {
50
+ params: {
51
+ query: query,
52
+ format: 'json'
53
+ },
54
+ headers: {
55
+ 'Accept': 'application/sparql-results+json'
56
+ }
57
+ });
58
+
59
+ if (response.data && response.data.results && response.data.results.bindings) {
60
+ return response.data.results.bindings.map(binding => ({
61
+ name: binding.personLabel?.value || 'Unknown',
62
+ birthDate: binding.birthDate?.value || null,
63
+ wikidataId: binding.person.value.split('/').pop()
64
+ }));
65
+ }
66
+
67
+ return [];
68
+ } catch (error) {
69
+ console.error('Error fetching people from Wikidata:', error.message);
70
+ return [];
71
+ }
72
+ }
73
+
74
+ // Backward compatibility function
75
+ async function fetchAuthorsFromWikidata(limit = 500) {
76
+ return fetchPeopleByOccupation('authors', limit);
77
+ }
78
+
79
+ // Function to parse birth date and extract astrological data
80
+ function parseBirthDate(birthDateString) {
81
+ if (!birthDateString) return null;
82
+
83
+ try {
84
+ // Parse ISO date string (format: +YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ssZ or +YYYY-MM-DD or YYYY-MM-DD or +YYYY-MM or YYYY-MM or +YYYY or YYYY)
85
+ const dateMatch = birthDateString.match(/^\+?(\d{4})(?:-(\d{1,2})(?:-(\d{1,2}))?)?/);
86
+ if (!dateMatch) return null;
87
+
88
+ const year = parseInt(dateMatch[1]);
89
+ const month = dateMatch[2] ? parseInt(dateMatch[2]) : 1; // Default to January if month not specified
90
+ const day = dateMatch[3] ? parseInt(dateMatch[3]) : 1; // Default to 1st if day not specified
91
+
92
+ // Handle invalid month/day values (like 00 for century-level dates)
93
+ const validMonth = month === 0 ? 1 : month;
94
+ const validDay = day === 0 ? 1 : day;
95
+
96
+ // Default time to noon if not specified
97
+ return {
98
+ year: year,
99
+ month: validMonth,
100
+ day: validDay,
101
+ hour: 12,
102
+ minute: 0
103
+ };
104
+ } catch (error) {
105
+ console.error('Error parsing birth date:', error.message);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // Aspect type mapping for shorthand support
111
+ const ASPECT_SHORTHAND = {
112
+ 'c': 'conjunction',
113
+ 'o': 'opposition',
114
+ 's': 'square',
115
+ 't': 'trine',
116
+ 'se': 'sextile',
117
+ 'q': 'quincunx'
118
+ };
119
+
120
+ // Function to create a Wikipedia search URL from a person's name
121
+ function createWikipediaSearchUrl(personName) {
122
+ // Create a Wikipedia search URL using the person's name
123
+ // This will take users to a search page where they can find the correct article
124
+ const encodedName = encodeURIComponent(personName);
125
+ return `https://en.wikipedia.org/wiki/Special:Search?search=${encodedName}`;
126
+ }
127
+
128
+ // Function to normalize aspect type (support shorthands)
129
+ function normalizeAspectType(aspectType) {
130
+ const normalized = aspectType.toLowerCase();
131
+ return ASPECT_SHORTHAND[normalized] || normalized;
132
+ }
133
+
134
+ // Function to check if an author has a specific aspect in their natal chart
135
+ function checkAuthorAspect(authorData, planet1, aspectType, planet2) {
136
+ if (!authorData.birthDateData) return null;
137
+
138
+ try {
139
+ // Normalize aspect type to handle shorthands
140
+ const normalizedAspectType = normalizeAspectType(aspectType);
141
+
142
+ // Calculate aspects for this author's birth chart
143
+ const aspects = calculatePlanetComboAspects([planet1, planet2], authorData.birthDateData, true);
144
+
145
+ // Look for the specific aspect type
146
+ const matchingAspect = aspects.find(aspect =>
147
+ ((aspect.planet1 === planet1 && aspect.planet2 === planet2) ||
148
+ (aspect.planet1 === planet2 && aspect.planet2 === planet1)) &&
149
+ aspect.type.toLowerCase() === normalizedAspectType.toLowerCase()
150
+ );
151
+
152
+ return matchingAspect || null;
153
+ } catch (error) {
154
+ console.error('Error checking aspect for author:', authorData.name, error.message);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ // Main function to find people with specific aspects by occupation
160
+ async function findPeopleWithAspect(occupation, planet1, aspectType, planet2, limit = 500) {
161
+ // Fetch people by occupation from Wikidata
162
+ const people = await fetchPeopleByOccupation(occupation, limit);
163
+
164
+ if (people.length === 0) {
165
+ console.log(`No ${occupation} found in Wikidata.`);
166
+ return [];
167
+ }
168
+
169
+ console.log(`Fetched ${people.length} ${occupation} from Wikidata. Checking aspects...`);
170
+
171
+ // Process each person to check for the specified aspect
172
+ const results = [];
173
+ let processedCount = 0;
174
+
175
+ for (const person of people) {
176
+ // Parse birth date
177
+ const birthDateData = parseBirthDate(person.birthDate);
178
+
179
+
180
+ // Add birth date data to person object
181
+ person.birthDateData = birthDateData;
182
+
183
+ // Check for the specific aspect
184
+ const aspect = checkAuthorAspect(person, planet1, aspectType, planet2);
185
+
186
+ if (aspect) {
187
+ // Create Wikipedia search URL using the person's name
188
+ const linkUrl = createWikipediaSearchUrl(person.name);
189
+
190
+ results.push({
191
+ name: person.name,
192
+ birthDate: person.birthDate,
193
+ linkUrl: linkUrl,
194
+ aspect: aspect,
195
+ birthDateData: person.birthDateData
196
+ });
197
+ }
198
+
199
+ processedCount++;
200
+ if (processedCount % 50 === 0) {
201
+ console.log(`Processed ${processedCount}/${people.length} ${occupation}...`);
202
+ }
203
+ }
204
+
205
+ console.log(`Found ${results.length} ${occupation} with ${planet1} ${aspectType} ${planet2} aspect.`);
206
+
207
+ // Sort by orb (closest aspects first)
208
+ results.sort((a, b) => parseFloat(a.aspect.orb) - parseFloat(b.aspect.orb));
209
+
210
+ return results;
211
+ }
212
+
213
+ // Backward compatibility function
214
+ async function findAuthorsWithAspect(planet1, aspectType, planet2, limit = 500) {
215
+ return findPeopleWithAspect('authors', planet1, aspectType, planet2, limit);
216
+ }
217
+
218
+ module.exports = {
219
+ fetchAuthorsFromWikidata,
220
+ findAuthorsWithAspect,
221
+ fetchPeopleByOccupation,
222
+ findPeopleWithAspect,
223
+ parseBirthDate,
224
+ checkAuthorAspect,
225
+ normalizeAspectType,
226
+ ASPECT_SHORTHAND,
227
+ PREDEFINED_OCCUPATIONS
228
+ };