@lincoln504/pi-research 0.0.0
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/LICENSE +21 -0
- package/README.md +131 -0
- package/config/searxng/default-settings.yml +189 -0
- package/config/searxng/limiter.toml +34 -0
- package/config/tooling/eslint.config.js +73 -0
- package/config/tooling/vitest.config.integration.ts +17 -0
- package/config/tooling/vitest.config.ts +40 -0
- package/config/tooling/vitest.config.unit.ts +12 -0
- package/docs/ARCHITECTURE.md +109 -0
- package/docs/assets/README-banner.jpg +0 -0
- package/package.json +92 -0
- package/scripts/setup.js +174 -0
- package/src/config.ts +206 -0
- package/src/constants.ts +148 -0
- package/src/healthcheck/index.ts +309 -0
- package/src/index.ts +71 -0
- package/src/infrastructure/searxng-lifecycle.ts +577 -0
- package/src/infrastructure/searxng-manager.ts +1173 -0
- package/src/infrastructure/state-manager.ts +918 -0
- package/src/logger.ts +246 -0
- package/src/orchestration/deep-research-orchestrator.ts +848 -0
- package/src/orchestration/deep-research-reducer.ts +129 -0
- package/src/orchestration/deep-research-types.ts +54 -0
- package/src/orchestration/id-utils.ts +63 -0
- package/src/orchestration/researcher.ts +104 -0
- package/src/orchestration/session-context.ts +43 -0
- package/src/prompts/research-tool-usage.md +45 -0
- package/src/prompts/researcher.md +166 -0
- package/src/prompts/system-coordinator.md +24 -0
- package/src/prompts/system-lead-evaluator.md +225 -0
- package/src/security/cisa-kev.ts +242 -0
- package/src/security/github-advisories.ts +607 -0
- package/src/security/index.ts +406 -0
- package/src/security/nvd.ts +532 -0
- package/src/security/osv.ts +487 -0
- package/src/security/types.ts +138 -0
- package/src/stackexchange/cache.ts +111 -0
- package/src/stackexchange/index.ts +352 -0
- package/src/stackexchange/output/compact.ts +64 -0
- package/src/stackexchange/output/index.ts +7 -0
- package/src/stackexchange/output/json.ts +21 -0
- package/src/stackexchange/output/table.ts +123 -0
- package/src/stackexchange/queries.ts +168 -0
- package/src/stackexchange/rest-client.ts +127 -0
- package/src/stackexchange/types.ts +129 -0
- package/src/tool.ts +430 -0
- package/src/tools/grep.ts +335 -0
- package/src/tools/index.ts +77 -0
- package/src/tools/scrape.ts +451 -0
- package/src/tools/search.ts +188 -0
- package/src/tools/security.ts +292 -0
- package/src/tools/stackexchange.ts +101 -0
- package/src/tui/research-panel.ts +576 -0
- package/src/tui/searxng-status.ts +80 -0
- package/src/types/extension-context.ts +60 -0
- package/src/types/llm.ts +111 -0
- package/src/utils/inject-date.ts +29 -0
- package/src/utils/input-validation.ts +103 -0
- package/src/utils/json-utils.ts +262 -0
- package/src/utils/make-resource-loader.ts +43 -0
- package/src/utils/research-error.ts +205 -0
- package/src/utils/research-export.ts +199 -0
- package/src/utils/searxng-config.ts +94 -0
- package/src/utils/session-state.ts +295 -0
- package/src/utils/shared-links.ts +123 -0
- package/src/utils/shutdown-manager.ts +54 -0
- package/src/utils/text-utils.ts +83 -0
- package/src/utils/tool-usage-tracker.ts +135 -0
- package/src/web-research/dependencies.ts +20 -0
- package/src/web-research/retry-utils.ts +232 -0
- package/src/web-research/scrapers.ts +935 -0
- package/src/web-research/search.ts +235 -0
- package/src/web-research/types.ts +47 -0
- package/src/web-research/utils.ts +158 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* QUICK connectivity validation for search and scrape tools.
|
|
5
|
+
* Makes real requests to verify network is alive.
|
|
6
|
+
* Fast timeouts, simple checks (just verify tools respond with content).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../logger.ts';
|
|
10
|
+
import { getConfig } from '../config.ts';
|
|
11
|
+
import { getManager } from '../infrastructure/searxng-lifecycle.ts';
|
|
12
|
+
import { search } from '../web-research/search.ts';
|
|
13
|
+
import { scrapeSingle } from '../web-research/scrapers.ts';
|
|
14
|
+
import { setSearxngManager } from '../web-research/utils.ts';
|
|
15
|
+
import { getActiveSearxngEngines } from '../utils/searxng-config.ts';
|
|
16
|
+
import { withTimeout } from '../web-research/retry-utils.ts';
|
|
17
|
+
import type { SearXNGResult } from '../web-research/types.ts';
|
|
18
|
+
|
|
19
|
+
// Health check cache to avoid running multiple health checks concurrently
|
|
20
|
+
interface CachedHealthCheck {
|
|
21
|
+
result: HealthCheckResult;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedHealthCheck: CachedHealthCheck | null = null;
|
|
26
|
+
const HEALTH_CHECK_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
27
|
+
let healthCheckPromise: Promise<HealthCheckResult> | null = null;
|
|
28
|
+
|
|
29
|
+
// Get timeout from config or use defaults
|
|
30
|
+
const config = getConfig();
|
|
31
|
+
const SEARCH_TIMEOUT_MS = config.HEALTH_CHECK_TIMEOUT_MS || 15000; // 15s max for search
|
|
32
|
+
const SCRAPE_TIMEOUT_MS = config.HEALTH_CHECK_TIMEOUT_MS ? config.HEALTH_CHECK_TIMEOUT_MS + 5000 : 20000; // 5s longer than search
|
|
33
|
+
|
|
34
|
+
export interface HealthCheckResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
searchOk: boolean;
|
|
37
|
+
scrapeOk: boolean;
|
|
38
|
+
error?: string;
|
|
39
|
+
details: {
|
|
40
|
+
searchQuery?: string;
|
|
41
|
+
searchResultCount?: number;
|
|
42
|
+
searchDurationMs?: number;
|
|
43
|
+
workingEngines?: string[];
|
|
44
|
+
workingEngineCount?: number;
|
|
45
|
+
scrapedUrl?: string;
|
|
46
|
+
scrapedContentLength?: number;
|
|
47
|
+
scrapedDurationMs?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* QUICK scrape validation: just check content exists and is readable
|
|
53
|
+
*/
|
|
54
|
+
function validateScrapeOutput(markdown: string): { valid: boolean; error?: string } {
|
|
55
|
+
if (typeof markdown !== 'string') {
|
|
56
|
+
return { valid: false, error: 'Content not a string' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const trimmed = markdown.trim();
|
|
60
|
+
if (trimmed.length === 0) {
|
|
61
|
+
return { valid: false, error: 'Content is empty' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Just check it has some readable text (not garbage)
|
|
65
|
+
if (!/[a-zA-Z0-9]{10,}/.test(markdown)) {
|
|
66
|
+
return { valid: false, error: 'Content not readable' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Healthcheck uses the same web-research search/scrape helpers as the main tool.
|
|
74
|
+
* Ensure they are wired to the current lifecycle manager instead of relying on
|
|
75
|
+
* an external caller to have registered it beforehand.
|
|
76
|
+
*/
|
|
77
|
+
function ensureHealthcheckManager(): void {
|
|
78
|
+
const manager = getManager();
|
|
79
|
+
if (!manager) {
|
|
80
|
+
throw new Error('SearXNG manager not initialized. Call initLifecycle() before runHealthCheck().');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setSearxngManager(manager);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Internal health check implementation
|
|
88
|
+
* Runs the actual health check tests
|
|
89
|
+
*/
|
|
90
|
+
async function performHealthCheck(): Promise<HealthCheckResult> {
|
|
91
|
+
logger.log('[healthcheck] Starting network connectivity health check...');
|
|
92
|
+
|
|
93
|
+
const result: HealthCheckResult = {
|
|
94
|
+
success: false,
|
|
95
|
+
searchOk: false,
|
|
96
|
+
scrapeOk: false,
|
|
97
|
+
error: undefined,
|
|
98
|
+
details: {},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Skip health check if explicitly requested via env
|
|
103
|
+
if (process.env['PI_RESEARCH_SKIP_HEALTHCHECK'] === '1') {
|
|
104
|
+
logger.log('[healthcheck] Skipping health check (PI_RESEARCH_SKIP_HEALTHCHECK=1)');
|
|
105
|
+
result.success = true;
|
|
106
|
+
result.searchOk = true;
|
|
107
|
+
result.scrapeOk = true;
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ensureHealthcheckManager();
|
|
112
|
+
|
|
113
|
+
// PHASE 1: Test general web search engines (Bing, DuckDuckGo, Brave — NOT Wikipedia)
|
|
114
|
+
logger.log('[healthcheck] Phase 1: Testing general web search engines (timeout: ' + SEARCH_TIMEOUT_MS + 'ms)...');
|
|
115
|
+
const searchQuery = 'open source software';
|
|
116
|
+
const searchStartTime = Date.now();
|
|
117
|
+
|
|
118
|
+
let queryResults;
|
|
119
|
+
try {
|
|
120
|
+
queryResults = await withTimeout(
|
|
121
|
+
search([searchQuery]),
|
|
122
|
+
SEARCH_TIMEOUT_MS,
|
|
123
|
+
'Search'
|
|
124
|
+
);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
result.error = `Search request failed: ${msg}. Network may be unreachable or SearXNG not responding.`;
|
|
128
|
+
logger.error('[healthcheck] Search error:', result.error);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const searchDurationMs = Date.now() - searchStartTime;
|
|
133
|
+
|
|
134
|
+
if (queryResults.length === 0 || !queryResults[0]) {
|
|
135
|
+
result.error = 'Search returned empty result object (SearXNG not initialized?)';
|
|
136
|
+
logger.error('[healthcheck]', result.error);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const qr = queryResults[0];
|
|
141
|
+
if (qr.error) {
|
|
142
|
+
const errMsg = qr.error.message || String(qr.error);
|
|
143
|
+
result.error = `Search request failed: ${errMsg}. SearXNG or engines are not responding.`;
|
|
144
|
+
logger.error('[healthcheck]', result.error);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check for at least 2 working general search engines (not Wikipedia)
|
|
149
|
+
// Wikipedia is an encyclopedic engine, not suitable for general web research
|
|
150
|
+
// Load the list of active engines from the actual SearXNG config (not hardcoded)
|
|
151
|
+
const generalEngines = getActiveSearxngEngines();
|
|
152
|
+
if (generalEngines.length === 0) {
|
|
153
|
+
result.error = 'No active general search engines found in SearXNG configuration';
|
|
154
|
+
logger.error('[healthcheck]', result.error);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
logger.log(`[healthcheck] Checking for results from engines: [${generalEngines.join(', ')}]`);
|
|
158
|
+
|
|
159
|
+
const generalResults = qr.results.filter((r: SearXNGResult) =>
|
|
160
|
+
generalEngines.includes((r.engine || '').toLowerCase())
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Count how many engines returned at least one result
|
|
164
|
+
const engineCounts = new Map<string, number>();
|
|
165
|
+
generalEngines.forEach(e => engineCounts.set(e, 0));
|
|
166
|
+
generalResults.forEach((r: SearXNGResult) => {
|
|
167
|
+
const engine = (r.engine || '').toLowerCase();
|
|
168
|
+
if (engineCounts.has(engine)) {
|
|
169
|
+
engineCounts.set(engine, (engineCounts.get(engine) || 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const workingEngines = Array.from(engineCounts.entries()).filter(([_, count]) => count > 0);
|
|
174
|
+
const workingEngineCount = workingEngines.length;
|
|
175
|
+
|
|
176
|
+
// Require at least 1 engine to return results (local use may have rate limits)
|
|
177
|
+
if (workingEngineCount < 1) {
|
|
178
|
+
const engineReport = Array.from(engineCounts.entries())
|
|
179
|
+
.map(([e, c]) => `${e}: ${c}`)
|
|
180
|
+
.join(', ');
|
|
181
|
+
result.error = `No working engines found: 0/${generalEngines.length} returned results. Need at least 1 engine. Engine results: ${engineReport}. Your network or search engines may be blocked.`;
|
|
182
|
+
logger.error('[healthcheck] Search validation failed:', result.error);
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
result.searchOk = true;
|
|
187
|
+
result.details.searchQuery = searchQuery;
|
|
188
|
+
result.details.searchResultCount = generalResults.length;
|
|
189
|
+
result.details.searchDurationMs = searchDurationMs;
|
|
190
|
+
result.details.workingEngines = workingEngines.map(([e, _]) => e);
|
|
191
|
+
result.details.workingEngineCount = workingEngineCount;
|
|
192
|
+
const workingEnginesList = workingEngines.map(([e, _]) => e).join(', ');
|
|
193
|
+
logger.log(`[healthcheck] ✓ Search OK: ${generalResults.length} results from ${workingEngineCount} engines [${workingEnginesList}] in ${searchDurationMs}ms`);
|
|
194
|
+
|
|
195
|
+
// PHASE 2: Test scrape with timeout enforcement
|
|
196
|
+
logger.log('[healthcheck] Phase 2: Testing scrape tool (timeout: ' + SCRAPE_TIMEOUT_MS + 'ms)...');
|
|
197
|
+
const scrapeUrl = 'https://en.wikipedia.org/wiki/Python_(programming_language)';
|
|
198
|
+
const scrapeStartTime = Date.now();
|
|
199
|
+
|
|
200
|
+
let scrapeResult;
|
|
201
|
+
try {
|
|
202
|
+
scrapeResult = await withTimeout(
|
|
203
|
+
scrapeSingle(scrapeUrl),
|
|
204
|
+
SCRAPE_TIMEOUT_MS,
|
|
205
|
+
'Scrape'
|
|
206
|
+
);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
209
|
+
result.error = `Scrape request failed: ${msg}. Network timeout or target unreachable.`;
|
|
210
|
+
logger.error('[healthcheck]', result.error);
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const scrapeDurationMs = Date.now() - scrapeStartTime;
|
|
215
|
+
|
|
216
|
+
if (!scrapeResult) {
|
|
217
|
+
result.error = 'Scrape returned null result (scraper crashed?)';
|
|
218
|
+
logger.error('[healthcheck]', result.error);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (scrapeResult.source === 'failed') {
|
|
223
|
+
result.error = `Scrape failed: ${scrapeResult.error || 'unknown error'}. Check if target URL is accessible.`;
|
|
224
|
+
logger.error('[healthcheck]', result.error);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ROBUST validation of scraped content
|
|
229
|
+
const markdown = scrapeResult.markdown || '';
|
|
230
|
+
const scrapeValidation = validateScrapeOutput(markdown);
|
|
231
|
+
if (!scrapeValidation.valid) {
|
|
232
|
+
result.error = `Scrape validation failed: ${scrapeValidation.error}`;
|
|
233
|
+
logger.error('[healthcheck]', result.error);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
result.scrapeOk = true;
|
|
238
|
+
result.details.scrapedUrl = scrapeUrl;
|
|
239
|
+
result.details.scrapedContentLength = markdown.length;
|
|
240
|
+
result.details.scrapedDurationMs = scrapeDurationMs;
|
|
241
|
+
logger.log(`[healthcheck] ✓ Scrape OK: ${markdown.length} bytes in ${scrapeDurationMs}ms`);
|
|
242
|
+
|
|
243
|
+
// All checks passed
|
|
244
|
+
result.success = true;
|
|
245
|
+
const totalMs = searchDurationMs + scrapeDurationMs;
|
|
246
|
+
logger.log(`[healthcheck] ✓ HEALTH CHECK PASSED in ${totalMs}ms - all systems ready`);
|
|
247
|
+
return result;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
result.error = `Unexpected error during health check: ${err instanceof Error ? err.message : String(err)}`;
|
|
250
|
+
logger.error('[healthcheck] Unexpected error:', err);
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run health check with caching to avoid concurrent executions
|
|
257
|
+
* If a health check is already in progress, returns the same promise
|
|
258
|
+
* If a recent successful health check is cached, returns the cached result
|
|
259
|
+
*/
|
|
260
|
+
export async function runHealthCheck(): Promise<HealthCheckResult> {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
|
|
263
|
+
// Check if we have a recent cached result
|
|
264
|
+
if (cachedHealthCheck) {
|
|
265
|
+
const age = now - cachedHealthCheck.timestamp;
|
|
266
|
+
if (age < HEALTH_CHECK_CACHE_TTL_MS) {
|
|
267
|
+
logger.log(`[healthcheck] Using cached health check result (${Math.round(age / 1000)}s old)`);
|
|
268
|
+
return cachedHealthCheck.result;
|
|
269
|
+
} else {
|
|
270
|
+
logger.log(`[healthcheck] Cached health check result expired (${Math.round(age / 1000)}s old)`);
|
|
271
|
+
cachedHealthCheck = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// If a health check is already running, return the existing promise
|
|
276
|
+
if (healthCheckPromise) {
|
|
277
|
+
logger.log('[healthcheck] Health check already in progress, reusing existing promise');
|
|
278
|
+
return healthCheckPromise;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Start a new health check
|
|
282
|
+
healthCheckPromise = (async () => {
|
|
283
|
+
try {
|
|
284
|
+
const result = await performHealthCheck();
|
|
285
|
+
// Cache successful results
|
|
286
|
+
if (result.success) {
|
|
287
|
+
cachedHealthCheck = {
|
|
288
|
+
result,
|
|
289
|
+
timestamp: now,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
} finally {
|
|
294
|
+
// Clear the promise so a new health check can be started
|
|
295
|
+
healthCheckPromise = null;
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
298
|
+
|
|
299
|
+
return healthCheckPromise;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Clear the health check cache
|
|
304
|
+
* Useful for testing or forcing a recheck
|
|
305
|
+
*/
|
|
306
|
+
export function clearHealthCheckCache(): void {
|
|
307
|
+
cachedHealthCheck = null;
|
|
308
|
+
logger.log('[healthcheck] Health check cache cleared');
|
|
309
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-research Extension
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates multi-agent research using a state-driven deep mode architecture.
|
|
5
|
+
* Manages SearXNG as singleton (shared across all agents).
|
|
6
|
+
* SearXNG is initialized on first research() call, NOT on session_start.
|
|
7
|
+
* Extension-owned cleanup is delegated to pi's session lifecycle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
11
|
+
import { createResearchTool } from './tool.ts';
|
|
12
|
+
import { logger, isVerboseFromEnv, getDefaultDebugLogPathTemplate } from './logger.ts';
|
|
13
|
+
import { checkDockerAvailability } from './infrastructure/searxng-lifecycle.ts';
|
|
14
|
+
import { shutdownManager } from './utils/shutdown-manager.ts';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load prompt from markdown file
|
|
24
|
+
*/
|
|
25
|
+
function loadPrompt(name: string): string {
|
|
26
|
+
const path = join(__dirname, 'prompts', `${name}.md`);
|
|
27
|
+
try {
|
|
28
|
+
return readFileSync(path, 'utf-8');
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
31
|
+
logger.error(`[pi-research] Failed to load prompt from ${path}: ${msg}`);
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function (pi: ExtensionAPI) {
|
|
37
|
+
logger.log('[pi-research] Extension loading...');
|
|
38
|
+
|
|
39
|
+
// NOTE: SearXNG is still initialized lazily on first research() call in tool.ts.
|
|
40
|
+
// Cleanup runs on pi's session lifecycle instead of owning host process signals.
|
|
41
|
+
|
|
42
|
+
pi.on('session_shutdown', async (_event, _ctx) => {
|
|
43
|
+
await shutdownManager.runCleanup('session_shutdown');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Notify user of Docker status and log file location when starting a new session
|
|
47
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
48
|
+
// Check Docker availability on startup
|
|
49
|
+
const dockerCheck = await checkDockerAvailability();
|
|
50
|
+
if (!dockerCheck.running) {
|
|
51
|
+
ctx.ui.notify(`pi-research: ${dockerCheck.error}`, 'warning');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isVerboseFromEnv()) {
|
|
55
|
+
ctx.ui.notify(`pi-research: debug log → ${getDefaultDebugLogPathTemplate()}`, 'info');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Register research tool
|
|
60
|
+
pi.registerTool(createResearchTool());
|
|
61
|
+
|
|
62
|
+
// Append research tool usage instructions to the system prompt
|
|
63
|
+
pi.on('before_agent_start', async (event) => {
|
|
64
|
+
const researchPrompt = loadPrompt('research-tool-usage');
|
|
65
|
+
return {
|
|
66
|
+
systemPrompt: event.systemPrompt + '\n\n' + researchPrompt
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
logger.log('[pi-research] Extension loaded');
|
|
71
|
+
}
|