@f5xc-salesdemos/xcsh 17.1.4 → 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.1.4",
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.1.4",
50
- "@f5xc-salesdemos/pi-agent-core": "17.1.4",
51
- "@f5xc-salesdemos/pi-ai": "17.1.4",
52
- "@f5xc-salesdemos/pi-natives": "17.1.4",
53
- "@f5xc-salesdemos/pi-tui": "17.1.4",
54
- "@f5xc-salesdemos/pi-utils": "17.1.4",
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);
@@ -114,7 +114,7 @@ happens under load, in a degraded state, or with an adversarial payload?"
114
114
 
115
115
  <stakes>
116
116
  The operator works in live infrastructure. Routing changes, firewall rules, TLS configurations,
117
- API deployments, traffic policies... Misconfigurations → outages, security exposures, or
117
+ API deployments, traffic policies Misconfigurations → outages, security exposures, or
118
118
  systems that fail under adversarial conditions.
119
119
  - You **MUST NOT** yield incomplete or unvalidated configurations.
120
120
  - You **MUST** only recommend operations and configurations you can defend.
@@ -322,7 +322,6 @@ These are inviolable. Violation is system failure.
322
322
 
323
323
  Configuration integrity means infrastructure tells the truth about what is actually deployed.
324
324
  Every stale config left in IaC without a corresponding live object is a lie to the next operator.
325
-
326
325
  - **The unit of change is the infrastructure decision, not the ticket.** When topology changes,
327
326
  every dependent config, policy reference, and IaC file changes in the same commit. Work is
328
327
  complete when the configuration is coherent, not when the API accepts it.
@@ -38,6 +38,21 @@ export const webSearchSchema = Type.Object({
38
38
  max_tokens: Type.Optional(Type.Number({ description: "Maximum output tokens" })),
39
39
  temperature: Type.Optional(Type.Number({ description: "Sampling temperature" })),
40
40
  num_search_results: Type.Optional(Type.Number({ description: "Number of search results to retrieve" })),
41
+ allowed_domains: Type.Optional(Type.Array(Type.String(), { description: "Only return results from these domains" })),
42
+ blocked_domains: Type.Optional(Type.Array(Type.String(), { description: "Exclude results from these domains" })),
43
+ max_uses: Type.Optional(Type.Number({ description: "Maximum number of web searches per request" })),
44
+ user_location: Type.Optional(
45
+ Type.Object(
46
+ {
47
+ type: Type.Literal("approximate"),
48
+ city: Type.Optional(Type.String()),
49
+ region: Type.Optional(Type.String()),
50
+ country: Type.Optional(Type.String()),
51
+ timezone: Type.Optional(Type.String()),
52
+ },
53
+ { description: "Approximate user location for localized results" },
54
+ ),
55
+ ),
41
56
  });
42
57
 
43
58
  export type SearchToolParams = {
@@ -50,6 +65,16 @@ export type SearchToolParams = {
50
65
  temperature?: number;
51
66
  /** Number of search results to retrieve. Defaults to 10. */
52
67
  num_search_results?: number;
68
+ allowed_domains?: string[];
69
+ blocked_domains?: string[];
70
+ max_uses?: number;
71
+ user_location?: {
72
+ type: "approximate";
73
+ city?: string;
74
+ region?: string;
75
+ country?: string;
76
+ timezone?: string;
77
+ };
53
78
  };
54
79
 
55
80
  export interface SearchQueryParams extends SearchToolParams {
@@ -143,10 +168,14 @@ async function executeSearch(
143
168
  _toolCallId: string,
144
169
  params: SearchQueryParams,
145
170
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
171
+ const hasAnthropicOnlyParams =
172
+ params.allowed_domains?.length || params.blocked_domains?.length || params.max_uses || params.user_location;
173
+ const effectiveProvider =
174
+ hasAnthropicOnlyParams && (!params.provider || params.provider === "auto") ? "anthropic" : params.provider;
146
175
  const providers =
147
- params.provider && params.provider !== "auto"
148
- ? (await getSearchProvider(params.provider).isAvailable())
149
- ? [getSearchProvider(params.provider)]
176
+ effectiveProvider && effectiveProvider !== "auto"
177
+ ? (await getSearchProvider(effectiveProvider).isAvailable())
178
+ ? [getSearchProvider(effectiveProvider)]
150
179
  : await resolveProviderChain("auto")
151
180
  : await resolveProviderChain();
152
181
  if (providers.length === 0) {
@@ -171,6 +200,10 @@ async function executeSearch(
171
200
  maxOutputTokens: params.max_tokens,
172
201
  numSearchResults: params.num_search_results,
173
202
  temperature: params.temperature,
203
+ allowedDomains: params.allowed_domains,
204
+ blockedDomains: params.blocked_domains,
205
+ maxUses: params.max_uses,
206
+ userLocation: params.user_location,
174
207
  });
175
208
 
176
209
  const text = formatForLLM(response);
@@ -27,9 +27,18 @@ 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
 
34
+ export interface AnthropicUserLocation {
35
+ type: "approximate";
36
+ city?: string;
37
+ region?: string;
38
+ country?: string;
39
+ timezone?: string;
40
+ }
41
+
33
42
  export interface AnthropicSearchParams {
34
43
  query: string;
35
44
  system_prompt?: string;
@@ -38,6 +47,31 @@ export interface AnthropicSearchParams {
38
47
  max_tokens?: number;
39
48
  /** Sampling temperature (0–1). Lower = more focused/factual. */
40
49
  temperature?: number;
50
+ allowed_domains?: string[];
51
+ blocked_domains?: string[];
52
+ max_uses?: number;
53
+ user_location?: AnthropicUserLocation;
54
+ }
55
+
56
+ interface WebSearchToolConfig {
57
+ allowed_domains?: string[];
58
+ blocked_domains?: string[];
59
+ max_uses?: number;
60
+ user_location?: AnthropicUserLocation;
61
+ }
62
+
63
+ export function extractSiteOperators(query: string): { cleanQuery: string; domains: string[] } {
64
+ const sitePattern = /\bsite:(\S+)/gi;
65
+ const domains: string[] = [];
66
+ const cleanQuery = query
67
+ .replace(sitePattern, (_, domain) => {
68
+ domains.push(domain);
69
+ return "";
70
+ })
71
+ .replace(/\s{2,}/g, " ")
72
+ .trim();
73
+ const fallback = domains.length > 0 ? domains.join(" ") : query;
74
+ return { cleanQuery: cleanQuery || fallback, domains };
41
75
  }
42
76
 
43
77
  /**
@@ -86,22 +120,27 @@ async function callSearch(
86
120
  systemPrompt?: string,
87
121
  maxTokens?: number,
88
122
  temperature?: number,
123
+ toolConfig?: WebSearchToolConfig,
89
124
  ): Promise<AnthropicApiResponse> {
90
125
  const url = buildAnthropicUrl(auth);
91
126
  const headers = buildAnthropicSearchHeaders(auth);
92
127
 
93
128
  const systemBlocks = buildSystemBlocks(auth, model, systemPrompt);
94
129
 
130
+ const tool: Record<string, unknown> = {
131
+ type: WEB_SEARCH_TOOL_TYPE,
132
+ name: WEB_SEARCH_TOOL_NAME,
133
+ };
134
+ if (toolConfig?.allowed_domains?.length) tool.allowed_domains = toolConfig.allowed_domains;
135
+ if (toolConfig?.blocked_domains?.length) tool.blocked_domains = toolConfig.blocked_domains;
136
+ tool.max_uses = toolConfig?.max_uses ?? DEFAULT_MAX_USES;
137
+ if (toolConfig?.user_location) tool.user_location = toolConfig.user_location;
138
+
95
139
  const body: Record<string, unknown> = {
96
140
  model,
97
141
  max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
98
142
  messages: [{ role: "user", content: query }],
99
- tools: [
100
- {
101
- type: WEB_SEARCH_TOOL_TYPE,
102
- name: WEB_SEARCH_TOOL_NAME,
103
- },
104
- ],
143
+ tools: [tool],
105
144
  };
106
145
 
107
146
  if (temperature !== undefined) {
@@ -246,13 +285,23 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<Se
246
285
  }
247
286
 
248
287
  const model = getModel();
288
+
289
+ const { cleanQuery, domains: siteDomains } = extractSiteOperators(params.query);
290
+ const allAllowedDomains = [...(params.allowed_domains ?? []), ...siteDomains];
291
+
249
292
  const response = await callSearch(
250
293
  auth,
251
294
  model,
252
- params.query,
295
+ cleanQuery,
253
296
  params.system_prompt,
254
297
  params.max_tokens,
255
298
  params.temperature,
299
+ {
300
+ allowed_domains: allAllowedDomains.length > 0 ? allAllowedDomains : undefined,
301
+ blocked_domains: params.blocked_domains,
302
+ max_uses: params.max_uses,
303
+ user_location: params.user_location,
304
+ },
256
305
  );
257
306
 
258
307
  const result = parseResponse(response);
@@ -281,6 +330,10 @@ export class AnthropicProvider extends SearchProvider {
281
330
  num_results: params.numSearchResults ?? params.limit,
282
331
  max_tokens: params.maxOutputTokens,
283
332
  temperature: params.temperature,
333
+ allowed_domains: params.allowedDomains,
334
+ blocked_domains: params.blockedDomains,
335
+ max_uses: params.maxUses,
336
+ user_location: params.userLocation,
284
337
  });
285
338
  }
286
339
  }
@@ -10,6 +10,16 @@ export interface SearchParams {
10
10
  maxOutputTokens?: number;
11
11
  numSearchResults?: number;
12
12
  temperature?: number;
13
+ allowedDomains?: string[];
14
+ blockedDomains?: string[];
15
+ maxUses?: number;
16
+ userLocation?: {
17
+ type: "approximate";
18
+ city?: string;
19
+ region?: string;
20
+ country?: string;
21
+ timezone?: string;
22
+ };
13
23
  googleSearch?: Record<string, unknown>;
14
24
  codeExecution?: Record<string, unknown>;
15
25
  urlContext?: Record<string, unknown>;