@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,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended Type Definitions for pi Extension Context
|
|
3
|
+
*
|
|
4
|
+
* These types extend the base ExtensionContext from @mariozechner/pi-coding-agent
|
|
5
|
+
* to provide type safety for additional properties that are accessed at runtime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SettingsManager } from '@mariozechner/pi-coding-agent';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Session manager interface for accessing session metadata
|
|
12
|
+
*/
|
|
13
|
+
export interface SessionManager {
|
|
14
|
+
getSessionId(): string;
|
|
15
|
+
getSessionFile(): string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extended ExtensionContext with additional properties
|
|
20
|
+
* Note: We don't extend ExtensionContext to avoid type conflicts with
|
|
21
|
+
* ReadonlySessionManager. This is used as a cast target only.
|
|
22
|
+
*/
|
|
23
|
+
export interface ExtendedExtensionContext {
|
|
24
|
+
sessionManager?: SessionManager;
|
|
25
|
+
settingsManager?: SettingsManager;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Model with ID property and optional cost information
|
|
30
|
+
*/
|
|
31
|
+
export interface ModelWithId {
|
|
32
|
+
id: string;
|
|
33
|
+
cost?: {
|
|
34
|
+
input: number;
|
|
35
|
+
output: number;
|
|
36
|
+
cacheRead?: number;
|
|
37
|
+
cacheWrite?: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Agent session event with usage and error information
|
|
43
|
+
*/
|
|
44
|
+
export interface ExtendedAgentSessionEvent {
|
|
45
|
+
type: string;
|
|
46
|
+
message?: {
|
|
47
|
+
role?: string;
|
|
48
|
+
usage?: {
|
|
49
|
+
totalTokens?: number;
|
|
50
|
+
input?: number;
|
|
51
|
+
output?: number;
|
|
52
|
+
cacheRead?: number;
|
|
53
|
+
cacheWrite?: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
isError?: boolean;
|
|
57
|
+
details?: {
|
|
58
|
+
blocked?: boolean;
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/types/llm.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-Related Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Shared type definitions for LLM responses, usage, and message content.
|
|
5
|
+
* These types complement the types from @mariozechner/pi-ai and provide
|
|
6
|
+
* more specific typing for our use cases.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Token usage information from LLM responses
|
|
11
|
+
*/
|
|
12
|
+
export interface TokenUsage {
|
|
13
|
+
/** Input tokens consumed */
|
|
14
|
+
input?: number;
|
|
15
|
+
/** Output tokens generated */
|
|
16
|
+
output?: number;
|
|
17
|
+
/** Cache read tokens (for prompt caching) */
|
|
18
|
+
cacheRead?: number;
|
|
19
|
+
/** Cache write tokens (for prompt caching) */
|
|
20
|
+
cacheWrite?: number;
|
|
21
|
+
/** Total tokens (may be provided by some providers) */
|
|
22
|
+
totalTokens?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Text content block in a message
|
|
27
|
+
*/
|
|
28
|
+
export interface TextContentBlock {
|
|
29
|
+
type: 'text';
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Message content (can be a string or array of blocks)
|
|
35
|
+
*/
|
|
36
|
+
export type MessageContent = string | TextContentBlock[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter for text content blocks from a message content array
|
|
40
|
+
*/
|
|
41
|
+
export function isTextContentBlock(block: unknown): block is TextContentBlock {
|
|
42
|
+
return (
|
|
43
|
+
block !== null &&
|
|
44
|
+
typeof block === 'object' &&
|
|
45
|
+
(block as Record<string, unknown>)['type'] === 'text' &&
|
|
46
|
+
typeof (block as Record<string, unknown>)['text'] === 'string'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract text from a message content block or string
|
|
52
|
+
*/
|
|
53
|
+
export function extractTextFromContent(content: MessageContent): string {
|
|
54
|
+
if (typeof content === 'string') {
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(content)) {
|
|
58
|
+
return content
|
|
59
|
+
.filter(isTextContentBlock)
|
|
60
|
+
.map(block => block.text)
|
|
61
|
+
.join('\n');
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse token usage from an unknown object
|
|
68
|
+
* Returns a partial usage object with any fields that were present
|
|
69
|
+
*/
|
|
70
|
+
export function parseTokenUsage(usage: unknown): Partial<TokenUsage> {
|
|
71
|
+
if (!usage || typeof usage !== 'object') {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const obj = usage as Record<string, unknown>;
|
|
76
|
+
const result: Partial<TokenUsage> = {};
|
|
77
|
+
|
|
78
|
+
if (typeof obj['input'] === 'number') {
|
|
79
|
+
result.input = obj['input'];
|
|
80
|
+
}
|
|
81
|
+
if (typeof obj['output'] === 'number') {
|
|
82
|
+
result.output = obj['output'];
|
|
83
|
+
}
|
|
84
|
+
if (typeof obj['cacheRead'] === 'number') {
|
|
85
|
+
result.cacheRead = obj['cacheRead'];
|
|
86
|
+
}
|
|
87
|
+
if (typeof obj['cacheWrite'] === 'number') {
|
|
88
|
+
result.cacheWrite = obj['cacheWrite'];
|
|
89
|
+
}
|
|
90
|
+
if (typeof obj['totalTokens'] === 'number') {
|
|
91
|
+
result.totalTokens = obj['totalTokens'];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Calculate total tokens from usage object
|
|
99
|
+
* Falls back to sum of individual components if totalTokens not provided
|
|
100
|
+
*/
|
|
101
|
+
export function calculateTotalTokens(usage: Partial<TokenUsage>): number {
|
|
102
|
+
if (usage.totalTokens !== undefined) {
|
|
103
|
+
return usage.totalTokens;
|
|
104
|
+
}
|
|
105
|
+
return (
|
|
106
|
+
(usage.input ?? 0) +
|
|
107
|
+
(usage.output ?? 0) +
|
|
108
|
+
(usage.cacheRead ?? 0) +
|
|
109
|
+
(usage.cacheWrite ?? 0)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Injection Utility
|
|
3
|
+
*
|
|
4
|
+
* Injects the current date into agent prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format current date as a readable string
|
|
9
|
+
*/
|
|
10
|
+
function getCurrentDateString(): string {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const options: Intl.DateTimeFormatOptions = {
|
|
13
|
+
weekday: 'long',
|
|
14
|
+
year: 'numeric',
|
|
15
|
+
month: 'long',
|
|
16
|
+
day: 'numeric',
|
|
17
|
+
};
|
|
18
|
+
return now.toLocaleDateString('en-US', options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Inject current date into a prompt
|
|
23
|
+
* Prepends a date context line to the prompt
|
|
24
|
+
*/
|
|
25
|
+
export function injectCurrentDate(prompt: string, _agentType: 'coordinator' | 'researcher'): string {
|
|
26
|
+
const dateString = getCurrentDateString();
|
|
27
|
+
const dateContext = `**Current Date:** ${dateString}\n\n`;
|
|
28
|
+
return dateContext + prompt;
|
|
29
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Basic validation for research queries and parameters.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../logger.ts';
|
|
8
|
+
|
|
9
|
+
/** Maximum query length */
|
|
10
|
+
export const MAX_QUERY_LENGTH = 500;
|
|
11
|
+
|
|
12
|
+
/** Minimum query length */
|
|
13
|
+
export const MIN_QUERY_LENGTH = 3;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate research query
|
|
17
|
+
*
|
|
18
|
+
* @param query - The research query to validate
|
|
19
|
+
* @returns Object with isValid flag and optional error message
|
|
20
|
+
*/
|
|
21
|
+
export function validateQuery(query: string): { isValid: boolean; error?: string } {
|
|
22
|
+
// Check if query is provided
|
|
23
|
+
if (!query || typeof query !== 'string') {
|
|
24
|
+
return { isValid: false, error: 'Query must be a non-empty string' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const trimmed = query.trim();
|
|
28
|
+
|
|
29
|
+
// Check minimum length
|
|
30
|
+
if (trimmed.length < MIN_QUERY_LENGTH) {
|
|
31
|
+
return { isValid: false, error: `Query must be at least ${MIN_QUERY_LENGTH} characters` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check maximum length
|
|
35
|
+
if (trimmed.length > MAX_QUERY_LENGTH) {
|
|
36
|
+
return { isValid: false, error: `Query must be at most ${MAX_QUERY_LENGTH} characters` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for empty or whitespace-only queries
|
|
40
|
+
if (trimmed === '') {
|
|
41
|
+
return { isValid: false, error: 'Query cannot be empty or whitespace-only' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for potentially dangerous patterns
|
|
45
|
+
const dangerousPatterns = [
|
|
46
|
+
/<script/i,
|
|
47
|
+
/javascript:/i,
|
|
48
|
+
/on\w+\s*=/i, // Event handlers like onclick=
|
|
49
|
+
/<iframe/i,
|
|
50
|
+
/<object/i,
|
|
51
|
+
/<embed/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const pattern of dangerousPatterns) {
|
|
55
|
+
if (pattern.test(trimmed)) {
|
|
56
|
+
return { isValid: false, error: 'Query contains potentially dangerous content' };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { isValid: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sanitize research query for safe use
|
|
65
|
+
*
|
|
66
|
+
* @param query - The research query to sanitize
|
|
67
|
+
* @returns Sanitized query
|
|
68
|
+
*/
|
|
69
|
+
export function sanitizeQuery(query: string): string {
|
|
70
|
+
return query
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
73
|
+
.replace(/[\x00-\x1F\x7F]/g, ''); // eslint-disable-line no-control-regex -- Remove control characters
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate and sanitize research query in one step
|
|
78
|
+
*
|
|
79
|
+
* @param query - The research query to validate and sanitize
|
|
80
|
+
* @returns Sanitized query or throws error if invalid
|
|
81
|
+
* @throws Error if query is invalid
|
|
82
|
+
*/
|
|
83
|
+
export function validateAndSanitizeQuery(query: string): string {
|
|
84
|
+
const validation = validateQuery(query);
|
|
85
|
+
|
|
86
|
+
if (!validation.isValid) {
|
|
87
|
+
const error = validation.error || 'Invalid query';
|
|
88
|
+
logger.warn(`[input-validation] ${error}`);
|
|
89
|
+
throw new Error(error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return sanitizeQuery(query);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate complexity level
|
|
97
|
+
*
|
|
98
|
+
* @param complexity - The complexity level to validate
|
|
99
|
+
* @returns True if valid, false otherwise
|
|
100
|
+
*/
|
|
101
|
+
export function validateComplexity(complexity: number): boolean {
|
|
102
|
+
return complexity === 0 || complexity === 1 || complexity === 2 || complexity === 3;
|
|
103
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Parsing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for parsing JSON from LLM responses.
|
|
5
|
+
* Handles various formats including code blocks, raw JSON, and malformed input.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logger } from '../logger.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Result of a JSON extraction attempt
|
|
12
|
+
*/
|
|
13
|
+
export interface JsonExtractionResult<T = unknown> {
|
|
14
|
+
/** Whether extraction was successful */
|
|
15
|
+
success: boolean;
|
|
16
|
+
/** The parsed value if successful, undefined otherwise */
|
|
17
|
+
value: T | undefined;
|
|
18
|
+
/** Error message if unsuccessful */
|
|
19
|
+
error?: string;
|
|
20
|
+
/** The method that succeeded (for logging) */
|
|
21
|
+
method?: 'code-block' | 'raw-object' | 'raw-array';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract JSON from markdown code blocks
|
|
26
|
+
*
|
|
27
|
+
* Looks for ```json or ```javascript code blocks and parses the content.
|
|
28
|
+
* Returns the first successfully parsed block.
|
|
29
|
+
*
|
|
30
|
+
* @param text - Text to search for JSON code blocks
|
|
31
|
+
* @returns Extraction result with parsed value or error
|
|
32
|
+
*/
|
|
33
|
+
export function extractJsonFromCodeBlocks<T = unknown>(
|
|
34
|
+
text: string
|
|
35
|
+
): JsonExtractionResult<T> {
|
|
36
|
+
const codeBlockRegex = /```(?:json|javascript)?\s*([\s\S]*?)```/gi;
|
|
37
|
+
let match: RegExpExecArray | null;
|
|
38
|
+
|
|
39
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
40
|
+
const codeContent = match[1] ?? '';
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(codeContent.trim());
|
|
43
|
+
return { success: true, value: parsed as T, method: 'code-block' };
|
|
44
|
+
} catch {
|
|
45
|
+
// Try next code block
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
value: undefined,
|
|
53
|
+
error: 'No valid JSON found in code blocks',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract JSON object from raw text
|
|
59
|
+
*
|
|
60
|
+
* Looks for a JSON object spanning from the first `{` to the last `}`.
|
|
61
|
+
* Useful when JSON is embedded in prose without code blocks.
|
|
62
|
+
*
|
|
63
|
+
* @param text - Text to search for JSON object
|
|
64
|
+
* @returns Extraction result with parsed value or error
|
|
65
|
+
*/
|
|
66
|
+
export function extractJsonObject<T = unknown>(
|
|
67
|
+
text: string
|
|
68
|
+
): JsonExtractionResult<T> {
|
|
69
|
+
const objStart = text.indexOf('{');
|
|
70
|
+
const objEnd = text.lastIndexOf('}');
|
|
71
|
+
|
|
72
|
+
if (objStart === -1 || objEnd <= objStart) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
value: undefined,
|
|
76
|
+
error: 'No JSON object boundaries found',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const jsonStr = text.slice(objStart, objEnd + 1);
|
|
82
|
+
const parsed = JSON.parse(jsonStr);
|
|
83
|
+
return { success: true, value: parsed as T, method: 'raw-object' };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
value: undefined,
|
|
89
|
+
error: `Failed to parse JSON object: ${errorMsg}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract JSON array from raw text
|
|
96
|
+
*
|
|
97
|
+
* Looks for a JSON array spanning from the first `[` to the last `]`.
|
|
98
|
+
* Useful for extracting arrays of strings or objects.
|
|
99
|
+
*
|
|
100
|
+
* @param text - Text to search for JSON array
|
|
101
|
+
* @returns Extraction result with parsed value or error
|
|
102
|
+
*/
|
|
103
|
+
export function extractJsonArray<T = unknown>(
|
|
104
|
+
text: string
|
|
105
|
+
): JsonExtractionResult<T[]> {
|
|
106
|
+
const stripped = text.trim();
|
|
107
|
+
|
|
108
|
+
// Quick check if it's a JSON array
|
|
109
|
+
if (!stripped.startsWith('[') || !stripped.endsWith(']')) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
value: undefined,
|
|
113
|
+
error: 'Text is not a JSON array',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(stripped);
|
|
119
|
+
if (!Array.isArray(parsed)) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
value: undefined,
|
|
123
|
+
error: 'Parsed value is not an array',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { success: true, value: parsed as T[], method: 'raw-array' };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
value: undefined,
|
|
132
|
+
error: `Failed to parse JSON array: ${errorMsg}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extract JSON with fallback chain
|
|
139
|
+
*
|
|
140
|
+
* Tries multiple extraction methods in order:
|
|
141
|
+
* 1. Code blocks (most reliable)
|
|
142
|
+
* 2. Raw JSON object (for objects)
|
|
143
|
+
* 3. Raw JSON array (for arrays)
|
|
144
|
+
*
|
|
145
|
+
* @param text - Text to extract JSON from
|
|
146
|
+
* @param targetType - Whether to expect 'object', 'array', or 'any'
|
|
147
|
+
* @returns Extraction result with parsed value or error
|
|
148
|
+
*/
|
|
149
|
+
export function extractJson<T = unknown>(
|
|
150
|
+
text: string,
|
|
151
|
+
targetType: 'object' | 'array' | 'any' = 'any'
|
|
152
|
+
): JsonExtractionResult<T> {
|
|
153
|
+
// Try code blocks first (most reliable)
|
|
154
|
+
const codeBlockResult = extractJsonFromCodeBlocks<T>(text);
|
|
155
|
+
if (codeBlockResult.success) {
|
|
156
|
+
logger.debug('[json-utils] Extracted JSON from code block');
|
|
157
|
+
return codeBlockResult;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Try raw object (if expecting object or any)
|
|
161
|
+
if (targetType === 'object' || targetType === 'any') {
|
|
162
|
+
const objectResult = extractJsonObject<T>(text);
|
|
163
|
+
if (objectResult.success) {
|
|
164
|
+
logger.debug('[json-utils] Extracted JSON object from raw text');
|
|
165
|
+
return objectResult;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try raw array (if expecting array or any)
|
|
170
|
+
if (targetType === 'array' || targetType === 'any') {
|
|
171
|
+
const arrayResult = extractJsonArray<T>(text);
|
|
172
|
+
if (arrayResult.success) {
|
|
173
|
+
logger.debug('[json-utils] Extracted JSON array from raw text');
|
|
174
|
+
return arrayResult as JsonExtractionResult<T>;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// All methods failed
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
value: undefined,
|
|
182
|
+
error: 'No valid JSON found using any extraction method',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Result of string array normalization
|
|
188
|
+
*/
|
|
189
|
+
export interface NormalizeArrayResult {
|
|
190
|
+
/** Normalized strings */
|
|
191
|
+
strings: string[];
|
|
192
|
+
/** Count of items that were skipped (empty after normalization) */
|
|
193
|
+
skippedCount: number;
|
|
194
|
+
/** Count of items that were objects and needed extraction */
|
|
195
|
+
extractedCount: number;
|
|
196
|
+
/** Warnings about any issues */
|
|
197
|
+
warnings: string[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Normalize string array with detailed feedback
|
|
202
|
+
*
|
|
203
|
+
* Normalizes an array of values to strings with detailed feedback
|
|
204
|
+
* about what was normalized and any issues.
|
|
205
|
+
*
|
|
206
|
+
* @param arr - Array of values to normalize
|
|
207
|
+
* @returns Detailed normalization result
|
|
208
|
+
*/
|
|
209
|
+
export function normalizeStringArrayDetailed(
|
|
210
|
+
arr: unknown[]
|
|
211
|
+
): NormalizeArrayResult {
|
|
212
|
+
const strings: string[] = [];
|
|
213
|
+
const warnings: string[] = [];
|
|
214
|
+
let skippedCount = 0;
|
|
215
|
+
let extractedCount = 0;
|
|
216
|
+
|
|
217
|
+
arr.forEach((item, index) => {
|
|
218
|
+
if (typeof item === 'string') {
|
|
219
|
+
const trimmed = item.trim();
|
|
220
|
+
if (trimmed.length > 0) {
|
|
221
|
+
strings.push(trimmed);
|
|
222
|
+
} else {
|
|
223
|
+
skippedCount++;
|
|
224
|
+
warnings.push(`Item ${index}: empty string`);
|
|
225
|
+
}
|
|
226
|
+
} else if (item !== null && typeof item === 'object') {
|
|
227
|
+
extractedCount++;
|
|
228
|
+
const obj = item as Record<string, unknown>;
|
|
229
|
+
const candidate = obj['query'] ?? obj['topic'] ?? obj['text'] ?? obj['task'];
|
|
230
|
+
|
|
231
|
+
if (typeof candidate === 'string') {
|
|
232
|
+
const trimmed = candidate.trim();
|
|
233
|
+
if (trimmed.length > 0) {
|
|
234
|
+
strings.push(trimmed);
|
|
235
|
+
} else {
|
|
236
|
+
skippedCount++;
|
|
237
|
+
warnings.push(`Item ${index}: object extracted empty string`);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
// No recognized key, stringify the whole object
|
|
241
|
+
const stringified = JSON.stringify(item);
|
|
242
|
+
if (stringified.length > 0) {
|
|
243
|
+
strings.push(stringified);
|
|
244
|
+
warnings.push(`Item ${index}: no recognized key, stringified object`);
|
|
245
|
+
} else {
|
|
246
|
+
skippedCount++;
|
|
247
|
+
warnings.push(`Item ${index}: object stringified to empty`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
const stringified = String(item).trim();
|
|
252
|
+
if (stringified.length > 0) {
|
|
253
|
+
strings.push(stringified);
|
|
254
|
+
} else {
|
|
255
|
+
skippedCount++;
|
|
256
|
+
warnings.push(`Item ${index}: ${typeof item} converted to empty string`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return { strings, skippedCount, extractedCount, warnings };
|
|
262
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ResourceLoader Factory
|
|
3
|
+
*
|
|
4
|
+
* Extracted from coordinator.ts and researcher.ts to avoid duplication.
|
|
5
|
+
* Both files use identical ResourceLoader implementations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ResourceLoader, ExtensionRuntime } from '@mariozechner/pi-coding-agent';
|
|
9
|
+
|
|
10
|
+
export function makeResourceLoader(systemPromptText: string): ResourceLoader {
|
|
11
|
+
const mockRuntime: ExtensionRuntime = {
|
|
12
|
+
flagValues: new Map(),
|
|
13
|
+
pendingProviderRegistrations: [],
|
|
14
|
+
registerProvider: () => {},
|
|
15
|
+
unregisterProvider: () => {},
|
|
16
|
+
sendMessage: async () => {},
|
|
17
|
+
sendUserMessage: async () => {},
|
|
18
|
+
appendEntry: async () => {},
|
|
19
|
+
setSessionName: () => undefined,
|
|
20
|
+
getSessionName: () => undefined,
|
|
21
|
+
setLabel: async () => {},
|
|
22
|
+
getActiveTools: () => [],
|
|
23
|
+
getAllTools: () => [],
|
|
24
|
+
setActiveTools: () => {},
|
|
25
|
+
refreshTools: () => {},
|
|
26
|
+
getCommands: () => [],
|
|
27
|
+
setModel: async () => true,
|
|
28
|
+
getThinkingLevel: () => 'off',
|
|
29
|
+
setThinkingLevel: () => {},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
|
|
34
|
+
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
35
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
36
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
37
|
+
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
38
|
+
getSystemPrompt: () => systemPromptText,
|
|
39
|
+
getAppendSystemPrompt: () => [],
|
|
40
|
+
extendResources: () => {},
|
|
41
|
+
reload: async () => {},
|
|
42
|
+
};
|
|
43
|
+
}
|