@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,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
|
+
|