@f5xc-salesdemos/xcsh 17.2.0 → 17.4.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/package.json +7 -7
- package/src/cli/web-search-cli.ts +72 -2
- package/src/commands/web-search.ts +4 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/web/search/index.ts +5 -2
- package/src/web/search/providers/anthropic.ts +2 -1
- package/src/web/search/render.ts +27 -0
- package/src/web/search/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "17.
|
|
4
|
+
"version": "17.4.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@f5xc-salesdemos/xcsh-stats": "17.
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "17.
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "17.
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "17.
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "17.
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "17.
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "17.4.0",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "17.4.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "17.4.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "17.4.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "17.4.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "17.4.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Handles `xcsh q`/`xcsh web-search` subcommands for testing web search providers.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { buildAnthropicSearchHeaders, buildAnthropicUrl, findAnthropicAuth } from "@f5xc-salesdemos/pi-ai";
|
|
8
|
+
import { $env, APP_NAME } from "@f5xc-salesdemos/pi-utils";
|
|
8
9
|
import chalk from "chalk";
|
|
9
10
|
import { initTheme, theme } from "../modes/theme/theme";
|
|
10
11
|
import { runSearchQuery, type SearchQueryParams } from "../web/search/index";
|
|
11
12
|
import { SEARCH_PROVIDER_ORDER } from "../web/search/provider";
|
|
12
13
|
import { renderSearchResult } from "../web/search/render";
|
|
13
|
-
import type { SearchProviderId } from "../web/search/types";
|
|
14
|
+
import type { SearchProviderId, SearchResponse } from "../web/search/types";
|
|
14
15
|
|
|
15
16
|
export interface SearchCommandArgs {
|
|
16
17
|
query: string;
|
|
@@ -18,6 +19,8 @@ export interface SearchCommandArgs {
|
|
|
18
19
|
recency?: "day" | "week" | "month" | "year";
|
|
19
20
|
limit?: number;
|
|
20
21
|
expanded: boolean;
|
|
22
|
+
synthesize: boolean;
|
|
23
|
+
synthesizeModel?: string;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
const PROVIDERS: Array<SearchProviderId | "auto"> = ["auto", ...SEARCH_PROVIDER_ORDER];
|
|
@@ -36,6 +39,7 @@ export function parseSearchArgs(args: string[]): SearchCommandArgs | undefined {
|
|
|
36
39
|
const result: SearchCommandArgs = {
|
|
37
40
|
query: "",
|
|
38
41
|
expanded: true,
|
|
42
|
+
synthesize: true,
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
const positional: string[] = [];
|
|
@@ -50,6 +54,10 @@ export function parseSearchArgs(args: string[]): SearchCommandArgs | undefined {
|
|
|
50
54
|
result.limit = Number.parseInt(args[++i], 10);
|
|
51
55
|
} else if (arg === "--compact") {
|
|
52
56
|
result.expanded = false;
|
|
57
|
+
} else if (arg === "--no-synthesize") {
|
|
58
|
+
result.synthesize = false;
|
|
59
|
+
} else if (arg === "--model") {
|
|
60
|
+
result.synthesizeModel = args[++i];
|
|
53
61
|
} else if (!arg.startsWith("-")) {
|
|
54
62
|
positional.push(arg);
|
|
55
63
|
}
|
|
@@ -62,6 +70,54 @@ export function parseSearchArgs(args: string[]): SearchCommandArgs | undefined {
|
|
|
62
70
|
return result;
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
async function synthesizeResponse(
|
|
74
|
+
query: string,
|
|
75
|
+
searchResponse: SearchResponse,
|
|
76
|
+
model?: string,
|
|
77
|
+
): Promise<string | undefined> {
|
|
78
|
+
const auth = await findAnthropicAuth();
|
|
79
|
+
if (!auth) return undefined;
|
|
80
|
+
|
|
81
|
+
const sourcesContext = searchResponse.sources
|
|
82
|
+
.map((s, i) => `[${i + 1}] ${s.title}\n ${s.url}${s.snippet ? `\n ${s.snippet}` : ""}`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
|
|
85
|
+
const citationsContext =
|
|
86
|
+
searchResponse.citations?.map((c, i) => `[${i + 1}] ${c.title}: "${c.citedText}"`).join("\n") ?? "";
|
|
87
|
+
|
|
88
|
+
const synthesisPrompt = `Based on the following web search results for the query "${query}", provide a comprehensive, well-structured answer. Cite sources by number.
|
|
89
|
+
|
|
90
|
+
## Search Results
|
|
91
|
+
${searchResponse.answer ? `### Initial Answer\n${searchResponse.answer}\n` : ""}
|
|
92
|
+
### Sources
|
|
93
|
+
${sourcesContext}
|
|
94
|
+
${citationsContext ? `\n### Citations\n${citationsContext}` : ""}
|
|
95
|
+
|
|
96
|
+
Provide a thorough answer with key facts, numbers, and source citations.`;
|
|
97
|
+
|
|
98
|
+
const url = buildAnthropicUrl(auth);
|
|
99
|
+
const headers = buildAnthropicSearchHeaders(auth);
|
|
100
|
+
const selectedModel = model ?? $env.ANTHROPIC_SEARCH_MODEL ?? "claude-haiku-4-5";
|
|
101
|
+
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers,
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
model: selectedModel,
|
|
107
|
+
max_tokens: 4096,
|
|
108
|
+
messages: [{ role: "user", content: synthesisPrompt }],
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) return undefined;
|
|
113
|
+
|
|
114
|
+
const data = (await response.json()) as { content?: Array<{ type: string; text?: string }> };
|
|
115
|
+
return data.content
|
|
116
|
+
?.filter(b => b.type === "text" && b.text)
|
|
117
|
+
.map(b => b.text)
|
|
118
|
+
.join("\n\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
65
121
|
export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
66
122
|
if (!cmd.query) {
|
|
67
123
|
process.stderr.write(`${chalk.red("Error: Query is required")}\n`);
|
|
@@ -104,6 +160,18 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
|
104
160
|
const width = Math.max(60, process.stdout.columns ?? 100);
|
|
105
161
|
process.stdout.write(`${component.render(width).join("\n")}\n`);
|
|
106
162
|
|
|
163
|
+
if (cmd.synthesize && result.details?.response && result.details.response.sources.length > 0) {
|
|
164
|
+
try {
|
|
165
|
+
process.stderr.write(chalk.dim("\nSynthesizing response...\n"));
|
|
166
|
+
const synthesized = await synthesizeResponse(cmd.query, result.details.response, cmd.synthesizeModel);
|
|
167
|
+
if (synthesized) {
|
|
168
|
+
process.stdout.write(`\n${chalk.bold("Synthesized Answer:")}\n\n${synthesized}\n`);
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
process.stderr.write(chalk.dim("Synthesis unavailable — showing raw search results only.\n"));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
107
175
|
if (result.details?.error) {
|
|
108
176
|
process.exitCode = 1;
|
|
109
177
|
}
|
|
@@ -124,6 +192,8 @@ ${chalk.bold("Options:")}
|
|
|
124
192
|
--recency <value> Recency filter (Brave/Perplexity): ${RECENCY_OPTIONS.join(", ")}
|
|
125
193
|
-l, --limit <n> Max results to return
|
|
126
194
|
--compact Render condensed output
|
|
195
|
+
--no-synthesize Skip synthesis step (raw search only)
|
|
196
|
+
--model <name> Model for synthesis (default: ANTHROPIC_SEARCH_MODEL or claude-haiku-4-5)
|
|
127
197
|
-h, --help Show this help
|
|
128
198
|
|
|
129
199
|
${chalk.bold("Examples:")}
|
|
@@ -23,6 +23,8 @@ export default class Search extends Command {
|
|
|
23
23
|
recency: Flags.string({ description: "Recency filter", options: RECENCY }),
|
|
24
24
|
limit: Flags.integer({ char: "l", description: "Max results to return" }),
|
|
25
25
|
compact: Flags.boolean({ description: "Render condensed output" }),
|
|
26
|
+
"no-synthesize": Flags.boolean({ description: "Skip synthesis step (raw search only)" }),
|
|
27
|
+
model: Flags.string({ description: "Model for synthesis (default: claude-haiku-4-5)" }),
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
async run(): Promise<void> {
|
|
@@ -35,6 +37,8 @@ export default class Search extends Command {
|
|
|
35
37
|
recency: flags.recency as SearchCommandArgs["recency"],
|
|
36
38
|
limit: flags.limit,
|
|
37
39
|
expanded: !flags.compact,
|
|
40
|
+
synthesize: !flags["no-synthesize"],
|
|
41
|
+
synthesizeModel: flags.model,
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
await runSearchCommand(cmd);
|
|
@@ -1313,6 +1313,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
1313
1313
|
default: true,
|
|
1314
1314
|
ui: { tab: "tools", label: "Web Search", description: "Enable the web_search tool for web searching" },
|
|
1315
1315
|
},
|
|
1316
|
+
"web_search.verbose": {
|
|
1317
|
+
type: "boolean",
|
|
1318
|
+
default: false,
|
|
1319
|
+
ui: {
|
|
1320
|
+
tab: "tools",
|
|
1321
|
+
label: "Web Search Verbose",
|
|
1322
|
+
description: "Show detailed search panels (sources, metadata, tokens). When disabled, uses compact output.",
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1316
1325
|
|
|
1317
1326
|
"browser.enabled": {
|
|
1318
1327
|
type: "boolean",
|
package/src/web/search/index.ts
CHANGED
|
@@ -168,9 +168,10 @@ async function executeSearch(
|
|
|
168
168
|
_toolCallId: string,
|
|
169
169
|
params: SearchQueryParams,
|
|
170
170
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
171
|
-
const
|
|
171
|
+
const hasAnthropicOnlyParams =
|
|
172
|
+
params.allowed_domains?.length || params.blocked_domains?.length || params.max_uses || params.user_location;
|
|
172
173
|
const effectiveProvider =
|
|
173
|
-
|
|
174
|
+
hasAnthropicOnlyParams && (!params.provider || params.provider === "auto") ? "anthropic" : params.provider;
|
|
174
175
|
const providers =
|
|
175
176
|
effectiveProvider && effectiveProvider !== "auto"
|
|
176
177
|
? (await getSearchProvider(effectiveProvider).isAvailable())
|
|
@@ -191,6 +192,7 @@ async function executeSearch(
|
|
|
191
192
|
for (const provider of providers) {
|
|
192
193
|
lastProvider = provider;
|
|
193
194
|
try {
|
|
195
|
+
const searchStart = performance.now();
|
|
194
196
|
const response = await provider.search({
|
|
195
197
|
query: params.query.replace(/202\d/g, String(new Date().getFullYear())), // LUL
|
|
196
198
|
limit: params.limit,
|
|
@@ -204,6 +206,7 @@ async function executeSearch(
|
|
|
204
206
|
maxUses: params.max_uses,
|
|
205
207
|
userLocation: params.user_location,
|
|
206
208
|
});
|
|
209
|
+
response.durationMs = performance.now() - searchStart;
|
|
207
210
|
|
|
208
211
|
const text = formatForLLM(response);
|
|
209
212
|
|
|
@@ -27,6 +27,7 @@ import { SearchProvider } from "./base";
|
|
|
27
27
|
|
|
28
28
|
const DEFAULT_MODEL = "claude-haiku-4-5";
|
|
29
29
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
30
|
+
const DEFAULT_MAX_USES = 8;
|
|
30
31
|
const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
31
32
|
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
32
33
|
|
|
@@ -132,7 +133,7 @@ async function callSearch(
|
|
|
132
133
|
};
|
|
133
134
|
if (toolConfig?.allowed_domains?.length) tool.allowed_domains = toolConfig.allowed_domains;
|
|
134
135
|
if (toolConfig?.blocked_domains?.length) tool.blocked_domains = toolConfig.blocked_domains;
|
|
135
|
-
|
|
136
|
+
tool.max_uses = toolConfig?.max_uses ?? DEFAULT_MAX_USES;
|
|
136
137
|
if (toolConfig?.user_location) tool.user_location = toolConfig.user_location;
|
|
137
138
|
|
|
138
139
|
const body: Record<string, unknown> = {
|
package/src/web/search/render.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Component } from "@f5xc-salesdemos/pi-tui";
|
|
8
8
|
import { Text, visibleWidth, wrapTextWithAnsi } from "@f5xc-salesdemos/pi-tui";
|
|
9
|
+
import { Settings } from "../../config/settings";
|
|
9
10
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
10
11
|
import type { Theme } from "../../modes/theme/theme";
|
|
11
12
|
import {
|
|
@@ -25,6 +26,14 @@ import { CachedOutputBlock } from "../../tui/output-block";
|
|
|
25
26
|
import { getSearchProvider } from "./provider";
|
|
26
27
|
import type { SearchResponse } from "./types";
|
|
27
28
|
|
|
29
|
+
function getVerboseSetting(): boolean {
|
|
30
|
+
try {
|
|
31
|
+
return Settings.instance.get("web_search.verbose");
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
const MAX_COLLAPSED_ANSWER_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
29
38
|
const MAX_EXPANDED_ANSWER_LINES = PREVIEW_LIMITS.EXPANDED_LINES;
|
|
30
39
|
const MAX_ANSWER_LINE_LEN = TRUNCATE_LENGTHS.LINE;
|
|
@@ -92,6 +101,20 @@ export function renderSearchResult(
|
|
|
92
101
|
return renderFallbackText(rawText, options.expanded, theme);
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
const verbose = getVerboseSetting();
|
|
105
|
+
if (!verbose) {
|
|
106
|
+
const searches = response.usage?.searchRequests ?? 1;
|
|
107
|
+
const plural = searches !== 1 ? "es" : "";
|
|
108
|
+
const dur =
|
|
109
|
+
response.durationMs !== undefined
|
|
110
|
+
? response.durationMs >= 1000
|
|
111
|
+
? `${Math.round(response.durationMs / 1000)}s`
|
|
112
|
+
: `${Math.round(response.durationMs)}ms`
|
|
113
|
+
: "";
|
|
114
|
+
const line = ` \u23BF Did ${searches} search${plural}${dur ? ` in ${dur}` : ""}`;
|
|
115
|
+
return new Text(theme.fg("dim", line), 0, 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
const sources = Array.isArray(response.sources) ? response.sources : [];
|
|
96
119
|
const sourceCount = sources.length;
|
|
97
120
|
const citations = Array.isArray(response.citations) ? response.citations : [];
|
|
@@ -288,6 +311,10 @@ export function renderSearchCall(
|
|
|
288
311
|
theme: Theme,
|
|
289
312
|
): Component {
|
|
290
313
|
const query = truncateToWidth(args.query ?? "", 80);
|
|
314
|
+
const verbose = getVerboseSetting();
|
|
315
|
+
if (!verbose) {
|
|
316
|
+
return new Text(`${theme.fg("text", `Web Search("${query}")`)}`, 0, 0);
|
|
317
|
+
}
|
|
291
318
|
const text = renderStatusLine({ icon: "pending", title: "Web Search", description: query }, theme);
|
|
292
319
|
return new Text(text, 0, 0);
|
|
293
320
|
}
|
package/src/web/search/types.ts
CHANGED
|
@@ -94,6 +94,8 @@ export interface SearchResponse {
|
|
|
94
94
|
requestId?: string;
|
|
95
95
|
/** Authentication mode used by the provider (e.g. oauth, api-key) */
|
|
96
96
|
authMode?: string;
|
|
97
|
+
/** Wall-clock duration of the search request in milliseconds */
|
|
98
|
+
durationMs?: number;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/** Provider-specific error with optional HTTP status */
|