@f5xc-salesdemos/xcsh 17.2.0 → 17.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "17.2.0",
4
+ "version": "17.3.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.2.0",
50
- "@f5xc-salesdemos/pi-agent-core": "17.2.0",
51
- "@f5xc-salesdemos/pi-ai": "17.2.0",
52
- "@f5xc-salesdemos/pi-natives": "17.2.0",
53
- "@f5xc-salesdemos/pi-tui": "17.2.0",
54
- "@f5xc-salesdemos/pi-utils": "17.2.0",
49
+ "@f5xc-salesdemos/xcsh-stats": "17.3.0",
50
+ "@f5xc-salesdemos/pi-agent-core": "17.3.0",
51
+ "@f5xc-salesdemos/pi-ai": "17.3.0",
52
+ "@f5xc-salesdemos/pi-natives": "17.3.0",
53
+ "@f5xc-salesdemos/pi-tui": "17.3.0",
54
+ "@f5xc-salesdemos/pi-utils": "17.3.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 { APP_NAME } from "@f5xc-salesdemos/pi-utils";
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);
@@ -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 hasDomainFilter = params.allowed_domains?.length || params.blocked_domains?.length;
171
+ const hasAnthropicOnlyParams =
172
+ params.allowed_domains?.length || params.blocked_domains?.length || params.max_uses || params.user_location;
172
173
  const effectiveProvider =
173
- hasDomainFilter && (!params.provider || params.provider === "auto") ? "anthropic" : params.provider;
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())
@@ -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
- if (toolConfig?.max_uses) tool.max_uses = toolConfig.max_uses;
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> = {