@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,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* security_search Tool
|
|
3
|
+
*
|
|
4
|
+
* Search security vulnerability databases (NVD, CISA KEV, GitHub Advisories, OSV).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
8
|
+
import { Type } from '@sinclair/typebox';
|
|
9
|
+
import { searchSecurityDatabases } from '../security/index.ts';
|
|
10
|
+
import type { SecuritySearchParams } from '../security/types.ts';
|
|
11
|
+
import type { ToolUsageTracker } from '../utils/tool-usage-tracker.ts';
|
|
12
|
+
|
|
13
|
+
interface SecuritySearchParamsType {
|
|
14
|
+
databases?: string[];
|
|
15
|
+
terms: string[];
|
|
16
|
+
severity?: string;
|
|
17
|
+
maxResults?: number;
|
|
18
|
+
includeExploited?: boolean;
|
|
19
|
+
ecosystem?: string;
|
|
20
|
+
githubRepo?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createSecuritySearchTool(options: {
|
|
25
|
+
searxngUrl: string;
|
|
26
|
+
ctx: ExtensionContext;
|
|
27
|
+
tracker: ToolUsageTracker;
|
|
28
|
+
}): ToolDefinition {
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'security_search',
|
|
32
|
+
label: 'Security Search',
|
|
33
|
+
description: 'Search security vulnerability databases (NVD, CISA KEV, GitHub Advisories, OSV). Returns CVEs, advisories, and vulnerability details. Filter by severity, CVE ID, package name, or include only actively exploited vulnerabilities.',
|
|
34
|
+
promptSnippet: 'Search security vulnerability databases for CVEs and advisories',
|
|
35
|
+
promptGuidelines: [
|
|
36
|
+
'Use security_search to look up CVE IDs, package vulnerabilities, or security advisories.',
|
|
37
|
+
'Supports databases: NVD (340k+ CVEs), CISA KEV (actively exploited), GitHub Advisories (open source), OSV (packages).',
|
|
38
|
+
'Filter by severity, CVE ID, package name, or include only actively exploited vulnerabilities.',
|
|
39
|
+
'CRITICAL: You are allowed a maximum of 4 gathering calls total across ALL tools. Use them for breadth.',
|
|
40
|
+
],
|
|
41
|
+
parameters: Type.Object({
|
|
42
|
+
databases: Type.Optional(Type.Array(Type.String({
|
|
43
|
+
description: 'Databases to search (default: all): nvd, cisa_kev, github, osv',
|
|
44
|
+
}))),
|
|
45
|
+
terms: Type.Array(Type.String({
|
|
46
|
+
description: 'Search terms: CVE IDs (e.g., CVE-2024-1234), package names, keywords',
|
|
47
|
+
}), { minItems: 1 }),
|
|
48
|
+
severity: Type.Optional(Type.String({
|
|
49
|
+
description: 'Filter by severity: LOW, MEDIUM, HIGH, CRITICAL',
|
|
50
|
+
})),
|
|
51
|
+
maxResults: Type.Optional(Type.Number({
|
|
52
|
+
description: 'Max results per database (default: 20)',
|
|
53
|
+
default: 20,
|
|
54
|
+
minimum: 1,
|
|
55
|
+
maximum: 100,
|
|
56
|
+
})),
|
|
57
|
+
includeExploited: Type.Optional(Type.Boolean({
|
|
58
|
+
description: 'Only include actively exploited vulnerabilities',
|
|
59
|
+
default: false,
|
|
60
|
+
})),
|
|
61
|
+
ecosystem: Type.Optional(Type.String({
|
|
62
|
+
description: 'Package ecosystem for OSV: npm, pip, maven, go, rust, cargo, etc.',
|
|
63
|
+
})),
|
|
64
|
+
githubRepo: Type.Optional(Type.String({
|
|
65
|
+
description: 'GitHub repository for advisories: "owner/repo" format',
|
|
66
|
+
})),
|
|
67
|
+
}),
|
|
68
|
+
async execute(
|
|
69
|
+
_toolCallId,
|
|
70
|
+
params,
|
|
71
|
+
_signal,
|
|
72
|
+
_onUpdate,
|
|
73
|
+
_extensionCtx,
|
|
74
|
+
): Promise<AgentToolResult<unknown>> {
|
|
75
|
+
// Record call in tracker - returns false if limit reached
|
|
76
|
+
const allowed = options.tracker.recordCall('security_search');
|
|
77
|
+
if (!allowed) {
|
|
78
|
+
// THROW to prevent researcher from calling again
|
|
79
|
+
throw new Error(options.tracker.getLimitMessage('security_search'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const startTime = Date.now();
|
|
83
|
+
const paramsRecord = params as Record<string, unknown>;
|
|
84
|
+
|
|
85
|
+
if (!isSecuritySearchParams(paramsRecord)) {
|
|
86
|
+
throw new Error('Invalid parameters for security_search');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const terms = paramsRecord['terms'] as string[];
|
|
90
|
+
if (terms.length === 0) {
|
|
91
|
+
throw new Error('At least one search term is required');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const databasesParam = paramsRecord['databases'] as string[] | undefined;
|
|
95
|
+
const databases = databasesParam !== undefined && databasesParam.length > 0
|
|
96
|
+
? databasesParam
|
|
97
|
+
: ['nvd', 'cisa_kev', 'github', 'osv'];
|
|
98
|
+
|
|
99
|
+
const maxResults = (paramsRecord['maxResults'] as number | undefined) ?? 20;
|
|
100
|
+
|
|
101
|
+
let results;
|
|
102
|
+
try {
|
|
103
|
+
results = await searchSecurityDatabases({
|
|
104
|
+
databases,
|
|
105
|
+
terms,
|
|
106
|
+
severity: paramsRecord['severity'] as string | undefined,
|
|
107
|
+
maxResults,
|
|
108
|
+
includeExploited: (paramsRecord['includeExploited'] as boolean | undefined) ?? false,
|
|
109
|
+
ecosystem: paramsRecord['ecosystem'] as string | undefined,
|
|
110
|
+
githubRepo: paramsRecord['githubRepo'] as string | undefined,
|
|
111
|
+
} as SecuritySearchParams);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: `# Security Vulnerability Search Failed\n\n**Error:** ${errorMsg}\n\n**Databases:** ${databases.join(', ')}\n\n**Terms:** ${terms.join(', ')}\n\nUnable to search security databases. This may be a temporary issue - try again later.`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
details: {
|
|
122
|
+
error: errorMsg,
|
|
123
|
+
databases,
|
|
124
|
+
terms,
|
|
125
|
+
duration: Date.now() - startTime,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const elapsed = Date.now() - startTime;
|
|
131
|
+
|
|
132
|
+
let markdown = '# Security Vulnerability Search Results\n\n';
|
|
133
|
+
markdown += `**Searched:** ${databases.join(', ')}\n`;
|
|
134
|
+
markdown += `**Terms:** ${terms.join(', ')}\n`;
|
|
135
|
+
markdown += `**Duration:** ${(elapsed / 1000).toFixed(2)}s\n\n`;
|
|
136
|
+
markdown += `**Total Vulnerabilities Found:** ${results.totalVulnerabilities}\n\n`;
|
|
137
|
+
|
|
138
|
+
if (results.results.nvd !== undefined) {
|
|
139
|
+
markdown += '## NIST NVD\n\n';
|
|
140
|
+
if (results.results.nvd.error !== undefined) {
|
|
141
|
+
markdown += `❌ **Error:** ${results.results.nvd.error}\n\n`;
|
|
142
|
+
} else {
|
|
143
|
+
markdown += `Found: ${results.results.nvd.count} vulnerabilities\n\n`;
|
|
144
|
+
for (const vuln of results.results.nvd.vulnerabilities.slice(0, 20)) {
|
|
145
|
+
markdown += `### ${vuln.id}\n`;
|
|
146
|
+
markdown += `- **Severity:** ${vuln.severity}\n`;
|
|
147
|
+
if (vuln.cvssScore !== undefined) {
|
|
148
|
+
markdown += `- **CVSS Score:** ${vuln.cvssScore}\n`;
|
|
149
|
+
if (vuln.cvssVector !== undefined) {
|
|
150
|
+
markdown += `- **CVSS Vector:** ${vuln.cvssVector}\n`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const description = vuln.description;
|
|
154
|
+
markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}\n`;
|
|
155
|
+
if (vuln.knownExploited === true) {
|
|
156
|
+
markdown += '- ⚠️ **Actively Exploited**\n';
|
|
157
|
+
}
|
|
158
|
+
if (vuln.published !== undefined) {
|
|
159
|
+
markdown += `- **Published:** ${vuln.published}\n`;
|
|
160
|
+
}
|
|
161
|
+
if (vuln.cwes !== undefined && vuln.cwes.length > 0) {
|
|
162
|
+
markdown += `- **CWEs:** ${vuln.cwes.join(', ')}\n`;
|
|
163
|
+
}
|
|
164
|
+
if (vuln.references !== undefined && vuln.references.length > 0) {
|
|
165
|
+
markdown += `- **References:** ${vuln.references.slice(0, 3).join(', ')}\n`;
|
|
166
|
+
}
|
|
167
|
+
markdown += '\n';
|
|
168
|
+
}
|
|
169
|
+
if (results.results.nvd.vulnerabilities.length > 20) {
|
|
170
|
+
const moreCount = results.results.nvd.vulnerabilities.length - 20;
|
|
171
|
+
const moreText = moreCount === 1 ? 'vulnerability' : 'vulnerabilities';
|
|
172
|
+
markdown += `\n*... and ${moreCount} more ${moreText} not shown.*\n`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
markdown += '\n---\n\n';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (results.results.cisa_kev !== undefined) {
|
|
179
|
+
markdown += '## CISA Known Exploited Vulnerabilities\n\n';
|
|
180
|
+
if (results.results.cisa_kev.error !== undefined) {
|
|
181
|
+
markdown += `❌ **Error:** ${results.results.cisa_kev.error}\n\n`;
|
|
182
|
+
} else {
|
|
183
|
+
markdown += `Found: ${results.results.cisa_kev.count} actively exploited vulnerabilities\n\n`;
|
|
184
|
+
for (const vuln of results.results.cisa_kev.vulnerabilities.slice(0, 20)) {
|
|
185
|
+
markdown += `### ${vuln.id}\n`;
|
|
186
|
+
if (vuln.vendor !== undefined) {
|
|
187
|
+
markdown += `- **Vendor:** ${vuln.vendor}\n`;
|
|
188
|
+
}
|
|
189
|
+
if (vuln.product !== undefined) {
|
|
190
|
+
markdown += `- **Product:** ${vuln.product}\n`;
|
|
191
|
+
}
|
|
192
|
+
const description = vuln.description;
|
|
193
|
+
markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}\n`;
|
|
194
|
+
if (vuln.dueDate !== undefined) {
|
|
195
|
+
markdown += `- **Due Date:** ${vuln.dueDate}\n`;
|
|
196
|
+
}
|
|
197
|
+
if (vuln.requiredAction !== undefined) {
|
|
198
|
+
markdown += `- **Required Action:** ${vuln.requiredAction}\n`;
|
|
199
|
+
}
|
|
200
|
+
markdown += '\n';
|
|
201
|
+
}
|
|
202
|
+
if (results.results.cisa_kev.vulnerabilities.length > 20) {
|
|
203
|
+
const moreCount = results.results.cisa_kev.vulnerabilities.length - 20;
|
|
204
|
+
const moreText = moreCount === 1 ? 'vulnerability' : 'vulnerabilities';
|
|
205
|
+
markdown += `\n*... and ${moreCount} more ${moreText} not shown.*\n`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
markdown += '\n---\n\n';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (results.results.github !== undefined) {
|
|
212
|
+
markdown += '## GitHub Security Advisories\n\n';
|
|
213
|
+
if (results.results.github.error !== undefined) {
|
|
214
|
+
markdown += `❌ **Error:** ${results.results.github.error}\n\n`;
|
|
215
|
+
} else {
|
|
216
|
+
markdown += `Found: ${results.results.github.count} advisories\n\n`;
|
|
217
|
+
for (const adv of results.results.github.advisories.slice(0, 20)) {
|
|
218
|
+
markdown += `### ${adv.id}\n`;
|
|
219
|
+
markdown += `- **Severity:** ${adv.severity}\n`;
|
|
220
|
+
if (adv.cveId) {
|
|
221
|
+
markdown += `- **CVE ID:** ${adv.cveId}\n`;
|
|
222
|
+
}
|
|
223
|
+
markdown += `- **Summary:** ${adv.summary}\n`;
|
|
224
|
+
const description = adv.description ?? '';
|
|
225
|
+
markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}\n`;
|
|
226
|
+
if (adv.published) {
|
|
227
|
+
markdown += `- **Published:** ${adv.published}\n`;
|
|
228
|
+
}
|
|
229
|
+
if (adv.affectedPackages && adv.affectedPackages.length > 0) {
|
|
230
|
+
markdown += `- **Affected:** ${adv.affectedPackages.join(', ')}\n`;
|
|
231
|
+
}
|
|
232
|
+
markdown += '\n';
|
|
233
|
+
}
|
|
234
|
+
if (results.results.github.advisories.length > 20) {
|
|
235
|
+
const moreCount = results.results.github.advisories.length - 20;
|
|
236
|
+
const moreText = moreCount === 1 ? 'advisory' : 'advisories';
|
|
237
|
+
markdown += `\n*... and ${moreCount} more ${moreText} not shown.*\n`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
markdown += '\n---\n\n';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (results.results.osv !== undefined) {
|
|
244
|
+
markdown += '## Open Source Vulnerabilities (OSV)\n\n';
|
|
245
|
+
if (results.results.osv.error !== undefined) {
|
|
246
|
+
markdown += `❌ **Error:** ${results.results.osv.error}\n\n`;
|
|
247
|
+
} else {
|
|
248
|
+
markdown += `Found: ${results.results.osv.count} vulnerabilities\n\n`;
|
|
249
|
+
for (const vuln of results.results.osv.vulnerabilities.slice(0, 20)) {
|
|
250
|
+
markdown += `### ${vuln.id}\n`;
|
|
251
|
+
markdown += `- **Severity:** ${vuln.severity}\n`;
|
|
252
|
+
const description = vuln.description;
|
|
253
|
+
markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}\n`;
|
|
254
|
+
if (vuln.affectedProducts && vuln.affectedProducts.length > 0) {
|
|
255
|
+
markdown += `- **Affected:** ${vuln.affectedProducts.join(', ')}\n`;
|
|
256
|
+
}
|
|
257
|
+
if (vuln.fixes && vuln.fixes.length > 0) {
|
|
258
|
+
markdown += `- **Fixes:** ${vuln.fixes.slice(0, 3).join('; ')}\n`;
|
|
259
|
+
}
|
|
260
|
+
markdown += '\n';
|
|
261
|
+
}
|
|
262
|
+
if (results.results.osv.vulnerabilities.length > 20) {
|
|
263
|
+
const moreCount = results.results.osv.vulnerabilities.length - 20;
|
|
264
|
+
const moreText = moreCount === 1 ? 'vulnerability' : 'vulnerabilities';
|
|
265
|
+
markdown += `\n*... and ${moreCount} more ${moreText} not shown.*\n`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
markdown += '\n---\n\n';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: 'text', text: markdown }],
|
|
273
|
+
details: {
|
|
274
|
+
results,
|
|
275
|
+
totalDatabases: results.totalDatabases,
|
|
276
|
+
totalVulnerabilities: results.totalVulnerabilities,
|
|
277
|
+
duration: elapsed,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isSecuritySearchParams(params: Record<string, unknown>): params is SecuritySearchParamsType {
|
|
285
|
+
return (
|
|
286
|
+
typeof params === 'object' &&
|
|
287
|
+
params !== null &&
|
|
288
|
+
'terms' in params &&
|
|
289
|
+
Array.isArray(params['terms']) &&
|
|
290
|
+
(params['terms'] as unknown[]).every((t: unknown) => typeof t === 'string')
|
|
291
|
+
);
|
|
292
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackexchange Tool
|
|
3
|
+
*
|
|
4
|
+
* Search and retrieve data from Stack Exchange network via REST API v2.3.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
8
|
+
import { Type } from '@sinclair/typebox';
|
|
9
|
+
import { stackexchangeCommand } from '../stackexchange/index.ts';
|
|
10
|
+
import type { ToolUsageTracker } from '../utils/tool-usage-tracker.ts';
|
|
11
|
+
|
|
12
|
+
export function createStackexchangeTool(options: {
|
|
13
|
+
ctx: ExtensionContext;
|
|
14
|
+
tracker: ToolUsageTracker;
|
|
15
|
+
}): ToolDefinition {
|
|
16
|
+
const { tracker } = options;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
name: 'stackexchange',
|
|
20
|
+
label: 'Stack Exchange Search',
|
|
21
|
+
description: 'Search and retrieve data from Stack Exchange network via REST API v2.3 (anonymous: 300 requests/day, with key: 10,000 requests/day)',
|
|
22
|
+
promptSnippet: 'Search Stack Overflow and other Stack Exchange sites for questions, answers, and user information',
|
|
23
|
+
promptGuidelines: [
|
|
24
|
+
'Use stackexchange to find technical answers on Stack Overflow',
|
|
25
|
+
'Great for finding code solutions, debugging help, and best practices',
|
|
26
|
+
'Works with any Stack Exchange site (Stack Overflow, SuperUser, AskUbuntu, etc.)',
|
|
27
|
+
'Anonymous access: 300 requests/day. Set STACKEXCHANGE_API_KEY env var for 10,000/day.',
|
|
28
|
+
'Use tags to filter by specific topics.',
|
|
29
|
+
'CRITICAL: You are allowed a maximum of 4 gathering calls total across ALL tools. Use them for breadth.',
|
|
30
|
+
],
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
command: Type.String({
|
|
33
|
+
description: 'Command: search, get, user, or site',
|
|
34
|
+
}),
|
|
35
|
+
query: Type.Optional(Type.String({
|
|
36
|
+
description: 'Search query (for search command)',
|
|
37
|
+
})),
|
|
38
|
+
id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
|
39
|
+
site: Type.Optional(Type.String({
|
|
40
|
+
description: 'Stack Exchange site (default: stackoverflow.com)',
|
|
41
|
+
})),
|
|
42
|
+
limit: Type.Optional(Type.Number({
|
|
43
|
+
description: 'Results count (1-100, default: 10)',
|
|
44
|
+
default: 10,
|
|
45
|
+
minimum: 1,
|
|
46
|
+
maximum: 100,
|
|
47
|
+
})),
|
|
48
|
+
format: Type.Optional(Type.String({
|
|
49
|
+
description: 'Output format: table, json, or compact (default: table)',
|
|
50
|
+
default: 'table',
|
|
51
|
+
})),
|
|
52
|
+
tags: Type.Optional(Type.String({
|
|
53
|
+
description: 'Filter by tags (comma-separated)',
|
|
54
|
+
})),
|
|
55
|
+
}),
|
|
56
|
+
async execute(
|
|
57
|
+
_toolCallId,
|
|
58
|
+
params,
|
|
59
|
+
signal,
|
|
60
|
+
_onUpdate,
|
|
61
|
+
extensionCtx,
|
|
62
|
+
): Promise<AgentToolResult<unknown>> {
|
|
63
|
+
// Record call in tracker - returns false if limit reached
|
|
64
|
+
const allowed = tracker.recordCall('stackexchange');
|
|
65
|
+
if (!allowed) {
|
|
66
|
+
// THROW to prevent researcher from calling again
|
|
67
|
+
throw new Error(tracker.getLimitMessage('stackexchange'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const paramsRecord = params as Record<string, unknown>;
|
|
71
|
+
const command = paramsRecord['command'] as string;
|
|
72
|
+
|
|
73
|
+
if (!command || typeof command !== 'string') {
|
|
74
|
+
throw new Error('Stack Exchange command is required and must be a string');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return await stackexchangeCommand({
|
|
79
|
+
command,
|
|
80
|
+
params: paramsRecord,
|
|
81
|
+
ctx: extensionCtx,
|
|
82
|
+
signal,
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: `# Stack Exchange Search Failed\n\n**Error:** ${errorMsg}\n\n**Command:** ${command}\n\nFailed to query Stack Exchange. This may be due to API rate limiting (300/day anonymous, 10,000/day with key), network issues, or invalid query parameters.`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
details: {
|
|
94
|
+
command,
|
|
95
|
+
error: errorMsg,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|