@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/config/searxng/default-settings.yml +189 -0
  4. package/config/searxng/limiter.toml +34 -0
  5. package/config/tooling/eslint.config.js +73 -0
  6. package/config/tooling/vitest.config.integration.ts +17 -0
  7. package/config/tooling/vitest.config.ts +40 -0
  8. package/config/tooling/vitest.config.unit.ts +12 -0
  9. package/docs/ARCHITECTURE.md +109 -0
  10. package/docs/assets/README-banner.jpg +0 -0
  11. package/package.json +92 -0
  12. package/scripts/setup.js +174 -0
  13. package/src/config.ts +206 -0
  14. package/src/constants.ts +148 -0
  15. package/src/healthcheck/index.ts +309 -0
  16. package/src/index.ts +71 -0
  17. package/src/infrastructure/searxng-lifecycle.ts +577 -0
  18. package/src/infrastructure/searxng-manager.ts +1173 -0
  19. package/src/infrastructure/state-manager.ts +918 -0
  20. package/src/logger.ts +246 -0
  21. package/src/orchestration/deep-research-orchestrator.ts +848 -0
  22. package/src/orchestration/deep-research-reducer.ts +129 -0
  23. package/src/orchestration/deep-research-types.ts +54 -0
  24. package/src/orchestration/id-utils.ts +63 -0
  25. package/src/orchestration/researcher.ts +104 -0
  26. package/src/orchestration/session-context.ts +43 -0
  27. package/src/prompts/research-tool-usage.md +45 -0
  28. package/src/prompts/researcher.md +166 -0
  29. package/src/prompts/system-coordinator.md +24 -0
  30. package/src/prompts/system-lead-evaluator.md +225 -0
  31. package/src/security/cisa-kev.ts +242 -0
  32. package/src/security/github-advisories.ts +607 -0
  33. package/src/security/index.ts +406 -0
  34. package/src/security/nvd.ts +532 -0
  35. package/src/security/osv.ts +487 -0
  36. package/src/security/types.ts +138 -0
  37. package/src/stackexchange/cache.ts +111 -0
  38. package/src/stackexchange/index.ts +352 -0
  39. package/src/stackexchange/output/compact.ts +64 -0
  40. package/src/stackexchange/output/index.ts +7 -0
  41. package/src/stackexchange/output/json.ts +21 -0
  42. package/src/stackexchange/output/table.ts +123 -0
  43. package/src/stackexchange/queries.ts +168 -0
  44. package/src/stackexchange/rest-client.ts +127 -0
  45. package/src/stackexchange/types.ts +129 -0
  46. package/src/tool.ts +430 -0
  47. package/src/tools/grep.ts +335 -0
  48. package/src/tools/index.ts +77 -0
  49. package/src/tools/scrape.ts +451 -0
  50. package/src/tools/search.ts +188 -0
  51. package/src/tools/security.ts +292 -0
  52. package/src/tools/stackexchange.ts +101 -0
  53. package/src/tui/research-panel.ts +576 -0
  54. package/src/tui/searxng-status.ts +80 -0
  55. package/src/types/extension-context.ts +60 -0
  56. package/src/types/llm.ts +111 -0
  57. package/src/utils/inject-date.ts +29 -0
  58. package/src/utils/input-validation.ts +103 -0
  59. package/src/utils/json-utils.ts +262 -0
  60. package/src/utils/make-resource-loader.ts +43 -0
  61. package/src/utils/research-error.ts +205 -0
  62. package/src/utils/research-export.ts +199 -0
  63. package/src/utils/searxng-config.ts +94 -0
  64. package/src/utils/session-state.ts +295 -0
  65. package/src/utils/shared-links.ts +123 -0
  66. package/src/utils/shutdown-manager.ts +54 -0
  67. package/src/utils/text-utils.ts +83 -0
  68. package/src/utils/tool-usage-tracker.ts +135 -0
  69. package/src/web-research/dependencies.ts +20 -0
  70. package/src/web-research/retry-utils.ts +232 -0
  71. package/src/web-research/scrapers.ts +935 -0
  72. package/src/web-research/search.ts +235 -0
  73. package/src/web-research/types.ts +47 -0
  74. package/src/web-research/utils.ts +158 -0
  75. package/tsconfig.json +40 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * rg_grep Tool (Ripgrep)
3
+ *
4
+ * Standalone grep tool using ripgrep (rg) or fallback to grep.
5
+ */
6
+
7
+ import { spawn } from 'node:child_process';
8
+ import * as nodePath from 'node:path';
9
+ import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
10
+ import { Type } from '@sinclair/typebox';
11
+ import type { ToolUsageTracker } from '../utils/tool-usage-tracker.ts';
12
+
13
+ const DEFAULT_MAX_BYTES = 100 * 1024; // 100KB
14
+ const DEFAULT_MAX_LINES = 200;
15
+ const STREAM_MAX_BYTES = 10 * 1024 * 1024; // 10MB max in stream (prevent RangeError)
16
+
17
+ interface RgGrepParams {
18
+ pattern: string;
19
+ path?: string;
20
+ flags?: string;
21
+ }
22
+
23
+ function truncateHead(content: string, maxBytes: number, maxLines: number): string {
24
+ const lines = content.split('\n');
25
+
26
+ if (lines.length <= maxLines && Buffer.byteLength(content, 'utf-8') <= maxBytes) {
27
+ return content;
28
+ }
29
+
30
+ const truncatedLines = lines.slice(0, maxLines);
31
+ let result = truncatedLines.join('\n');
32
+
33
+ if (Buffer.byteLength(result, 'utf-8') > maxBytes) {
34
+ let byteCount = 0;
35
+ const charArray: string[] = [];
36
+
37
+ for (const char of result) {
38
+ byteCount += Buffer.byteLength(char, 'utf-8');
39
+ if (byteCount > maxBytes) break;
40
+ charArray.push(char);
41
+ }
42
+
43
+ result = charArray.join('');
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ async function execCommand(
50
+ command: string,
51
+ args: string[],
52
+ ): Promise<{ stdout: string; stderr: string; exitCode: number | null; wasTruncated: boolean }> {
53
+ return new Promise((resolve, reject) => {
54
+ const child = spawn(command, args);
55
+ let stdout = '';
56
+ let stderr = '';
57
+ let wasTruncated = false;
58
+
59
+ child.stdout?.on('data', (data) => {
60
+ // Check if adding this chunk would exceed stream limit
61
+ const chunk = data.toString();
62
+ if (stdout.length + chunk.length > STREAM_MAX_BYTES) {
63
+ // Stop collecting data to prevent RangeError
64
+ wasTruncated = true;
65
+ child.kill();
66
+ return;
67
+ }
68
+ stdout += chunk;
69
+ });
70
+
71
+ child.stderr?.on('data', (data) => {
72
+ stderr += data.toString();
73
+ });
74
+
75
+ child.on('close', (code) => {
76
+ resolve({ stdout, stderr, exitCode: code, wasTruncated });
77
+ });
78
+
79
+ child.on('error', (error) => {
80
+ reject(error);
81
+ });
82
+ });
83
+ }
84
+
85
+ function resolveWorkspacePath(input: string): string {
86
+ const root = process.cwd();
87
+ const resolved = nodePath.resolve(root, input);
88
+ if (resolved !== root && !resolved.startsWith(root + nodePath.sep)) {
89
+ throw new Error(`Path outside workspace: ${input}`);
90
+ }
91
+ return resolved;
92
+ }
93
+
94
+ function sanitizeInput(input: string): string[] {
95
+ // Simple sanitization: split on whitespace, preserve quoted strings
96
+ const result: string[] = [];
97
+ let current = '';
98
+ let inQuotes = false;
99
+
100
+ for (const char of input) {
101
+ if (char === '"' || char === "'") {
102
+ inQuotes = !inQuotes;
103
+ } else if (char === ' ' && !inQuotes) {
104
+ if (current) {
105
+ result.push(current);
106
+ current = '';
107
+ }
108
+ } else {
109
+ current += char;
110
+ }
111
+ }
112
+
113
+ if (current) {
114
+ result.push(current);
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ function normalizeSearchOutput(output: string): string {
121
+ return process.platform === 'win32' ? output.replace(/\\/g, '/') : output;
122
+ }
123
+
124
+ export function createGrepTool(options: {
125
+ tracker: ToolUsageTracker;
126
+ }): ToolDefinition {
127
+ return {
128
+ name: 'grep',
129
+ label: 'Code Search',
130
+ description: 'Search codebase using ripgrep (rg) or grep fallback. Fast recursive text search.',
131
+ promptSnippet: 'Search codebase using ripgrep/grep',
132
+ promptGuidelines: [
133
+ 'Use grep for fast recursive text search in codebases.',
134
+ 'Pattern supports regex. Path and flags are optional.',
135
+ 'Falls back to grep if rg is not available.',
136
+ 'CRITICAL: You are allowed a maximum of 4 gathering calls total across ALL tools. Use them for breadth.',
137
+ ],
138
+ parameters: Type.Object({
139
+ pattern: Type.String({ description: 'Search pattern (regex supported)' }),
140
+ path: Type.Optional(Type.String({ description: 'Directory to search (default: current directory)' })),
141
+ flags: Type.Optional(Type.String({ description: 'Additional flags (e.g., "-i" for case-insensitive)' })),
142
+ }),
143
+ async execute(
144
+ _toolCallId: string,
145
+ params: unknown,
146
+ _signal: unknown,
147
+ _onUpdate: unknown,
148
+ _ctx: ExtensionContext,
149
+ ): Promise<AgentToolResult<unknown>> {
150
+ // Record call in tracker - returns false if limit reached
151
+ const allowed = options.tracker.recordCall('grep');
152
+ if (!allowed) {
153
+ // THROW to prevent researcher from calling again
154
+ throw new Error(options.tracker.getLimitMessage('grep'));
155
+ }
156
+
157
+ const record = params as RgGrepParams;
158
+ const { pattern } = record;
159
+ const path = resolveWorkspacePath(record.path ?? '.');
160
+ const flags = record.flags ?? '';
161
+
162
+ if (!pattern) {
163
+ return {
164
+ content: [{ type: 'text', text: 'Error: pattern is required' }],
165
+ details: {},
166
+ };
167
+ }
168
+
169
+ // Sanitize inputs to prevent shell injection
170
+ const flagParts = flags ? sanitizeInput(flags) : [];
171
+
172
+ // Try rg first
173
+ try {
174
+ const rgArgs = ['--no-heading', '-n', ...flagParts, pattern, path];
175
+ const { stdout, stderr, exitCode, wasTruncated } = await execCommand('rg', rgArgs);
176
+
177
+ const truncated = truncateHead(normalizeSearchOutput(stdout), DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES);
178
+
179
+ let markdown = '# Search Results (rg)\n\n';
180
+ markdown += `**Pattern:** \`${pattern}\`\n`;
181
+ markdown += `**Path:** \`${path}\`\n`;
182
+ if (flags) markdown += `**Flags:** \`${flags}\`\n`;
183
+ markdown += `**Exit Code:** ${exitCode}\n\n`;
184
+
185
+ if (wasTruncated) {
186
+ markdown += '⚠️ **Output was truncated to prevent memory overflow (>10MB)**\n\n';
187
+ }
188
+
189
+ if (truncated) {
190
+ markdown += '```\n' + truncated + '\n```';
191
+ } else {
192
+ markdown += 'No matches found.';
193
+ }
194
+
195
+ if (stderr) {
196
+ markdown += '\n\n**Stderr:**\n```\n' + stderr + '\n```';
197
+ }
198
+
199
+ return {
200
+ content: [{ type: 'text', text: markdown }],
201
+ details: {
202
+ command: 'rg',
203
+ args: rgArgs,
204
+ exitCode,
205
+ wasTruncated,
206
+ },
207
+ };
208
+ } catch (rgError) {
209
+ // rg not found, try grep
210
+ try {
211
+ const grepArgs = ['-rn', ...flagParts, pattern, path];
212
+ const { stdout, stderr, exitCode, wasTruncated } = await execCommand('grep', grepArgs);
213
+
214
+ const truncated = truncateHead(normalizeSearchOutput(stdout), DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES);
215
+
216
+ let markdown = '# Search Results (grep)\n\n';
217
+ markdown += `**Pattern:** \`${pattern}\`\n`;
218
+ markdown += `**Path:** \`${path}\`\n`;
219
+ if (flags) markdown += `**Flags:** \`${flags}\`\n`;
220
+ markdown += `**Exit Code:** ${exitCode}\n\n`;
221
+ markdown += '*Note: Using grep fallback (rg not available)*\n\n';
222
+
223
+ if (wasTruncated) {
224
+ markdown += '⚠️ **Output was truncated to prevent memory overflow (>10MB)**\n\n';
225
+ }
226
+
227
+ if (truncated) {
228
+ markdown += '```\n' + truncated + '\n```';
229
+ } else {
230
+ markdown += 'No matches found.';
231
+ }
232
+
233
+ if (stderr) {
234
+ markdown += '\n\n**Stderr:**\n```\n' + stderr + '\n```';
235
+ }
236
+
237
+ return {
238
+ content: [{ type: 'text', text: markdown }],
239
+ details: {
240
+ command: 'grep',
241
+ args: grepArgs,
242
+ exitCode,
243
+ wasTruncated,
244
+ },
245
+ };
246
+ } catch (grepError) {
247
+ return {
248
+ content: [
249
+ {
250
+ type: 'text',
251
+ text: `Error: Neither rg nor grep is available.\n\nrg error: ${rgError}\ngrep error: ${grepError}`,
252
+ },
253
+ ],
254
+ details: {},
255
+ };
256
+ }
257
+ }
258
+ },
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Standalone function to execute rg/grep search
264
+ */
265
+ export async function grep(pattern: string, path: string = '.', flags: string = ''): Promise<string> {
266
+ if (!pattern) {
267
+ throw new Error('Pattern is required');
268
+ }
269
+
270
+ const resolvedPath = resolveWorkspacePath(path);
271
+ const flagParts = flags ? sanitizeInput(flags) : [];
272
+
273
+ // Try rg first
274
+ try {
275
+ const rgArgs = ['--no-heading', '-n', ...flagParts, pattern, resolvedPath];
276
+ const { stdout, stderr, exitCode, wasTruncated } = await execCommand('rg', rgArgs);
277
+
278
+ let markdown = '# Search Results (rg)\n\n';
279
+ markdown += `**Pattern:** \`${pattern}\`\n`;
280
+ markdown += `**Path:** \`${resolvedPath}\`\n`;
281
+ if (flags) markdown += `**Flags:** \`${flags}\`\n`;
282
+ markdown += `**Exit Code:** ${exitCode}\n\n`;
283
+
284
+ const truncated = truncateHead(normalizeSearchOutput(stdout), DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES);
285
+
286
+ if (wasTruncated) {
287
+ markdown += '⚠️ **Output was truncated to prevent memory overflow (>10MB)**\n\n';
288
+ }
289
+
290
+ if (truncated) {
291
+ markdown += '```\n' + truncated + '\n```';
292
+ } else {
293
+ markdown += 'No matches found.';
294
+ }
295
+
296
+ if (stderr) {
297
+ markdown += '\n\n**Stderr:**\n```\n' + stderr + '\n```';
298
+ }
299
+
300
+ return markdown;
301
+ } catch (rgError) {
302
+ // rg not found, try grep
303
+ try {
304
+ const grepArgs = ['-rn', ...flagParts, pattern, resolvedPath];
305
+ const { stdout, stderr, exitCode, wasTruncated } = await execCommand('grep', grepArgs);
306
+
307
+ let markdown = '# Search Results (grep)\n\n';
308
+ markdown += `**Pattern:** \`${pattern}\`\n`;
309
+ markdown += `**Path:** \`${resolvedPath}\`\n`;
310
+ if (flags) markdown += `**Flags:** \`${flags}\`\n`;
311
+ markdown += `**Exit Code:** ${exitCode}\n\n`;
312
+ markdown += '*Note: Using grep fallback (rg not available)*\n\n';
313
+
314
+ const truncated = truncateHead(normalizeSearchOutput(stdout), DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES);
315
+
316
+ if (wasTruncated) {
317
+ markdown += '⚠️ **Output was truncated to prevent memory overflow (>10MB)**\n\n';
318
+ }
319
+
320
+ if (truncated) {
321
+ markdown += '```\n' + truncated + '\n```';
322
+ } else {
323
+ markdown += 'No matches found.';
324
+ }
325
+
326
+ if (stderr) {
327
+ markdown += '\n\n**Stderr:**\n```\n' + stderr + '\n```';
328
+ }
329
+
330
+ return markdown;
331
+ } catch (grepError) {
332
+ return `Error: Neither rg nor grep is available.\n\nrg error: ${rgError}\ngrep error: ${grepError}`;
333
+ }
334
+ }
335
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tool Factory
3
+ *
4
+ * Creates all research tools with properly configured SearXNG URL.
5
+ * These tools are used by both direct tool calls and researcher agents.
6
+ */
7
+
8
+ import type { ToolDefinition, ExtensionContext } from '@mariozechner/pi-coding-agent';
9
+ import type { ToolUsageTracker } from '../utils/tool-usage-tracker.ts';
10
+ import type { SystemResearchState } from '../orchestration/deep-research-types.ts';
11
+ import { createSearchTool } from './search.ts';
12
+ import { createScrapeTool } from './scrape.ts';
13
+ import { createSecuritySearchTool } from './security.ts';
14
+ import { createStackexchangeTool } from './stackexchange.ts';
15
+ import { createGrepTool } from './grep.ts';
16
+
17
+ interface CreateToolsOptions {
18
+ searxngUrl: string;
19
+ ctx: ExtensionContext;
20
+ tracker: ToolUsageTracker;
21
+ getGlobalState?: () => SystemResearchState;
22
+ updateGlobalLinks?: (links: string[]) => void;
23
+ /** Returns tokens consumed by this researcher session so far (for context-aware scrape gating). */
24
+ getTokensUsed?: () => number;
25
+ /** Model context window size in tokens; defaults to DEFAULT_MODEL_CONTEXT_WINDOW. */
26
+ contextWindowSize?: number;
27
+ }
28
+
29
+ /**
30
+ * Create all research tools
31
+ */
32
+ export function createResearchTools(options: CreateToolsOptions): ToolDefinition[] {
33
+ // Create a fresh fallback state per tool instance to avoid state leakage between research runs
34
+ // This is only used when getGlobalState is not provided (e.g., quick mode with single researcher)
35
+ const createFallbackState = (): SystemResearchState => ({
36
+ version: 1,
37
+ rootQuery: '',
38
+ complexity: 1,
39
+ currentRound: 1,
40
+ status: 'researching',
41
+ lastUpdated: Date.now(),
42
+ initialAgenda: [],
43
+ allScrapedLinks: [],
44
+ aspects: {},
45
+ });
46
+
47
+ const fallbackState = createFallbackState();
48
+
49
+ const resolvedOptions = {
50
+ ...options,
51
+ getGlobalState: options.getGlobalState ?? (() => fallbackState),
52
+ updateGlobalLinks: options.updateGlobalLinks ?? ((links: string[]) => {
53
+ fallbackState.allScrapedLinks = [...new Set([...fallbackState.allScrapedLinks, ...links])];
54
+ }),
55
+ };
56
+
57
+ return [
58
+ createSearchTool(resolvedOptions),
59
+ createScrapeTool({
60
+ ...resolvedOptions,
61
+ getTokensUsed: options.getTokensUsed,
62
+ contextWindowSize: options.contextWindowSize,
63
+ }),
64
+ createSecuritySearchTool(resolvedOptions),
65
+ createStackexchangeTool(resolvedOptions),
66
+ createGrepTool(resolvedOptions),
67
+ ];
68
+ }
69
+
70
+ /**
71
+ * Create individual tool exports for direct registration
72
+ */
73
+ export { createSearchTool } from './search.ts';
74
+ export { createScrapeTool } from './scrape.ts';
75
+ export { createSecuritySearchTool } from './security.ts';
76
+ export { createStackexchangeTool } from './stackexchange.ts';
77
+ export { createGrepTool } from './grep.ts';