@apmantza/greedysearch-pi 1.9.1 → 2.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.
@@ -1,186 +1,187 @@
1
- /**
2
- * Shared types, utilities, and runSearch for Pi tool handlers
3
- */
4
-
5
- import { spawn } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
- import { join } from "node:path";
8
- import type { ProgressUpdate, ToolResult } from "../types.js";
9
-
10
- export type { ProgressUpdate, ToolResult } from "../types.js";
11
-
12
- // Canonical source is src/search/constants.mjs keep in sync
13
- const ALL_ENGINES = ["perplexity", "bing", "google"] as const;
14
-
15
- export { ALL_ENGINES };
16
-
17
- /** Strip surrounding double-quotes that some framework versions inject into string params */
18
- export function stripQuotes(val: string): string {
19
- return val.replace(/^"|"$/g, "");
20
- }
21
-
22
- /**
23
- * Check if the CDP module is available in the package directory
24
- */
25
- export function cdpAvailable(baseDir: string): boolean {
26
- return existsSync(join(baseDir, "bin", "cdp.mjs"));
27
- }
28
-
29
- /**
30
- * Create a "cdp missing" error result
31
- */
32
- export function cdpMissingResult(): ToolResult {
33
- return {
34
- content: [
35
- {
36
- type: "text",
37
- text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
38
- },
39
- ],
40
- details: {} as Record<string, unknown>,
41
- };
42
- }
43
-
44
- /**
45
- * Create an error result with a message
46
- */
47
- export function errorResult(prefix: string, e: unknown): ToolResult {
48
- const msg = e instanceof Error ? e.message : String(e);
49
- return {
50
- content: [{ type: "text", text: `${prefix}: ${msg}` }],
51
- details: {} as Record<string, unknown>,
52
- };
53
- }
54
-
55
- /**
56
- * Spawn search.mjs and collect JSON results, with progress streaming via stderr.
57
- * Shared by GreedySearch tool handlers.
58
- */
59
- export function runSearch(
60
- engine: string,
61
- query: string,
62
- flags: string[],
63
- searchBin: string,
64
- signal?: AbortSignal,
65
- onProgress?: (
66
- engine: string,
67
- status: "done" | "error" | "needs-human",
68
- ) => void,
69
- headless?: boolean, // defaults to true (headless is the default)
70
- ): Promise<Record<string, unknown>> {
71
- return new Promise((resolve, reject) => {
72
- const allFlags = [...flags];
73
- // Headless is default — only skip if explicitly false or GREEDY_SEARCH_VISIBLE=1
74
- if (headless !== false && process.env.GREEDY_SEARCH_VISIBLE !== "1")
75
- allFlags.push("--headless");
76
- if (headless === false) allFlags.push("--always-visible");
77
- // Propagate visibility preference via env (--headless flag is informational;
78
- // the actual headless control in search.mjs / launch.mjs reads the env var).
79
- const procEnv = { ...process.env };
80
- if (headless === false) {
81
- procEnv.GREEDY_SEARCH_VISIBLE = "1";
82
- procEnv.GREEDY_SEARCH_ALWAYS_VISIBLE = "1";
83
- }
84
- const proc = spawn(
85
- process.execPath,
86
- [searchBin, engine, "--inline", "--stdin", ...allFlags],
87
- { stdio: ["pipe", "pipe", "pipe"], env: procEnv },
88
- );
89
- // Pipe query via stdin to avoid leaking it in process table command-line
90
- proc.stdin.write(query);
91
- proc.stdin.end();
92
- let out = "";
93
- let err = "";
94
-
95
- const onAbort = () => {
96
- proc.kill("SIGTERM");
97
- reject(new Error("Aborted"));
98
- };
99
- signal?.addEventListener("abort", onAbort, { once: true });
100
-
101
- proc.stderr.on("data", (d: Buffer) => {
102
- err += d;
103
- for (const line of d.toString().split("\n")) {
104
- // Engine progress: perplexity/bing/google
105
- const engineMatch = line.match(
106
- /^PROGRESS:(perplexity|bing|google):(done|error|needs-human)$/,
107
- );
108
- if (engineMatch && onProgress) {
109
- onProgress(
110
- engineMatch[1],
111
- engineMatch[2] as "done" | "error" | "needs-human",
112
- );
113
- }
114
- // Synthesis progress: skipped (manual verification) or done/error
115
- const synthMatch = line.match(
116
- /^PROGRESS:synthesis:(done|error|skipped)$/,
117
- );
118
- if (synthMatch && onProgress) {
119
- onProgress(
120
- "synthesis",
121
- synthMatch[1] as "done" | "error" | "needs-human",
122
- );
123
- }
124
- }
125
- });
126
-
127
- proc.stdout.on("data", (d: Buffer) => (out += d));
128
- proc.on("close", (code: number) => {
129
- signal?.removeEventListener("abort", onAbort);
130
- if (code !== 0) {
131
- reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
132
- } else {
133
- try {
134
- resolve(JSON.parse(out.trim()));
135
- } catch {
136
- reject(
137
- new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
138
- );
139
- }
140
- }
141
- });
142
- });
143
- }
144
-
145
- /**
146
- * Build a progress callback that tracks completed engines.
147
- * Returns an onProgress function suitable for runSearch.
148
- */
149
- export function makeProgressTracker(
150
- engines: readonly string[],
151
- onUpdate: ((update: ProgressUpdate) => void) | undefined,
152
- suffix: "Searching" | "Researching",
153
- depth: string,
154
- ) {
155
- const completed = new Map<string, "done" | "error" | "needs-human">();
156
-
157
- return (eng: string, status: "done" | "error" | "needs-human") => {
158
- completed.set(eng, status);
159
- const parts: string[] = [];
160
- for (const e of engines) {
161
- const s = completed.get(e);
162
- if (s === "done") parts.push(`✅ ${e} done`);
163
- else if (s === "error") parts.push(`❌ ${e} failed`);
164
- else if (s === "needs-human")
165
- parts.push(`🔓 ${e} needs manual verification`);
166
- else parts.push(`⏳ ${e}`);
167
- }
168
- // Synthesis status: when all engines complete in non-fast mode,
169
- // show synthesis progress. Handle "skipped" status (emitted when
170
- // manual verification is needed and synthesis is bypassed).
171
- if (depth !== "fast" && completed.size >= 3) {
172
- const synStatus = completed.get("synthesis");
173
- if (synStatus === "done") parts.push("✅ synthesized");
174
- else if (synStatus === "error") parts.push(" synthesis failed");
175
- else if (synStatus === "needs-human") parts.push("⏭️ synthesis skipped");
176
- else parts.push("🔄 synthesizing");
177
- }
178
-
179
- onUpdate?.({
180
- content: [
181
- { type: "text", text: `**${suffix}...** ${parts.join(" · ")}` },
182
- ],
183
- details: { _progress: true },
184
- } satisfies ProgressUpdate);
185
- };
186
- }
1
+ /**
2
+ * Shared types, utilities, and runSearch for Pi tool handlers
3
+ */
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import type { ProgressUpdate, ToolResult } from "../types.js";
9
+
10
+ export type { ProgressUpdate, ToolResult } from "../types.js";
11
+
12
+ // Import and re-export ALL_ENGINES from constants.mjs so it's always in sync.
13
+ // constants.mjs reads ~/.pi/greedyconfig for user overrides.
14
+ import { ALL_ENGINES } from "../search/constants.mjs";
15
+ export { ALL_ENGINES };
16
+
17
+ /** Strip surrounding double-quotes that some framework versions inject into string params */
18
+ export function stripQuotes(val: string): string {
19
+ return val.replace(/^"|"$/g, "");
20
+ }
21
+
22
+ /**
23
+ * Check if the CDP module is available in the package directory
24
+ */
25
+ export function cdpAvailable(baseDir: string): boolean {
26
+ return existsSync(join(baseDir, "bin", "cdp.mjs"));
27
+ }
28
+
29
+ /**
30
+ * Create a "cdp missing" error result
31
+ */
32
+ export function cdpMissingResult(): ToolResult {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: "cdp.mjs missing — try reinstalling: pi install git:github.com/apmantza/GreedySearch-pi",
38
+ },
39
+ ],
40
+ details: {} as Record<string, unknown>,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Create an error result with a message
46
+ */
47
+ export function errorResult(prefix: string, e: unknown): ToolResult {
48
+ const msg = e instanceof Error ? e.message : String(e);
49
+ return {
50
+ content: [{ type: "text", text: `${prefix}: ${msg}` }],
51
+ details: {} as Record<string, unknown>,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Spawn search.mjs and collect JSON results, with progress streaming via stderr.
57
+ * Shared by GreedySearch tool handlers.
58
+ */
59
+ export function runSearch(
60
+ engine: string,
61
+ query: string,
62
+ flags: string[],
63
+ searchBin: string,
64
+ signal?: AbortSignal,
65
+ onProgress?: (
66
+ engine: string,
67
+ status: "done" | "error" | "needs-human",
68
+ ) => void,
69
+ options: { headless?: boolean } = {},
70
+ ): Promise<Record<string, unknown>> {
71
+ return new Promise((resolve, reject) => {
72
+ const { headless = true } = options;
73
+ const allFlags = [...flags];
74
+ // Headless is default — only skip if explicitly false or GREEDY_SEARCH_VISIBLE=1
75
+ if (headless !== false && process.env.GREEDY_SEARCH_VISIBLE !== "1")
76
+ allFlags.push("--headless");
77
+ if (headless === false) allFlags.push("--always-visible");
78
+ // Propagate visibility preference via env (--headless flag is informational;
79
+ // the actual headless control in search.mjs / launch.mjs reads the env var).
80
+ const procEnv = { ...process.env };
81
+ if (headless === false) {
82
+ procEnv.GREEDY_SEARCH_VISIBLE = "1";
83
+ procEnv.GREEDY_SEARCH_ALWAYS_VISIBLE = "1";
84
+ }
85
+ const proc = spawn(
86
+ process.execPath,
87
+ [searchBin, engine, "--inline", "--stdin", ...allFlags],
88
+ { stdio: ["pipe", "pipe", "pipe"], env: procEnv },
89
+ );
90
+ // Pipe query via stdin to avoid leaking it in process table command-line
91
+ proc.stdin.write(query);
92
+ proc.stdin.end();
93
+ let out = "";
94
+ let err = "";
95
+
96
+ const onAbort = () => {
97
+ proc.kill("SIGTERM");
98
+ reject(new Error("Aborted"));
99
+ };
100
+ signal?.addEventListener("abort", onAbort, { once: true });
101
+
102
+ proc.stderr.on("data", (d: Buffer) => {
103
+ err += d;
104
+ // Match PROGRESS lines for any known engine.
105
+ const ENGINE_PROGRESS_RE =
106
+ /^PROGRESS:(perplexity|google|chatgpt|bing|gemini|semantic-scholar|semanticscholar|s2|logically):(done|error|needs-human)$/;
107
+ for (const line of d.toString().split("\n")) {
108
+ // Engine progress: any known engine
109
+ const engineMatch = line.match(ENGINE_PROGRESS_RE);
110
+ if (engineMatch && onProgress) {
111
+ onProgress(
112
+ engineMatch[1],
113
+ engineMatch[2] as "done" | "error" | "needs-human",
114
+ );
115
+ }
116
+ // Synthesis progress: skipped (manual verification) or done/error
117
+ const synthMatch = line.match(
118
+ /^PROGRESS:synthesis:(done|error|skipped)$/,
119
+ );
120
+ if (synthMatch && onProgress) {
121
+ onProgress(
122
+ "synthesis",
123
+ synthMatch[1] as "done" | "error" | "needs-human",
124
+ );
125
+ }
126
+ }
127
+ });
128
+
129
+ proc.stdout.on("data", (d: Buffer) => (out += d));
130
+ proc.on("close", (code: number) => {
131
+ signal?.removeEventListener("abort", onAbort);
132
+ if (code !== 0) {
133
+ reject(new Error(err.trim() || `search.mjs exited with code ${code}`));
134
+ } else {
135
+ try {
136
+ resolve(JSON.parse(out.trim()));
137
+ } catch {
138
+ reject(
139
+ new Error(`Invalid JSON from search.mjs: ${out.slice(0, 200)}`),
140
+ );
141
+ }
142
+ }
143
+ });
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Build a progress callback that tracks completed engines.
149
+ * Returns an onProgress function suitable for runSearch.
150
+ */
151
+ export function makeProgressTracker(
152
+ engines: readonly string[],
153
+ onUpdate: ((update: ProgressUpdate) => void) | undefined,
154
+ suffix: "Searching" | "Researching",
155
+ showSynthesis: boolean,
156
+ ) {
157
+ const completed = new Map<string, "done" | "error" | "needs-human">();
158
+
159
+ return (eng: string, status: "done" | "error" | "needs-human") => {
160
+ completed.set(eng, status);
161
+ const parts: string[] = [];
162
+ for (const e of engines) {
163
+ const s = completed.get(e);
164
+ if (s === "done") parts.push(`✅ ${e} done`);
165
+ else if (s === "error") parts.push(`❌ ${e} failed`);
166
+ else if (s === "needs-human")
167
+ parts.push(`🔓 ${e} needs manual verification`);
168
+ else parts.push(`⏳ ${e}`);
169
+ }
170
+ // Synthesis status is shown only when the caller explicitly requested
171
+ // Gemini synthesis for a multi-engine search.
172
+ if (showSynthesis && completed.size >= engines.length) {
173
+ const synStatus = completed.get("synthesis");
174
+ if (synStatus === "done") parts.push(" synthesized");
175
+ else if (synStatus === "error") parts.push(" synthesis failed");
176
+ else if (synStatus === "needs-human") parts.push("⏭️ synthesis skipped");
177
+ else parts.push("🔄 synthesizing");
178
+ }
179
+
180
+ onUpdate?.({
181
+ content: [
182
+ { type: "text", text: `**${suffix}...** ${parts.join(" · ")}` },
183
+ ],
184
+ details: { _progress: true },
185
+ } satisfies ProgressUpdate);
186
+ };
187
+ }
package/src/types.ts CHANGED
@@ -1,104 +1,110 @@
1
- /**
2
- * TypeScript interfaces for GreedySearch data structures
3
- *
4
- * These types document the shape of data flowing between modules.
5
- * They can be imported by TypeScript files (index.ts, tool handlers, formatters)
6
- * and used for type safety without runtime overhead.
7
- */
8
-
9
- // ============================================================================
10
- // Search Result Types
11
- // ============================================================================
12
-
13
- /** A single source extracted from search results */
14
- export interface Source {
15
- url: string;
16
- title: string;
17
- type?: "official-docs" | "maintainer-blog" | "repo" | "community" | "website";
18
- domain?: string;
19
- snippet?: string;
20
- }
21
-
22
- /** Result from a single search engine */
23
- export interface SearchResult {
24
- engine: string;
25
- answer: string;
26
- sources: Source[];
27
- url?: string;
28
- query?: string;
29
- error?: string;
30
- }
31
-
32
- /** Synthesis result combining multiple engine results */
33
- export interface SynthesisResult {
34
- answer: string;
35
- agreementLevel?: "consensus" | "majority" | "mixed" | "conflicting";
36
- claims?: Claim[];
37
- sourceIds?: string[];
38
- confidence?: ConfidenceMetrics;
39
- }
40
-
41
- /** A single claim within a synthesis */
42
- export interface Claim {
43
- text: string;
44
- sourceIds: string[];
45
- confidence?: "high" | "medium" | "low";
46
- }
47
-
48
- /** Confidence metrics for a synthesis */
49
- export interface ConfidenceMetrics {
50
- overall: number; // 0-1
51
- consensus: number; // fraction of engines agreeing
52
- sourceCount: number;
53
- engineCount: number;
54
- }
55
-
56
- // ============================================================================
57
- // Source Registry Types
58
- // ============================================================================
59
-
60
- /** A classified source in the registry */
61
- export interface ClassifiedSource extends Source {
62
- engineOrigin: string[];
63
- isOfficial: boolean;
64
- consensus: number; // fraction of engines citing this source
65
- }
66
-
67
- // ============================================================================
68
- // Tool Result Types
69
- // ============================================================================
70
-
71
- /** Progress update sent via onUpdate during long-running searches */
72
- export interface ProgressUpdate {
73
- content: Array<{ type: "text"; text: string }>;
74
- details: { _progress: true };
75
- }
76
-
77
- /** Pi tool result format */
78
- export interface ToolResult {
79
- content: Array<{ type: "text"; text: string }>;
80
- details: Record<string, unknown>;
81
- }
82
-
83
- // ============================================================================
84
- // Engine Configuration Types
85
- // ============================================================================
86
-
87
- /** Engine definition for the ENGINES map */
88
- export interface EngineConfig {
89
- /** Extractor script filename (e.g. "perplexity.mjs") */
90
- script: string;
91
- /** Human-readable label for progress messages */
92
- label: string;
93
- /** Domain pattern for source matching */
94
- domain: string;
95
- /** URL pattern for the engine */
96
- url: string;
97
- }
98
-
99
- // ============================================================================
100
- // Constants
101
- // ============================================================================
102
-
103
- // Runtime defaults are in src/search/defaults.mjs (since .ts files can't be
104
- // imported directly by Node.js). Import DEFAULTS from there for runtime values.
1
+ /**
2
+ * TypeScript interfaces for GreedySearch data structures
3
+ *
4
+ * These types document the shape of data flowing between modules.
5
+ * They can be imported by TypeScript files (index.ts, tool handlers, formatters)
6
+ * and used for type safety without runtime overhead.
7
+ */
8
+
9
+ // ============================================================================
10
+ // Search Result Types
11
+ // ============================================================================
12
+
13
+ /** A single source extracted from search results */
14
+ export interface Source {
15
+ url: string;
16
+ title: string;
17
+ type?:
18
+ | "official-docs"
19
+ | "maintainer-blog"
20
+ | "repo"
21
+ | "academic"
22
+ | "community"
23
+ | "website";
24
+ domain?: string;
25
+ snippet?: string;
26
+ }
27
+
28
+ /** Result from a single search engine */
29
+ export interface SearchResult {
30
+ engine: string;
31
+ answer: string;
32
+ sources: Source[];
33
+ url?: string;
34
+ query?: string;
35
+ error?: string;
36
+ }
37
+
38
+ /** Synthesis result combining multiple engine results */
39
+ export interface SynthesisResult {
40
+ answer: string;
41
+ agreementLevel?: "consensus" | "majority" | "mixed" | "conflicting";
42
+ claims?: Claim[];
43
+ sourceIds?: string[];
44
+ confidence?: ConfidenceMetrics;
45
+ }
46
+
47
+ /** A single claim within a synthesis */
48
+ export interface Claim {
49
+ text: string;
50
+ sourceIds: string[];
51
+ confidence?: "high" | "medium" | "low";
52
+ }
53
+
54
+ /** Confidence metrics for a synthesis */
55
+ export interface ConfidenceMetrics {
56
+ overall: number; // 0-1
57
+ consensus: number; // fraction of engines agreeing
58
+ sourceCount: number;
59
+ engineCount: number;
60
+ }
61
+
62
+ // ============================================================================
63
+ // Source Registry Types
64
+ // ============================================================================
65
+
66
+ /** A classified source in the registry */
67
+ export interface ClassifiedSource extends Source {
68
+ engineOrigin: string[];
69
+ isOfficial: boolean;
70
+ consensus: number; // fraction of engines citing this source
71
+ }
72
+
73
+ // ============================================================================
74
+ // Tool Result Types
75
+ // ============================================================================
76
+
77
+ /** Progress update sent via onUpdate during long-running searches */
78
+ export interface ProgressUpdate {
79
+ content: Array<{ type: "text"; text: string }>;
80
+ details: { _progress: true };
81
+ }
82
+
83
+ /** Pi tool result format */
84
+ export interface ToolResult {
85
+ content: Array<{ type: "text"; text: string }>;
86
+ details: Record<string, unknown>;
87
+ }
88
+
89
+ // ============================================================================
90
+ // Engine Configuration Types
91
+ // ============================================================================
92
+
93
+ /** Engine definition for the ENGINES map */
94
+ export interface EngineConfig {
95
+ /** Extractor script filename (e.g. "perplexity.mjs") */
96
+ script: string;
97
+ /** Human-readable label for progress messages */
98
+ label: string;
99
+ /** Domain pattern for source matching */
100
+ domain: string;
101
+ /** URL pattern for the engine */
102
+ url: string;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Constants
107
+ // ============================================================================
108
+
109
+ // Runtime defaults are in src/search/defaults.mjs (since .ts files can't be
110
+ // imported directly by Node.js). Import DEFAULTS from there for runtime values.