@apmantza/greedysearch-pi 1.8.0 → 1.8.2

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/package.json CHANGED
@@ -1,46 +1,53 @@
1
- {
2
- "name": "@apmantza/greedysearch-pi",
3
- "version": "1.8.0",
4
- "description": "Pi extension: multi-engine AI search (Perplexity, Bing Copilot, Google AI) via browser automation -- NO API KEYS needed. Extracts answers with sources, optional Gemini synthesis. Grounded AI answers from real browser interactions.",
5
- "type": "module",
6
- "keywords": [
7
- "pi-package"
8
- ],
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/apmantza/GreedySearch-pi.git"
12
- },
13
- "author": "Apostolos Mantzaris",
14
- "license": "MIT",
15
- "scripts": {
16
- "test": "./test.sh",
17
- "test:quick": "./test.sh quick",
18
- "test:smoke": "./test.sh smoke"
19
- },
20
- "files": [
21
- "index.ts",
22
- "bin/",
23
- "src/",
24
- "skills/",
25
- "extractors/",
26
- "CHANGELOG.md",
27
- "README.md"
28
- ],
29
- "pi": {
30
- "extensions": [
31
- "./index.ts"
32
- ],
33
- "skills": [
34
- "./skills"
35
- ]
36
- },
37
- "dependencies": {
38
- "jsdom": "^24.0.0",
39
- "@mozilla/readability": "^0.5.0",
40
- "turndown": "^7.1.2"
41
- },
42
- "peerDependencies": {
43
- "@mariozechner/pi-coding-agent": "*",
44
- "@sinclair/typebox": "*"
45
- }
46
- }
1
+ {
2
+ "name": "@apmantza/greedysearch-pi",
3
+ "version": "1.8.2",
4
+ "description": "Pi extension: multi-engine AI search (Perplexity, Bing Copilot, Google AI) via browser automation -- NO API KEYS needed. Extracts answers with sources, optional Gemini synthesis. Grounded AI answers from real browser interactions.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/apmantza/GreedySearch-pi.git"
12
+ },
13
+ "author": "Apostolos Mantzaris",
14
+ "license": "MIT",
15
+ "scripts": {
16
+ "test": "node test.mjs",
17
+ "test:quick": "node test.mjs quick",
18
+ "test:smoke": "node test.mjs smoke",
19
+ "test:bash": "./test.sh",
20
+ "test:bash:quick": "./test.sh quick",
21
+ "test:bash:smoke": "./test.sh smoke"
22
+ },
23
+ "engines": {
24
+ "node": ">=20.11.0"
25
+ },
26
+ "files": [
27
+ "index.ts",
28
+ "test.mjs",
29
+ "bin/",
30
+ "src/",
31
+ "skills/",
32
+ "extractors/",
33
+ "CHANGELOG.md",
34
+ "README.md"
35
+ ],
36
+ "pi": {
37
+ "extensions": [
38
+ "./index.ts"
39
+ ],
40
+ "skills": [
41
+ "./skills"
42
+ ]
43
+ },
44
+ "dependencies": {
45
+ "jsdom": "^24.0.0",
46
+ "@mozilla/readability": "^0.5.0",
47
+ "turndown": "^7.1.2"
48
+ },
49
+ "peerDependencies": {
50
+ "@mariozechner/pi-coding-agent": "*",
51
+ "@sinclair/typebox": "*"
52
+ }
53
+ }
package/src/github.mjs CHANGED
@@ -1,237 +1,237 @@
1
- // src/github.mjs - GitHub content fetching via REST API
2
-
3
- const GITHUB_API = "https://api.github.com";
4
- const DEFAULT_HEADERS = {
5
- "user-agent": "GreedySearch/1.0",
6
- accept: "application/vnd.github+json",
7
- "x-github-api-version": "2022-11-28",
8
- };
9
-
10
- /**
11
- * Parse a GitHub URL into components
12
- * @param {string} url
13
- * @returns {{owner: string, repo: string, type: 'blob'|'tree'|'root', ref?: string, path?: string} | null}
14
- */
15
- export function parseGitHubUrl(url) {
16
- try {
17
- const parsed = new URL(url);
18
- if (!parsed.hostname.endsWith("github.com")) {
19
- return null;
20
- }
21
-
22
- const parts = parsed.pathname.split("/").filter(Boolean);
23
- if (parts.length < 2) {
24
- return null;
25
- }
26
-
27
- const [owner, repo] = parts;
28
-
29
- // Root: github.com/owner/repo
30
- if (parts.length === 2) {
31
- return { owner, repo, type: "root" };
32
- }
33
-
34
- // With type: github.com/owner/repo/blob|tree/ref/path
35
- if (parts.length >= 4 && (parts[2] === "blob" || parts[2] === "tree")) {
36
- const type = parts[2];
37
- const ref = parts[3];
38
- const path = parts.slice(4).join("/");
39
- return { owner, repo, type, ref, path };
40
- }
41
-
42
- return null;
43
- } catch {
44
- return null;
45
- }
46
- }
47
-
48
- /**
49
- * Fetch JSON from GitHub API with timeout
50
- */
51
- async function apiGet(path, timeoutMs = 10000) {
52
- const controller = new AbortController();
53
- const tid = setTimeout(() => controller.abort(), timeoutMs);
54
- try {
55
- const res = await fetch(`${GITHUB_API}${path}`, {
56
- headers: DEFAULT_HEADERS,
57
- signal: controller.signal,
58
- });
59
- clearTimeout(tid);
60
- if (!res.ok) {
61
- throw new Error(`GitHub API ${res.status}: ${path}`);
62
- }
63
- return await res.json();
64
- } catch (err) {
65
- clearTimeout(tid);
66
- throw err;
67
- }
68
- }
69
-
70
- /**
71
- * Fetch the default branch README as plain text
72
- */
73
- async function fetchReadme(owner, repo) {
74
- try {
75
- const data = await apiGet(`/repos/${owner}/${repo}/readme`);
76
- if (data.content && data.encoding === "base64") {
77
- return Buffer.from(data.content, "base64").toString("utf8");
78
- }
79
- return "";
80
- } catch {
81
- return "";
82
- }
83
- }
84
-
85
- /**
86
- * Fetch top-level file tree (non-recursive)
87
- */
88
- async function fetchTree(owner, repo, ref = "HEAD", subPath = "") {
89
- try {
90
- // Resolve ref to a tree SHA first when using HEAD or a branch name
91
- const refData = await apiGet(`/repos/${owner}/${repo}/git/ref/heads/${ref === "HEAD" ? "main" : ref}`).catch(() =>
92
- apiGet(`/repos/${owner}/${repo}/git/ref/heads/master`).catch(() => null)
93
- );
94
-
95
- let treeSha;
96
- if (refData?.object?.sha) {
97
- // Get commit to get tree SHA
98
- const commit = await apiGet(`/repos/${owner}/${repo}/git/commits/${refData.object.sha}`);
99
- treeSha = commit.tree.sha;
100
- } else {
101
- // Fall back to repo default branch info
102
- const repoInfo = await apiGet(`/repos/${owner}/${repo}`);
103
- const branch = await apiGet(`/repos/${owner}/${repo}/branches/${repoInfo.default_branch}`);
104
- treeSha = branch.commit.commit.tree.sha;
105
- }
106
-
107
- const treeData = await apiGet(`/repos/${owner}/${repo}/git/trees/${treeSha}`);
108
- let items = treeData.tree || [];
109
-
110
- // Filter to subPath if requested
111
- if (subPath) {
112
- items = items.filter((item) => item.path.startsWith(subPath));
113
- }
114
-
115
- return items.slice(0, 50).map((item) => ({
116
- path: item.path,
117
- type: item.type === "tree" ? "dir" : "file",
118
- size: item.size,
119
- }));
120
- } catch {
121
- return [];
122
- }
123
- }
124
-
125
- /**
126
- * Fetch a specific file via raw.githubusercontent.com
127
- */
128
- async function fetchRawFile(owner, repo, ref, filePath, timeoutMs = 10000) {
129
- const ref_ = ref && ref !== "HEAD" ? ref : "main";
130
- const urls = [
131
- `https://raw.githubusercontent.com/${owner}/${repo}/${ref_}/${filePath}`,
132
- `https://raw.githubusercontent.com/${owner}/${repo}/master/${filePath}`,
133
- ];
134
-
135
- for (const url of urls) {
136
- const controller = new AbortController();
137
- const tid = setTimeout(() => controller.abort(), timeoutMs);
138
- try {
139
- const res = await fetch(url, {
140
- headers: { "user-agent": DEFAULT_HEADERS["user-agent"] },
141
- signal: controller.signal,
142
- });
143
- clearTimeout(tid);
144
- if (res.ok) {
145
- return await res.text();
146
- }
147
- } catch {
148
- clearTimeout(tid);
149
- }
150
- }
151
- return null;
152
- }
153
-
154
- /**
155
- * Fetch GitHub content via API
156
- * @param {string} url - GitHub URL (blob, tree, or root)
157
- * @returns {Promise<{ok: boolean, content?: string, title?: string, error?: string, tree?: Array}>}
158
- */
159
- export async function fetchGitHubContent(url) {
160
- const parsed = parseGitHubUrl(url);
161
- if (!parsed) {
162
- return { ok: false, error: "Not a valid GitHub URL" };
163
- }
164
-
165
- const { owner, repo, type, ref, path } = parsed;
166
-
167
- try {
168
- if (type === "root" || (type === "tree" && !path)) {
169
- // Fetch repo info + README + top-level tree in parallel
170
- const [repoInfo, readme, tree] = await Promise.allSettled([
171
- apiGet(`/repos/${owner}/${repo}`),
172
- fetchReadme(owner, repo),
173
- fetchTree(owner, repo, ref || "HEAD"),
174
- ]);
175
-
176
- // If repo info failed (e.g. 404 — repo doesn't exist), bail out
177
- if (repoInfo.status === "rejected") {
178
- return { ok: false, error: repoInfo.reason?.message || "Repo not found" };
179
- }
180
-
181
- const info = repoInfo.value;
182
- const readmeText = readme.status === "fulfilled" ? readme.value : "";
183
- const treeItems = tree.status === "fulfilled" ? tree.value : [];
184
-
185
- const description = info?.description ? `\n\n> ${info.description}` : "";
186
- const stars = info?.stargazers_count != null ? ` ⭐ ${info.stargazers_count}` : "";
187
- const language = info?.language ? ` · ${info.language}` : "";
188
-
189
- let content = `# ${owner}/${repo}${stars}${language}${description}\n\n`;
190
-
191
- if (readmeText) {
192
- content += readmeText.slice(0, 6000);
193
- } else {
194
- content += `[No README found]\n\nFiles:\n${treeItems.map((t) => ` ${t.type === "dir" ? "📁" : "📄"} ${t.path}`).join("\n")}`;
195
- }
196
-
197
- return {
198
- ok: true,
199
- title: `${owner}/${repo}`,
200
- content,
201
- tree: treeItems.slice(0, 30),
202
- };
203
- }
204
-
205
- if (type === "blob" && path) {
206
- // Fetch specific file via raw URL
207
- const content = await fetchRawFile(owner, repo, ref, path);
208
- if (content === null) {
209
- return { ok: false, error: `File not found: ${path}` };
210
- }
211
- return {
212
- ok: true,
213
- title: `${owner}/${repo}: ${path}`,
214
- content,
215
- };
216
- }
217
-
218
- if (type === "tree" && path) {
219
- // Directory listing via API tree
220
- const treeItems = await fetchTree(owner, repo, ref || "HEAD", path);
221
- const listing = treeItems
222
- .map((t) => ` ${t.type === "dir" ? "📁" : "📄"} ${t.path}`)
223
- .join("\n");
224
-
225
- return {
226
- ok: true,
227
- title: `${owner}/${repo}/${path}`,
228
- content: `[Directory: ${path}]\n\nFiles:\n${listing}`,
229
- tree: treeItems,
230
- };
231
- }
232
-
233
- return { ok: false, error: "Unsupported GitHub URL type" };
234
- } catch (err) {
235
- return { ok: false, error: err.message };
236
- }
237
- }
1
+ // src/github.mjs - GitHub content fetching via REST API
2
+
3
+ const GITHUB_API = "https://api.github.com";
4
+ const DEFAULT_HEADERS = {
5
+ "user-agent": "GreedySearch/1.0",
6
+ accept: "application/vnd.github+json",
7
+ "x-github-api-version": "2022-11-28",
8
+ };
9
+
10
+ /**
11
+ * Parse a GitHub URL into components
12
+ * @param {string} url
13
+ * @returns {{owner: string, repo: string, type: 'blob'|'tree'|'root', ref?: string, path?: string} | null}
14
+ */
15
+ export function parseGitHubUrl(url) {
16
+ try {
17
+ const parsed = new URL(url);
18
+ if (!parsed.hostname.endsWith("github.com")) {
19
+ return null;
20
+ }
21
+
22
+ const parts = parsed.pathname.split("/").filter(Boolean);
23
+ if (parts.length < 2) {
24
+ return null;
25
+ }
26
+
27
+ const [owner, repo] = parts;
28
+
29
+ // Root: github.com/owner/repo
30
+ if (parts.length === 2) {
31
+ return { owner, repo, type: "root" };
32
+ }
33
+
34
+ // With type: github.com/owner/repo/blob|tree/ref/path
35
+ if (parts.length >= 4 && (parts[2] === "blob" || parts[2] === "tree")) {
36
+ const type = parts[2];
37
+ const ref = parts[3];
38
+ const path = parts.slice(4).join("/");
39
+ return { owner, repo, type, ref, path };
40
+ }
41
+
42
+ return null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fetch JSON from GitHub API with timeout
50
+ */
51
+ async function apiGet(path, timeoutMs = 10000) {
52
+ const controller = new AbortController();
53
+ const tid = setTimeout(() => controller.abort(), timeoutMs);
54
+ try {
55
+ const res = await fetch(`${GITHUB_API}${path}`, {
56
+ headers: DEFAULT_HEADERS,
57
+ signal: controller.signal,
58
+ });
59
+ clearTimeout(tid);
60
+ if (!res.ok) {
61
+ throw new Error(`GitHub API ${res.status}: ${path}`);
62
+ }
63
+ return await res.json();
64
+ } catch (err) {
65
+ clearTimeout(tid);
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Fetch the default branch README as plain text
72
+ */
73
+ async function fetchReadme(owner, repo) {
74
+ try {
75
+ const data = await apiGet(`/repos/${owner}/${repo}/readme`);
76
+ if (data.content && data.encoding === "base64") {
77
+ return Buffer.from(data.content, "base64").toString("utf8");
78
+ }
79
+ return "";
80
+ } catch {
81
+ return "";
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Fetch top-level file tree (non-recursive)
87
+ */
88
+ async function fetchTree(owner, repo, ref = "HEAD", subPath = "") {
89
+ try {
90
+ // Resolve ref to a tree SHA first when using HEAD or a branch name
91
+ const refData = await apiGet(`/repos/${owner}/${repo}/git/ref/heads/${ref === "HEAD" ? "main" : ref}`).catch(() =>
92
+ apiGet(`/repos/${owner}/${repo}/git/ref/heads/master`).catch(() => null)
93
+ );
94
+
95
+ let treeSha;
96
+ if (refData?.object?.sha) {
97
+ // Get commit to get tree SHA
98
+ const commit = await apiGet(`/repos/${owner}/${repo}/git/commits/${refData.object.sha}`);
99
+ treeSha = commit.tree.sha;
100
+ } else {
101
+ // Fall back to repo default branch info
102
+ const repoInfo = await apiGet(`/repos/${owner}/${repo}`);
103
+ const branch = await apiGet(`/repos/${owner}/${repo}/branches/${repoInfo.default_branch}`);
104
+ treeSha = branch.commit.commit.tree.sha;
105
+ }
106
+
107
+ const treeData = await apiGet(`/repos/${owner}/${repo}/git/trees/${treeSha}`);
108
+ let items = treeData.tree || [];
109
+
110
+ // Filter to subPath if requested
111
+ if (subPath) {
112
+ items = items.filter((item) => item.path.startsWith(subPath));
113
+ }
114
+
115
+ return items.slice(0, 50).map((item) => ({
116
+ path: item.path,
117
+ type: item.type === "tree" ? "dir" : "file",
118
+ size: item.size,
119
+ }));
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Fetch a specific file via raw.githubusercontent.com
127
+ */
128
+ async function fetchRawFile(owner, repo, ref, filePath, timeoutMs = 10000) {
129
+ const ref_ = ref && ref !== "HEAD" ? ref : "main";
130
+ const urls = [
131
+ `https://raw.githubusercontent.com/${owner}/${repo}/${ref_}/${filePath}`,
132
+ `https://raw.githubusercontent.com/${owner}/${repo}/master/${filePath}`,
133
+ ];
134
+
135
+ for (const url of urls) {
136
+ const controller = new AbortController();
137
+ const tid = setTimeout(() => controller.abort(), timeoutMs);
138
+ try {
139
+ const res = await fetch(url, {
140
+ headers: { "user-agent": DEFAULT_HEADERS["user-agent"] },
141
+ signal: controller.signal,
142
+ });
143
+ clearTimeout(tid);
144
+ if (res.ok) {
145
+ return await res.text();
146
+ }
147
+ } catch {
148
+ clearTimeout(tid);
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Fetch GitHub content via API
156
+ * @param {string} url - GitHub URL (blob, tree, or root)
157
+ * @returns {Promise<{ok: boolean, content?: string, title?: string, error?: string, tree?: Array}>}
158
+ */
159
+ export async function fetchGitHubContent(url) {
160
+ const parsed = parseGitHubUrl(url);
161
+ if (!parsed) {
162
+ return { ok: false, error: "Not a valid GitHub URL" };
163
+ }
164
+
165
+ const { owner, repo, type, ref, path } = parsed;
166
+
167
+ try {
168
+ if (type === "root" || (type === "tree" && !path)) {
169
+ // Fetch repo info + README + top-level tree in parallel
170
+ const [repoInfo, readme, tree] = await Promise.allSettled([
171
+ apiGet(`/repos/${owner}/${repo}`),
172
+ fetchReadme(owner, repo),
173
+ fetchTree(owner, repo, ref || "HEAD"),
174
+ ]);
175
+
176
+ // If repo info failed (e.g. 404 — repo doesn't exist), bail out
177
+ if (repoInfo.status === "rejected") {
178
+ return { ok: false, error: repoInfo.reason?.message || "Repo not found" };
179
+ }
180
+
181
+ const info = repoInfo.value;
182
+ const readmeText = readme.status === "fulfilled" ? readme.value : "";
183
+ const treeItems = tree.status === "fulfilled" ? tree.value : [];
184
+
185
+ const description = info?.description ? `\n\n> ${info.description}` : "";
186
+ const stars = info?.stargazers_count != null ? ` ⭐ ${info.stargazers_count}` : "";
187
+ const language = info?.language ? ` · ${info.language}` : "";
188
+
189
+ let content = `# ${owner}/${repo}${stars}${language}${description}\n\n`;
190
+
191
+ if (readmeText) {
192
+ content += readmeText.slice(0, 6000);
193
+ } else {
194
+ content += `[No README found]\n\nFiles:\n${treeItems.map((t) => ` ${t.type === "dir" ? "📁" : "📄"} ${t.path}`).join("\n")}`;
195
+ }
196
+
197
+ return {
198
+ ok: true,
199
+ title: `${owner}/${repo}`,
200
+ content,
201
+ tree: treeItems.slice(0, 30),
202
+ };
203
+ }
204
+
205
+ if (type === "blob" && path) {
206
+ // Fetch specific file via raw URL
207
+ const content = await fetchRawFile(owner, repo, ref, path);
208
+ if (content === null) {
209
+ return { ok: false, error: `File not found: ${path}` };
210
+ }
211
+ return {
212
+ ok: true,
213
+ title: `${owner}/${repo}: ${path}`,
214
+ content,
215
+ };
216
+ }
217
+
218
+ if (type === "tree" && path) {
219
+ // Directory listing via API tree
220
+ const treeItems = await fetchTree(owner, repo, ref || "HEAD", path);
221
+ const listing = treeItems
222
+ .map((t) => ` ${t.type === "dir" ? "📁" : "📄"} ${t.path}`)
223
+ .join("\n");
224
+
225
+ return {
226
+ ok: true,
227
+ title: `${owner}/${repo}/${path}`,
228
+ content: `[Directory: ${path}]\n\nFiles:\n${listing}`,
229
+ tree: treeItems,
230
+ };
231
+ }
232
+
233
+ return { ok: false, error: "Unsupported GitHub URL type" };
234
+ } catch (err) {
235
+ return { ok: false, error: err.message };
236
+ }
237
+ }