vipcare 0.3.9 → 0.3.10

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.
@@ -1,57 +1,176 @@
1
1
  import { execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
2
5
  import { checkTool } from '../config.js';
3
6
 
4
- export function search(query, maxResults = 5) {
5
- if (checkTool('ddgs')) {
6
- try {
7
- const output = execFileSync('ddgs', ['text', query, '-m', String(maxResults), '-o', 'json'], {
8
- encoding: 'utf-8',
9
- timeout: 15000,
10
- stdio: ['pipe', 'pipe', 'pipe'],
7
+ function findDdgs() {
8
+ if (checkTool('ddgs')) return 'ddgs';
9
+
10
+ const candidates = [
11
+ path.join(os.homedir(), 'Library', 'Python', '3.9', 'bin', 'ddgs'),
12
+ path.join(os.homedir(), 'Library', 'Python', '3.10', 'bin', 'ddgs'),
13
+ path.join(os.homedir(), 'Library', 'Python', '3.11', 'bin', 'ddgs'),
14
+ path.join(os.homedir(), 'Library', 'Python', '3.12', 'bin', 'ddgs'),
15
+ path.join(os.homedir(), 'Library', 'Python', '3.13', 'bin', 'ddgs'),
16
+ path.join(os.homedir(), '.local', 'bin', 'ddgs'),
17
+ '/opt/homebrew/bin/ddgs',
18
+ '/usr/local/bin/ddgs',
19
+ ];
20
+
21
+ for (const p of candidates) {
22
+ if (fs.existsSync(p)) return p;
23
+ }
24
+
25
+ return null;
26
+ }
27
+
28
+ function searchViaDdgs(query, maxResults) {
29
+ const ddgsPath = findDdgs();
30
+ if (!ddgsPath) return null;
31
+
32
+ try {
33
+ const output = execFileSync(ddgsPath, ['text', query, '-m', String(maxResults), '-o', 'json'], {
34
+ encoding: 'utf-8',
35
+ timeout: 15000,
36
+ stdio: ['pipe', 'pipe', 'pipe'],
37
+ });
38
+
39
+ const parsed = JSON.parse(output);
40
+ if (!Array.isArray(parsed)) return null;
41
+ return parsed.map(r => ({
42
+ title: String(r.title || ''),
43
+ url: String(r.href || r.url || ''),
44
+ body: String(r.body || ''),
45
+ }));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function searchViaDdgApi(query) {
52
+ // DuckDuckGo Instant Answer API — always works, no bot detection
53
+ try {
54
+ const encoded = encodeURIComponent(query);
55
+ const output = execFileSync('curl', [
56
+ '-s', `https://api.duckduckgo.com/?q=${encoded}&format=json&no_html=1`,
57
+ ], {
58
+ encoding: 'utf-8',
59
+ timeout: 10000,
60
+ stdio: ['pipe', 'pipe', 'pipe'],
61
+ });
62
+
63
+ const data = JSON.parse(output);
64
+ const results = [];
65
+
66
+ // Main abstract
67
+ if (data.Abstract) {
68
+ results.push({
69
+ title: data.Heading || query,
70
+ url: data.AbstractURL || '',
71
+ body: data.Abstract,
11
72
  });
73
+ }
74
+
75
+ // Related topics
76
+ if (data.RelatedTopics) {
77
+ for (const topic of data.RelatedTopics) {
78
+ if (topic.Text && topic.FirstURL) {
79
+ results.push({
80
+ title: topic.Text.substring(0, 80),
81
+ url: topic.FirstURL,
82
+ body: topic.Text,
83
+ });
84
+ }
85
+ // Subtopics
86
+ if (topic.Topics) {
87
+ for (const sub of topic.Topics) {
88
+ if (sub.Text && sub.FirstURL) {
89
+ results.push({
90
+ title: sub.Text.substring(0, 80),
91
+ url: sub.FirstURL,
92
+ body: sub.Text,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
12
99
 
13
- const parsed = JSON.parse(output);
14
- if (!Array.isArray(parsed)) return [];
15
- return parsed.map(r => ({
16
- title: String(r.title || ''),
17
- url: String(r.href || r.url || ''),
18
- body: String(r.body || ''),
19
- }));
20
- } catch {
21
- // fall through
100
+ // Infobox
101
+ if (data.Infobox?.content) {
102
+ for (const item of data.Infobox.content) {
103
+ if (item.label && item.value) {
104
+ results.push({
105
+ title: `${item.label}: ${item.value}`,
106
+ url: '',
107
+ body: `${item.label}: ${item.value}`,
108
+ });
109
+ }
110
+ }
22
111
  }
112
+
113
+ return results.length > 0 ? results : null;
114
+ } catch {
115
+ return null;
23
116
  }
117
+ }
24
118
 
25
- // Fallback: use curl with DuckDuckGo lite
119
+ function searchViaCurl(query, maxResults) {
120
+ // Fallback: DuckDuckGo HTML search
26
121
  try {
27
122
  const encoded = encodeURIComponent(query);
28
- const output = execFileSync('curl', ['-s', `https://lite.duckduckgo.com/lite/?q=${encoded}`, '-H', 'User-Agent: VIPCare/0.1'], {
123
+ const output = execFileSync('curl', [
124
+ '-s', '-L',
125
+ `https://html.duckduckgo.com/html/?q=${encoded}`,
126
+ '-H', 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
127
+ ], {
29
128
  encoding: 'utf-8',
30
129
  timeout: 10000,
31
130
  stdio: ['pipe', 'pipe', 'pipe'],
32
131
  });
33
132
 
34
133
  const results = [];
35
- const linkRegex = /<a[^>]+href="([^"]+)"[^>]*class="result-link"[^>]*>([^<]+)<\/a>/g;
36
- const snippetRegex = /<td class="result-snippet">([^<]+)<\/td>/g;
134
+ const resultRegex = /<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]*(?:<[^>]+>[^<]*)*)<\/a>/g;
135
+ const snippetRegex = /<a[^>]+class="result__snippet"[^>]*>([^<]*(?:<[^>]+>[^<]*)*)<\/a>/g;
37
136
 
38
137
  let match;
39
- while ((match = linkRegex.exec(output)) && results.length < maxResults) {
40
- results.push({ title: match[2].trim(), url: match[1], body: '' });
138
+ while ((match = resultRegex.exec(output)) && results.length < maxResults) {
139
+ const title = match[2].replace(/<[^>]+>/g, '').trim();
140
+ let url = match[1];
141
+ const uddg = url.match(/uddg=([^&]+)/);
142
+ if (uddg) url = decodeURIComponent(uddg[1]);
143
+ results.push({ title, url, body: '' });
41
144
  }
42
145
 
43
146
  let i = 0;
44
147
  while ((match = snippetRegex.exec(output)) && i < results.length) {
45
- results[i].body = match[1].trim();
148
+ results[i].body = match[1].replace(/<[^>]+>/g, '').trim();
46
149
  i++;
47
150
  }
48
151
 
49
- return results;
152
+ return results.length > 0 ? results : null;
50
153
  } catch {
51
- return [];
154
+ return null;
52
155
  }
53
156
  }
54
157
 
158
+ export function search(query, maxResults = 5) {
159
+ // Try ddgs CLI first (best results)
160
+ const ddgsResults = searchViaDdgs(query, maxResults);
161
+ if (ddgsResults?.length) return ddgsResults;
162
+
163
+ // Try DDG Instant Answer API (always works, but limited)
164
+ const apiResults = searchViaDdgApi(query);
165
+ if (apiResults?.length) return apiResults;
166
+
167
+ // Last resort: DDG HTML (may be blocked by CAPTCHA)
168
+ const curlResults = searchViaCurl(query, maxResults);
169
+ if (curlResults?.length) return curlResults;
170
+
171
+ return [];
172
+ }
173
+
55
174
  export function searchPerson(name, company) {
56
175
  const queries = [`"${name}"`];
57
176
  if (company) queries.push(`"${name}" "${company}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {