@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.
|
|
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.
|
|
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.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 {
|
|
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
|
|
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.
|
package/src/web/search/index.ts
CHANGED
|
@@ -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
|
-
|
|
148
|
-
? (await getSearchProvider(
|
|
149
|
-
? [getSearchProvider(
|
|
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
|
-
|
|
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>;
|