@lincoln504/pi-research 0.1.12 → 0.1.13
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 +22 -5
- package/package.json +4 -4
- package/scripts/setup.js +122 -84
- package/src/config.ts +12 -0
- package/src/index.ts +1 -1
- package/src/infrastructure/searxng-lifecycle.ts +70 -3
- package/src/utils/text-utils.ts +11 -1
package/README.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
Multi-agent web research extension for [pi](https://github.com/badlogic/pi-mono). Runs parallel AI researchers against a self-hosted SearXNG search container, scrapes pages, queries security databases, and tracks everything in a real-time terminal UI.
|
|
6
6
|
|
|
7
|
+
There are other ways to connect a `pi` session to the internet. Here is why this one is designed the way it is:
|
|
8
|
+
|
|
9
|
+
- **SearXNG-based** — free, no API keys required by default. A single Docker container is shared across all concurrent research calls on the same machine; no duplicate instances, no per-session overhead. Point `SEARXNG_URL` at an existing instance to skip container management entirely.
|
|
10
|
+
- **Unified subagent system** — web access, scraping, and synthesis are handled by isolated researcher agents. Research context stays out of the main chat session while still enabling thorough multi-source exploration.
|
|
11
|
+
- **Safety** — researcher agents have no access to file writes, file edits, or shell commands. All web tools are scoped to the subagent system and executed in a structured, tool-limited sequence to keep agents focused and prevent runaway use.
|
|
12
|
+
- **Minimal integration** — a system prompt hook provides light guidance to the host LLM on when and how to invoke the tool. No manual prompt engineering required. The extension does not conflict with other subagent or tool systems, and the unified toolset means no need to wire up or tune multiple separate tools.
|
|
13
|
+
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
## Contents
|
|
@@ -47,6 +54,7 @@ Multi-agent web research extension for [pi](https://github.com/badlogic/pi-mono)
|
|
|
47
54
|
- The `pi` CLI installed and configured
|
|
48
55
|
- Docker daemon running (required for SearXNG)
|
|
49
56
|
- Internet access
|
|
57
|
+
- LLM in pi with minimum 100k context window (optimized for 100k+)
|
|
50
58
|
|
|
51
59
|
---
|
|
52
60
|
|
|
@@ -56,10 +64,21 @@ Multi-agent web research extension for [pi](https://github.com/badlogic/pi-mono)
|
|
|
56
64
|
pi install npm:@lincoln504/pi-research
|
|
57
65
|
```
|
|
58
66
|
|
|
67
|
+
The installation will automatically download and install:
|
|
68
|
+
- Node.js dependencies
|
|
69
|
+
- Playwright browser binaries (Chromium)
|
|
70
|
+
|
|
71
|
+
This process may take a few minutes on first install. If browser installation fails during postinstall, chromium will be installed automatically when you first use the research tool.
|
|
72
|
+
|
|
73
|
+
**Manual browser installation (if needed)**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm run install:browsers
|
|
77
|
+
```
|
|
78
|
+
|
|
59
79
|
**From a checkout**
|
|
60
80
|
|
|
61
81
|
```bash
|
|
62
|
-
npm install
|
|
63
82
|
pi install .
|
|
64
83
|
```
|
|
65
84
|
|
|
@@ -69,11 +88,8 @@ pi install .
|
|
|
69
88
|
|
|
70
89
|
**macOS** — Install Docker Desktop and start it from Applications.
|
|
71
90
|
|
|
72
|
-
**Linux** — Install Docker
|
|
91
|
+
**Linux** — Install Docker for your distribution ([docs.docker.com/engine/install](https://docs.docker.com/engine/install)), then:
|
|
73
92
|
```bash
|
|
74
|
-
# Install Docker
|
|
75
|
-
sudo apt-get update && sudo apt-get install -y docker.io
|
|
76
|
-
|
|
77
93
|
# Start Docker
|
|
78
94
|
sudo systemctl start docker
|
|
79
95
|
sudo systemctl enable docker
|
|
@@ -110,6 +126,7 @@ Do a deep dive on CVE-2024-3094 at depth 2
|
|
|
110
126
|
|
|
111
127
|
| Variable | Default | Description |
|
|
112
128
|
|----------|---------|-------------|
|
|
129
|
+
| `SEARXNG_URL` | — | External SearXNG base URL (e.g. `http://localhost:8080`). When set, Docker container management is skipped entirely and this URL is used directly |
|
|
113
130
|
| `BRAVE_SEARCH_API_KEY` | — | Enables `braveapi` engine (official REST API) |
|
|
114
131
|
| `PI_RESEARCH_VERBOSE` | — | Set to `1` to write diagnostic JSONL logs to a temp file (see below) |
|
|
115
132
|
| `PI_RESEARCH_SKIP_HEALTHCHECK` | — | Skip startup health check (set to `1` if engines are blocked) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "A set of web research tools (search, scrape, and others) and research agent system that uses them safely and efficiently.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"test:integration": "vitest run --config config/tooling/vitest.config.integration.ts",
|
|
51
51
|
"test:watch": "vitest --config config/tooling/vitest.config.unit.ts",
|
|
52
52
|
"test:coverage": "vitest run --config config/tooling/vitest.config.ts --coverage",
|
|
53
|
-
"postinstall": "node scripts/setup.js",
|
|
53
|
+
"postinstall": "node scripts/setup.js || exit 0",
|
|
54
54
|
"install:browsers": "npx playwright install chromium",
|
|
55
|
-
"install:deps": "npx playwright install-deps",
|
|
56
|
-
"setup": "npm
|
|
55
|
+
"install:deps": "npx playwright install-deps chromium",
|
|
56
|
+
"setup": "npm run install:browsers"
|
|
57
57
|
},
|
|
58
58
|
"pi": {
|
|
59
59
|
"extensions": [
|
package/scripts/setup.js
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
* pi-research Postinstall Setup Script
|
|
5
5
|
*
|
|
6
6
|
* This script runs automatically after `npm install` and ensures:
|
|
7
|
-
* 1. Playwright browser binaries are installed (Chromium
|
|
7
|
+
* 1. Playwright browser binaries are installed (Chromium)
|
|
8
8
|
* 2. System dependencies are available (if run with --system-deps flag)
|
|
9
9
|
*
|
|
10
|
+
* Key Design Principles:
|
|
11
|
+
* - NEVER exits with code 1 - this would block npm install from completing
|
|
12
|
+
* - Uses npx playwright install, which works if playwright is in node_modules
|
|
13
|
+
* - Provides clear feedback about installation status
|
|
14
|
+
* - Gracefully handles missing dependencies (warns but doesn't fail)
|
|
15
|
+
*
|
|
10
16
|
* Usage:
|
|
11
|
-
* npm run setup
|
|
12
|
-
* npm run install:browsers
|
|
13
|
-
* npm run install:deps
|
|
17
|
+
* npm run setup # Full setup: install deps + browsers
|
|
18
|
+
* npm run install:browsers # Install only browser binaries
|
|
19
|
+
* npm run install:deps # Install system dependencies only
|
|
14
20
|
*
|
|
15
21
|
* Environment Variables:
|
|
16
22
|
* PLAYWRIGHT_BROWSERS_PATH - Custom path for browser cache
|
|
@@ -19,7 +25,7 @@
|
|
|
19
25
|
|
|
20
26
|
import { execSync } from 'child_process';
|
|
21
27
|
import { existsSync, readdirSync, statSync } from 'fs';
|
|
22
|
-
import {
|
|
28
|
+
import { dirname, join } from 'path';
|
|
23
29
|
import { fileURLToPath } from 'url';
|
|
24
30
|
import { homedir, platform } from 'os';
|
|
25
31
|
|
|
@@ -50,17 +56,6 @@ if (major < requiredVersions.min) {
|
|
|
50
56
|
|
|
51
57
|
console.log(` Required: >= ${requiredVersions.min}.0.0`);
|
|
52
58
|
|
|
53
|
-
// Check if playwright is installed
|
|
54
|
-
const playwrightPath = join(process.cwd(), 'node_modules', 'playwright');
|
|
55
|
-
|
|
56
|
-
if (!existsSync(playwrightPath)) {
|
|
57
|
-
console.error('\n❌ ERROR: playwright is not installed.');
|
|
58
|
-
console.error(' Run `npm install` first, then `npm run setup`.');
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log('✅ Playwright package installed');
|
|
63
|
-
|
|
64
59
|
// Detect OS for browser installation
|
|
65
60
|
const isLinux = platform() === 'linux';
|
|
66
61
|
const isDarwin = platform() === 'darwin';
|
|
@@ -68,88 +63,130 @@ const isWindows = platform() === 'win32';
|
|
|
68
63
|
|
|
69
64
|
console.log(`\n📦 Detected platform: ${platform()}`);
|
|
70
65
|
|
|
71
|
-
//
|
|
72
|
-
|
|
66
|
+
// Track installation success
|
|
67
|
+
let playwrightInstalled = false;
|
|
68
|
+
let browsersInstalled = false;
|
|
69
|
+
let systemDepsInstalled = false;
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const installDeps = process.argv.includes('--system-deps') ||
|
|
77
|
-
process.env.PLAYWRIGHT_INSTALL_DEPS === 'true';
|
|
78
|
-
|
|
79
|
-
if (installDeps && isLinux) {
|
|
80
|
-
console.log('⚙️ Installing Linux system dependencies for Playwright...');
|
|
81
|
-
try {
|
|
82
|
-
execSync('npx playwright install-deps', {
|
|
83
|
-
stdio: 'inherit',
|
|
84
|
-
env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '0' }
|
|
85
|
-
});
|
|
86
|
-
console.log('✅ System dependencies installed');
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.warn('⚠️ Could not install system dependencies automatically.');
|
|
89
|
-
console.warn(' Try running: sudo apt-get install $(npx playwright install-deps --print-deps)');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
71
|
+
// Check if we should skip browser installation
|
|
72
|
+
const skipBrowserDownload = process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1';
|
|
92
73
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
f.startsWith(browser) && statSync(join(cacheDir, f)).isDirectory()
|
|
119
|
-
);
|
|
120
|
-
} catch {
|
|
121
|
-
// cache dir may not exist yet or may be at a different path
|
|
74
|
+
if (skipBrowserDownload) {
|
|
75
|
+
console.log('\n⏭️ Skipping browser installation (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1)');
|
|
76
|
+
} else {
|
|
77
|
+
// Install browsers using npx playwright
|
|
78
|
+
console.log('\n🌐 Installing Playwright browsers...');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Check if user wants to install system dependencies
|
|
82
|
+
const installDeps = process.argv.includes('--system-deps') ||
|
|
83
|
+
process.env.PLAYWRIGHT_INSTALL_DEPS === 'true';
|
|
84
|
+
|
|
85
|
+
if (installDeps && isLinux) {
|
|
86
|
+
console.log('⚙️ Installing Linux system dependencies for Playwright...');
|
|
87
|
+
try {
|
|
88
|
+
execSync('npx playwright install-deps', {
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '0' }
|
|
91
|
+
});
|
|
92
|
+
console.log('✅ System dependencies installed');
|
|
93
|
+
systemDepsInstalled = true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn('⚠️ Could not install system dependencies automatically.');
|
|
96
|
+
console.warn(' Try running: sudo apt-get install $(npx playwright install-deps --print-deps)');
|
|
97
|
+
// Don't fail - continue with browser installation
|
|
98
|
+
}
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
|
|
125
|
-
|
|
101
|
+
// Install browser binaries
|
|
102
|
+
// Only Chromium is needed for scraping (as per scrapers.ts implementation)
|
|
103
|
+
console.log(' Installing Chromium browser...');
|
|
104
|
+
execSync('npx playwright install chromium', {
|
|
105
|
+
stdio: 'inherit',
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH || ''
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log('✅ Browsers installed successfully');
|
|
113
|
+
browsersInstalled = true;
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Parse error message to provide better guidance
|
|
117
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
118
|
+
|
|
119
|
+
if (errorMsg.includes('npx: command not found')) {
|
|
120
|
+
console.warn('\n⚠️ npx not available - skipping browser installation');
|
|
121
|
+
console.warn(' Chromium can be installed later with: npm run install:browsers');
|
|
122
|
+
} else if (errorMsg.includes('playwright') || errorMsg.includes('ENOENT')) {
|
|
123
|
+
console.warn('\n⚠️ Playwright package not yet available - skipping browser installation');
|
|
124
|
+
console.warn(' This is normal during initial npm install.');
|
|
125
|
+
console.warn(' Chromium will be installed automatically when you run the research tool.');
|
|
126
|
+
console.warn(' Or manually with: npm run install:browsers');
|
|
126
127
|
} else {
|
|
127
|
-
console.warn(
|
|
128
|
+
console.warn('\n⚠️ Browser installation failed:');
|
|
129
|
+
console.warn(` ${errorMsg}`);
|
|
130
|
+
console.warn('\n💡 You can install Chromium later with:');
|
|
131
|
+
console.warn(' npm run install:browsers');
|
|
128
132
|
}
|
|
129
|
-
});
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
// NEVER exit with code 1 - let npm install complete
|
|
135
|
+
console.log('\n📦 pi-research installation will continue...');
|
|
136
|
+
console.log('='.repeat(60));
|
|
137
|
+
console.log('✅ pi-research installed (browsers can be installed later)');
|
|
138
|
+
console.log('='.repeat(60) + '\n');
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Verify installation
|
|
144
|
+
console.log('\n📋 Verifying browser installation...');
|
|
145
|
+
|
|
146
|
+
const browsers = ['chromium']; // Only Chromium is required for scraping
|
|
147
|
+
const cacheDir = process.env['PLAYWRIGHT_BROWSERS_PATH'] ||
|
|
148
|
+
(process.platform === 'win32'
|
|
149
|
+
? join(process.env['LOCALAPPDATA'] || join(homedir(), 'AppData', 'Local'), 'ms-playwright')
|
|
150
|
+
: join(homedir(), '.cache', 'ms-playwright'));
|
|
151
|
+
|
|
152
|
+
browsers.forEach(browser => {
|
|
153
|
+
let browserDirs = [];
|
|
133
154
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
155
|
+
browserDirs = readdirSync(cacheDir).filter(f =>
|
|
156
|
+
f.startsWith(browser) && statSync(join(cacheDir, f)).isDirectory()
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
// cache dir may not exist yet or may be at a different path
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (browserDirs.length > 0) {
|
|
163
|
+
console.log(` ✅ ${browser}: ${browserDirs.sort().join(', ')}`);
|
|
164
|
+
} else {
|
|
165
|
+
console.warn(` ⚠️ ${browser}: Not found in cache`);
|
|
138
166
|
}
|
|
167
|
+
});
|
|
139
168
|
|
|
169
|
+
// Show browser versions
|
|
170
|
+
console.log('\n🔍 Browser versions:');
|
|
171
|
+
try {
|
|
172
|
+
const playwrightVersion = execSync('npx playwright --version', { encoding: 'utf8' }).trim();
|
|
173
|
+
console.log(` Playwright: ${playwrightVersion}`);
|
|
174
|
+
playwrightInstalled = true;
|
|
140
175
|
} catch (error) {
|
|
141
|
-
console.
|
|
142
|
-
console.error(' Error:', error.message);
|
|
143
|
-
console.error('\n💡 Troubleshooting:');
|
|
144
|
-
console.error(' 1. Ensure you have internet connectivity');
|
|
145
|
-
console.error(' 2. Try running: npm run install:browsers');
|
|
146
|
-
console.error(' 3. Check Node.js version: node --version');
|
|
147
|
-
console.error(' 4. For Linux: sudo apt-get install -y $(npx playwright install-deps --print-deps)');
|
|
148
|
-
process.exit(1);
|
|
176
|
+
console.warn(' Could not determine Playwright version');
|
|
149
177
|
}
|
|
150
178
|
|
|
179
|
+
// Final status message
|
|
180
|
+
const setupComplete = browsersInstalled && playwrightInstalled;
|
|
181
|
+
|
|
151
182
|
console.log('\n' + '='.repeat(60));
|
|
152
|
-
|
|
183
|
+
if (setupComplete) {
|
|
184
|
+
console.log('🎉 Setup complete! Chromium browser installed. pi-research is ready to use.');
|
|
185
|
+
} else {
|
|
186
|
+
console.log('📦 pi-research installed successfully!');
|
|
187
|
+
console.log(' Chromium browser installation may be in progress or was skipped.');
|
|
188
|
+
console.log(' The browser will be installed automatically when you first use the research tool.');
|
|
189
|
+
}
|
|
153
190
|
console.log('='.repeat(60));
|
|
154
191
|
|
|
155
192
|
// Additional info
|
|
@@ -158,6 +195,7 @@ console.log(' • npm test - Run unit tests');
|
|
|
158
195
|
console.log(' • npm run type-check - Type check TypeScript');
|
|
159
196
|
console.log(' • npm run lint - Run ESLint');
|
|
160
197
|
console.log(' • npm run setup - Full setup with system deps');
|
|
198
|
+
console.log(' • npm run install:browsers - Install Chromium browser only');
|
|
161
199
|
|
|
162
200
|
// Platform-specific tips
|
|
163
201
|
if (isLinux) {
|
package/src/config.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface Config {
|
|
|
27
27
|
CONSOLE_RESTORE_DELAY_MS: number;
|
|
28
28
|
/** BCP 47 language tag sent to SearXNG to filter search results (default: 'en-US') */
|
|
29
29
|
SEARCH_LANGUAGE: string;
|
|
30
|
+
/** External SearXNG base URL — bypasses Docker container management entirely when set */
|
|
31
|
+
SEARXNG_URL?: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -41,6 +43,7 @@ const DEFAULTS: Config = {
|
|
|
41
43
|
TUI_REFRESH_DEBOUNCE_MS: 10,
|
|
42
44
|
CONSOLE_RESTORE_DELAY_MS: 15000,
|
|
43
45
|
SEARCH_LANGUAGE: 'en-US',
|
|
46
|
+
SEARXNG_URL: undefined,
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -110,6 +113,7 @@ export function createConfig(env: Record<string, string | undefined> = process.e
|
|
|
110
113
|
DEFAULTS.CONSOLE_RESTORE_DELAY_MS
|
|
111
114
|
),
|
|
112
115
|
SEARCH_LANGUAGE: parseEnvString(env, 'PI_RESEARCH_SEARCH_LANGUAGE') ?? DEFAULTS.SEARCH_LANGUAGE,
|
|
116
|
+
SEARXNG_URL: parseEnvString(env, 'SEARXNG_URL'),
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -200,7 +204,15 @@ export function getBraveSearchApiKey(): string | undefined {
|
|
|
200
204
|
return getConfig().BRAVE_SEARCH_API_KEY;
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
/**
|
|
208
|
+
* @deprecated Use getConfig().SEARXNG_URL instead
|
|
209
|
+
*/
|
|
210
|
+
export function getExternalSearxngUrl(): string | undefined {
|
|
211
|
+
return getConfig().SEARXNG_URL;
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
// Legacy variable exports (for backward compatibility, but reflect module load time values)
|
|
204
215
|
export const RESEARCHER_TIMEOUT_MS = getResearcherTimeoutMs();
|
|
205
216
|
export const PROXY_URL = getProxyUrl();
|
|
206
217
|
export const BRAVE_SEARCH_API_KEY = getBraveSearchApiKey();
|
|
218
|
+
export const SEARXNG_URL = getExternalSearxngUrl();
|
package/src/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
47
47
|
pi.on('session_start', async (_event, ctx) => {
|
|
48
48
|
// Skip Docker check during install to avoid errors on fresh systems
|
|
49
49
|
// Docker will be checked when research tool is first used
|
|
50
|
-
if (!process.env['PI_INSTALL_MODE']) {
|
|
50
|
+
if (!process.env['PI_INSTALL_MODE'] && !process.env['SEARXNG_URL']) {
|
|
51
51
|
const dockerCheck = await checkDockerAvailability();
|
|
52
52
|
if (!dockerCheck.running) {
|
|
53
53
|
ctx.ui.notify(`pi-research: ${dockerCheck.error}`, 'warning');
|
|
@@ -24,7 +24,7 @@ import * as path from 'node:path';
|
|
|
24
24
|
import * as fs from 'node:fs';
|
|
25
25
|
import * as os from 'node:os';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
-
import { PROXY_URL, BRAVE_SEARCH_API_KEY } from '../config.ts';
|
|
27
|
+
import { PROXY_URL, BRAVE_SEARCH_API_KEY, SEARXNG_URL } from '../config.ts';
|
|
28
28
|
import { shutdownManager } from '../utils/shutdown-manager.ts';
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -98,6 +98,8 @@ export interface SearxngLifecycleConfig {
|
|
|
98
98
|
manager?: DockerSearxngManager | null;
|
|
99
99
|
proxyUrl?: string;
|
|
100
100
|
braveApiKey?: string;
|
|
101
|
+
/** External SearXNG base URL. When set, Docker container management is skipped entirely. */
|
|
102
|
+
searxngUrl?: string;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
type ResolvedConfig = {
|
|
@@ -106,6 +108,7 @@ type ResolvedConfig = {
|
|
|
106
108
|
manager: DockerSearxngManager | null;
|
|
107
109
|
proxyUrl: string | undefined;
|
|
108
110
|
braveApiKey: string | undefined;
|
|
111
|
+
searxngUrl: string | undefined;
|
|
109
112
|
};
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -132,6 +135,7 @@ const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
132
135
|
manager: null,
|
|
133
136
|
proxyUrl: PROXY_URL,
|
|
134
137
|
braveApiKey: BRAVE_SEARCH_API_KEY,
|
|
138
|
+
searxngUrl: SEARXNG_URL,
|
|
135
139
|
};
|
|
136
140
|
|
|
137
141
|
/**
|
|
@@ -152,6 +156,10 @@ export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
|
152
156
|
private statusCallbacks: StatusCallback[] = [];
|
|
153
157
|
private readonly config: ResolvedConfig;
|
|
154
158
|
private proxySettingsPath: string | null = null;
|
|
159
|
+
/** Coalesces concurrent init() calls onto a single in-flight promise. */
|
|
160
|
+
private initializationPromise: Promise<void> | null = null;
|
|
161
|
+
/** Set when SEARXNG_URL is configured; signals that Docker management is bypassed. */
|
|
162
|
+
private externalSearxngUrl: string | null = null;
|
|
155
163
|
|
|
156
164
|
constructor(config: Partial<SearxngLifecycleConfig> = {}) {
|
|
157
165
|
this.config = { ...DEFAULT_CONFIG, ...config } as ResolvedConfig;
|
|
@@ -162,9 +170,15 @@ export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
/**
|
|
165
|
-
* Get manager instance (for passing to web-research module)
|
|
173
|
+
* Get manager instance (for passing to web-research module).
|
|
174
|
+
* In external URL mode, returns a minimal shim that satisfies the getSearxngUrl()
|
|
175
|
+
* interface used by web-research tools and the health check.
|
|
166
176
|
*/
|
|
167
177
|
getManager(): DockerSearxngManager | null {
|
|
178
|
+
if (this.externalSearxngUrl) {
|
|
179
|
+
const url = this.externalSearxngUrl;
|
|
180
|
+
return { getSearxngUrl: () => url } as unknown as DockerSearxngManager;
|
|
181
|
+
}
|
|
168
182
|
return this.manager;
|
|
169
183
|
}
|
|
170
184
|
|
|
@@ -279,6 +293,46 @@ export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
|
279
293
|
return;
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
// Coalesce concurrent init() calls: only the first proceeds; latecomers
|
|
297
|
+
// await the in-flight promise and return once it resolves.
|
|
298
|
+
// This assignment and check are both synchronous (no await between them),
|
|
299
|
+
// so JavaScript's single-threaded event loop guarantees only one caller
|
|
300
|
+
// can set initializationPromise before any other caller checks it.
|
|
301
|
+
if (this.initializationPromise) {
|
|
302
|
+
logger.log('[pi-research] SearXNG init already in progress, awaiting');
|
|
303
|
+
await this.initializationPromise;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let resolveInit!: () => void;
|
|
308
|
+
let rejectInit!: (err: unknown) => void;
|
|
309
|
+
this.initializationPromise = new Promise<void>((resolve, reject) => {
|
|
310
|
+
resolveInit = resolve;
|
|
311
|
+
rejectInit = reject;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await this._performInit(ctx);
|
|
316
|
+
resolveInit();
|
|
317
|
+
} catch (err) {
|
|
318
|
+
rejectInit(err);
|
|
319
|
+
throw err;
|
|
320
|
+
} finally {
|
|
321
|
+
this.initializationPromise = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async _performInit(ctx: ExtensionContext): Promise<void> {
|
|
326
|
+
// External SearXNG URL: skip all Docker management and use the URL directly.
|
|
327
|
+
if (this.config.searxngUrl) {
|
|
328
|
+
this.externalSearxngUrl = this.config.searxngUrl;
|
|
329
|
+
this.currentStatus = { state: 'active', url: this.config.searxngUrl, isFunctional: false };
|
|
330
|
+
this.initialized = true;
|
|
331
|
+
logger.log('[pi-research] External SEARXNG_URL configured — Docker management skipped:', this.config.searxngUrl);
|
|
332
|
+
this.notifyStatusChange();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
282
336
|
// Skip real Docker verification when a test/dummy manager is injected.
|
|
283
337
|
if (!this.config.manager) {
|
|
284
338
|
const dockerCheck = await verifyDockerInstalled();
|
|
@@ -396,6 +450,10 @@ export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
|
396
450
|
}
|
|
397
451
|
|
|
398
452
|
async ensureRunning(): Promise<string> {
|
|
453
|
+
if (this.externalSearxngUrl) {
|
|
454
|
+
return this.externalSearxngUrl;
|
|
455
|
+
}
|
|
456
|
+
|
|
399
457
|
if (!this.manager) {
|
|
400
458
|
throw new Error('SearXNG not initialized. Call initLifecycle() first.');
|
|
401
459
|
}
|
|
@@ -411,6 +469,15 @@ export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
|
411
469
|
}
|
|
412
470
|
|
|
413
471
|
async shutdown(): Promise<void> {
|
|
472
|
+
if (this.externalSearxngUrl) {
|
|
473
|
+
// External URL mode — no container to tear down
|
|
474
|
+
this.externalSearxngUrl = null;
|
|
475
|
+
this.currentStatus = { state: 'inactive', url: '', isFunctional: false };
|
|
476
|
+
this.initialized = false;
|
|
477
|
+
this.notifyStatusChange();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
414
481
|
if (!this.manager || !this.initialized) {
|
|
415
482
|
logger.log('[pi-research] SearXNG not active, skipping shutdown');
|
|
416
483
|
return;
|
|
@@ -454,7 +521,7 @@ export class SearxngLifecycleManager implements ISearxngLifecycleManager {
|
|
|
454
521
|
}
|
|
455
522
|
|
|
456
523
|
isInitialized(): boolean {
|
|
457
|
-
return this.initialized && this.manager !== null;
|
|
524
|
+
return this.initialized && (this.manager !== null || this.externalSearxngUrl !== null);
|
|
458
525
|
}
|
|
459
526
|
}
|
|
460
527
|
|
package/src/utils/text-utils.ts
CHANGED
|
@@ -79,5 +79,15 @@ export function ensureAssistantResponse(session: AgentSession, label: string): s
|
|
|
79
79
|
throw new Error(`${label}: Provider error - ${msg}`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const text = extractText(last);
|
|
83
|
+
if (!text.trim()) {
|
|
84
|
+
// No text content blocks in the final assistant message. This typically means
|
|
85
|
+
// all tool calls failed (e.g. SearXNG unreachable) and the model produced only
|
|
86
|
+
// thinking blocks, or the session ended without a visible response.
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${label}: Researcher produced no text output. ` +
|
|
89
|
+
`This usually means SearXNG was unavailable during the run — check Docker status and retry.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return text;
|
|
83
93
|
}
|