@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,168 @@
1
+ /**
2
+ * Stack Exchange API Query Builders
3
+ */
4
+
5
+ import type { SortOrder, SortActivity, SortReputation } from './types.ts';
6
+
7
+ export interface SearchParams {
8
+ query?: string;
9
+ title?: string;
10
+ tagged?: string | string[];
11
+ nottagged?: string | string[];
12
+ intitle?: string;
13
+ accepted?: boolean;
14
+ answers?: number;
15
+ body?: string;
16
+ closed?: boolean;
17
+ migrated?: boolean;
18
+ notice?: string;
19
+ user?: number;
20
+ url?: string;
21
+ views?: number;
22
+ wiki?: boolean;
23
+ page?: number;
24
+ pagesize?: number;
25
+ sort?: SortActivity;
26
+ order?: SortOrder;
27
+ site?: string;
28
+ filter?: string;
29
+ }
30
+
31
+ export interface QuestionParams {
32
+ ids: string | number | (string | number)[];
33
+ site?: string;
34
+ filter?: string;
35
+ sort?: SortActivity;
36
+ order?: SortOrder;
37
+ page?: number;
38
+ pagesize?: number;
39
+ }
40
+
41
+ export interface UserParams {
42
+ ids: string | number | (string | number)[];
43
+ site?: string;
44
+ filter?: string;
45
+ sort?: SortReputation;
46
+ order?: SortOrder;
47
+ page?: number;
48
+ pagesize?: number;
49
+ }
50
+
51
+ export interface SitesParams {
52
+ page?: number;
53
+ pagesize?: number;
54
+ }
55
+
56
+ /**
57
+ * Build URL search parameters from object
58
+ */
59
+ export function buildSearchParams(params: SearchParams): URLSearchParams {
60
+ const searchParams = new URLSearchParams();
61
+
62
+ for (const [key, value] of Object.entries(params)) {
63
+ if (value === undefined || value === null) {
64
+ continue;
65
+ }
66
+
67
+ if (typeof value === 'boolean') {
68
+ searchParams.set(key, value ? 'true' : 'false');
69
+ } else if (Array.isArray(value)) {
70
+ searchParams.set(key, value.join(';')); // Stack Exchange uses semicolons for arrays
71
+ } else {
72
+ searchParams.set(key, String(value));
73
+ }
74
+ }
75
+
76
+ return searchParams;
77
+ }
78
+
79
+ /**
80
+ * Build search/advanced query
81
+ */
82
+ export function buildSearchQuery(params: SearchParams): URLSearchParams {
83
+ return buildSearchParams(params);
84
+ }
85
+
86
+ /**
87
+ * Build questions query
88
+ */
89
+ export function buildQuestionsQuery(params: QuestionParams): URLSearchParams {
90
+ const searchParams = new URLSearchParams();
91
+
92
+ searchParams.set('order', params.order ?? 'desc');
93
+ searchParams.set('sort', params.sort ?? 'activity');
94
+ searchParams.set('site', params.site ?? 'stackoverflow.com');
95
+ if (params.filter) {
96
+ searchParams.set('filter', params.filter);
97
+ }
98
+ if (params.page) {
99
+ searchParams.set('page', String(params.page));
100
+ }
101
+ if (params.pagesize) {
102
+ searchParams.set('pagesize', String(params.pagesize));
103
+ }
104
+
105
+ return searchParams;
106
+ }
107
+
108
+ /**
109
+ * Build user query
110
+ */
111
+ export function buildUserQuery(params: UserParams): URLSearchParams {
112
+ const searchParams = new URLSearchParams();
113
+
114
+ searchParams.set('site', params.site ?? 'stackoverflow.com');
115
+ searchParams.set('order', params.order ?? 'desc');
116
+ searchParams.set('sort', params.sort ?? 'reputation');
117
+ if (params.filter) {
118
+ searchParams.set('filter', params.filter);
119
+ }
120
+ if (params.page) {
121
+ searchParams.set('page', String(params.page));
122
+ }
123
+ if (params.pagesize) {
124
+ searchParams.set('pagesize', String(params.pagesize));
125
+ }
126
+
127
+ return searchParams;
128
+ }
129
+
130
+ /**
131
+ * Build sites query
132
+ */
133
+ export function buildSitesQuery(params?: SitesParams): URLSearchParams {
134
+ const searchParams = new URLSearchParams();
135
+
136
+ if (params?.page) {
137
+ searchParams.set('page', String(params.page));
138
+ }
139
+ if (params?.pagesize) {
140
+ searchParams.set('pagesize', String(params.pagesize));
141
+ }
142
+
143
+ return searchParams;
144
+ }
145
+
146
+ /**
147
+ * Pre-defined filters for common use cases
148
+ */
149
+ export const Filters = {
150
+ // Get question body and answers with body
151
+ WITH_BODY: '!9_bDDxJY5',
152
+ // Minimal info - just title and score
153
+ MINIMAL: '!-*f(6rL6Wq', // Minimal info - title, score, tags, owner
154
+ // Full details including comments
155
+ FULL: '!*S15IVPvS5',
156
+ // Get answers with body
157
+ ANSWERS_WITH_BODY: '!9_bDE(fI5',
158
+ };
159
+
160
+ /**
161
+ * Parse IDs from query string (handle both single IDs and comma/semicolon separated)
162
+ */
163
+ export function parseIds(ids: string | number | (string | number)[]): string {
164
+ if (Array.isArray(ids)) {
165
+ return ids.join(';');
166
+ }
167
+ return String(ids);
168
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Stack Exchange REST API v2.3 Client
3
+ * Handles API requests with rate limiting, quota tracking, and backoff handling
4
+ */
5
+
6
+ import type { StackExchangeWrapper } from './types.ts';
7
+ import { logger } from '../logger.ts';
8
+
9
+ const API_BASE = 'https://api.stackexchange.com/2.3';
10
+
11
+ export interface RequestOptions {
12
+ method: string;
13
+ endpoint: string;
14
+ params: URLSearchParams;
15
+ }
16
+
17
+ export class StackExchangeClient {
18
+ private readonly _apiKey: string | null;
19
+ private readonly _timeout: number;
20
+ private quotaRemaining = 300;
21
+ private quotaMax = 300;
22
+ private requestCount = 0;
23
+ private lastBackoff: number | null = null;
24
+
25
+ constructor(
26
+ apiKey: string | null,
27
+ timeout: number,
28
+ ) {
29
+ this._apiKey = apiKey;
30
+ this._timeout = timeout;
31
+ }
32
+
33
+ async request<T>(options: RequestOptions, signal?: AbortSignal): Promise<StackExchangeWrapper<T>> {
34
+ // Check for backoff from previous requests
35
+ if (this.lastBackoff && this.lastBackoff > Date.now()) {
36
+ const waitTime = Math.ceil((this.lastBackoff - Date.now()) / 1000);
37
+ throw new Error(
38
+ `Rate limited. Please wait ${waitTime} seconds before making more requests.`,
39
+ );
40
+ }
41
+
42
+ const url = new URL(`${API_BASE}${options.endpoint}`);
43
+ url.search = options.params.toString();
44
+
45
+ // Add API key if provided
46
+ if (this._apiKey) {
47
+ url.searchParams.set('key', this._apiKey);
48
+ }
49
+
50
+ // Add site parameter if not already present (for methods that need it)
51
+ if (!url.searchParams.has('site') && !options.endpoint.startsWith('/sites')) {
52
+ url.searchParams.set('site', 'stackoverflow.com');
53
+ }
54
+
55
+ const controller = new AbortController();
56
+ const timeoutId = setTimeout(() => {
57
+ controller.abort();
58
+ }, this._timeout);
59
+
60
+ // Chain the signal if provided
61
+ if (signal) {
62
+ signal.addEventListener('abort', () => {
63
+ clearTimeout(timeoutId);
64
+ controller.abort();
65
+ });
66
+ }
67
+
68
+ try {
69
+ const response = await fetch(url.toString(), {
70
+ method: options.method,
71
+ signal: controller.signal,
72
+ headers: {
73
+ 'Accept': 'application/json',
74
+ },
75
+ });
76
+
77
+ clearTimeout(timeoutId);
78
+
79
+ const data = await response.json() as StackExchangeWrapper<T>;
80
+
81
+ // Handle API errors
82
+ if (data.error_id) {
83
+ throw new Error(
84
+ `Stack Exchange API Error (${data.error_id} - ${data.error_name}): ${data.error_message}`,
85
+ );
86
+ }
87
+
88
+ // Update quota tracking
89
+ this.quotaRemaining = data.quota_remaining;
90
+ this.quotaMax = data.quota_max;
91
+ this.requestCount++;
92
+
93
+ // Handle backoff
94
+ if (data.backoff) {
95
+ this.lastBackoff = Date.now() + (data.backoff * 1000);
96
+ logger.warn(`[StackExchange] Backoff required: ${data.backoff} seconds`);
97
+ }
98
+
99
+ return data;
100
+ } catch (error) {
101
+ clearTimeout(timeoutId);
102
+
103
+ if (error instanceof Error && error.name === 'AbortError') {
104
+ throw new Error(`Request timeout after ${this._timeout}ms`, { cause: error });
105
+ }
106
+
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ getQuotaInfo(): { remaining: number; max: number; requestCount: number; lastBackoff: number | null } {
112
+ return {
113
+ remaining: this.quotaRemaining,
114
+ max: this.quotaMax,
115
+ requestCount: this.requestCount,
116
+ lastBackoff: this.lastBackoff,
117
+ };
118
+ }
119
+
120
+ isQuotaExhausted(): boolean {
121
+ return this.quotaRemaining <= 0;
122
+ }
123
+
124
+ isQuotaLow(): boolean {
125
+ return this.quotaRemaining < 30; // Warn when less than 30 requests remain
126
+ }
127
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Stack Exchange API v2.3 Types
3
+ */
4
+
5
+ export interface StackExchangeConfig {
6
+ defaultSite: string;
7
+ apiKey: string | null;
8
+ requestTimeout: number; // milliseconds
9
+ }
10
+
11
+ export interface StackExchangeWrapper<T> {
12
+ items: T[];
13
+ has_more: boolean;
14
+ quota_max: number;
15
+ quota_remaining: number;
16
+ backoff?: number;
17
+ error_id?: number;
18
+ error_name?: string;
19
+ error_message?: string;
20
+ }
21
+
22
+ export type SortOrder = 'desc' | 'asc';
23
+ export type SortActivity = 'activity' | 'creation' | 'votes' | 'relevance';
24
+ export type SortReputation = 'reputation' | 'creation' | 'name' | 'modified';
25
+
26
+ // Question types
27
+ export interface Question {
28
+ tags: string[];
29
+ owner?: {
30
+ account_id: number;
31
+ reputation: number;
32
+ user_id: number;
33
+ user_type: string;
34
+ profile_image: string;
35
+ display_name: string;
36
+ link: string;
37
+ };
38
+ is_answered: boolean;
39
+ view_count: number;
40
+ accepted_answer_id?: number;
41
+ answer_count: number;
42
+ score: number;
43
+ last_activity_date: number;
44
+ creation_date: number;
45
+ last_edit_date?: number;
46
+ question_id: number;
47
+ content_license: string;
48
+ link: string;
49
+ title: string;
50
+ body?: string;
51
+ body_markdown?: string;
52
+ }
53
+
54
+ export interface QuestionWithAnswers extends Question {
55
+ answers?: Answer[];
56
+ }
57
+
58
+ export interface Answer {
59
+ owner?: {
60
+ account_id: number;
61
+ reputation: number;
62
+ user_id: number;
63
+ user_type: string;
64
+ profile_image: string;
65
+ display_name: string;
66
+ link: string;
67
+ };
68
+ is_accepted: boolean;
69
+ score: number;
70
+ last_activity_date: number;
71
+ creation_date: number;
72
+ last_edit_date?: number;
73
+ answer_id: number;
74
+ question_id: number;
75
+ content_license: string;
76
+ body?: string;
77
+ body_markdown?: string;
78
+ }
79
+
80
+ // User types
81
+ export interface User {
82
+ badge_counts: {
83
+ bronze: number;
84
+ silver: number;
85
+ gold: number;
86
+ };
87
+ account_id: number;
88
+ is_employee: boolean;
89
+ last_modified_date?: number;
90
+ last_access_date?: number;
91
+ reputation_change_year: number;
92
+ reputation_change_quarter: number;
93
+ reputation_change_month: number;
94
+ reputation_change_week: number;
95
+ reputation_change_day: number;
96
+ reputation: number;
97
+ creation_date: number;
98
+ user_type: string;
99
+ user_id: number;
100
+ accept_rate?: number;
101
+ location?: string;
102
+ website_url?: string;
103
+ link: string;
104
+ profile_image: string;
105
+ display_name: string;
106
+ }
107
+
108
+ // Site types
109
+ export interface Site {
110
+ name: string;
111
+ api_site_parameter: string;
112
+ site_url: string;
113
+ audience: string;
114
+ icon_url?: string;
115
+ }
116
+
117
+ // Command parameters
118
+ export interface CommandParams {
119
+ command: 'search' | 'get' | 'user' | 'site';
120
+ query?: string;
121
+ id?: string | number;
122
+ site: string;
123
+ limit: number;
124
+ format: 'table' | 'json' | 'compact';
125
+ tags?: string | null;
126
+ full: boolean;
127
+ minimal: boolean;
128
+ }
129
+