@f5xc-salesdemos/xcsh 17.1.3 → 17.2.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/config/auto-config.ts +62 -11
- package/src/config/model-registry.ts +15 -1
- package/src/modes/components/welcome-checks.ts +5 -0
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/web/search/index.ts +35 -3
- package/src/web/search/providers/anthropic.ts +59 -7
- package/src/web/search/providers/base.ts +10 -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.2.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.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",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
@@ -199,6 +199,35 @@ export function healConfigYmlModelRoles(configPath: string): void {
|
|
|
199
199
|
// Backup
|
|
200
200
|
// ---------------------------------------------------------------------------
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Extract a quoted literal API key from an existing models.yml file.
|
|
204
|
+
* Only looks within the anthropic or litellm provider blocks to avoid
|
|
205
|
+
* accidentally picking up keys from unrelated providers.
|
|
206
|
+
* Returns undefined if the file uses an env var reference (unquoted) or doesn't exist.
|
|
207
|
+
*/
|
|
208
|
+
export function readApiKeyLiteral(modelsPath: string): string | undefined {
|
|
209
|
+
try {
|
|
210
|
+
const content = fs.readFileSync(modelsPath, "utf-8");
|
|
211
|
+
const lines = content.split("\n");
|
|
212
|
+
let inTargetBlock = false;
|
|
213
|
+
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
if (/^\s{2}(?:anthropic|litellm)\s*:/.test(line)) {
|
|
216
|
+
inTargetBlock = true;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (inTargetBlock && /^\s{2}\S/.test(line)) {
|
|
220
|
+
inTargetBlock = false;
|
|
221
|
+
}
|
|
222
|
+
if (inTargetBlock) {
|
|
223
|
+
const match = line.match(/^\s+apiKey:\s*"([^"]+)"/);
|
|
224
|
+
if (match) return match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
202
231
|
/** Create a .bak backup of a file if it exists. Returns true if backed up. */
|
|
203
232
|
function backupIfExists(filePath: string): boolean {
|
|
204
233
|
try {
|
|
@@ -481,9 +510,18 @@ export function autoFixModelsConfig(modelsPath: string): FixResult {
|
|
|
481
510
|
return { fixed: false, changes: ["Cannot fix: LITELLM_BASE_URL is invalid"] };
|
|
482
511
|
}
|
|
483
512
|
|
|
513
|
+
const existingLiteralKey = readApiKeyLiteral(modelsPath);
|
|
514
|
+
|
|
484
515
|
backupIfExists(modelsPath);
|
|
485
516
|
|
|
486
|
-
if (
|
|
517
|
+
if (
|
|
518
|
+
!safeWrite(
|
|
519
|
+
modelsPath,
|
|
520
|
+
generateModelsYml(baseUrl, {
|
|
521
|
+
...(existingLiteralKey ? { apiKeyLiteral: existingLiteralKey } : {}),
|
|
522
|
+
}),
|
|
523
|
+
)
|
|
524
|
+
) {
|
|
487
525
|
return { fixed: false, changes: [`Write failed: could not write to ${modelsPath}`] };
|
|
488
526
|
}
|
|
489
527
|
|
|
@@ -538,13 +576,13 @@ export function startupHealthCheck(
|
|
|
538
576
|
|
|
539
577
|
const expectedUrl = `${envBaseUrl}/anthropic`;
|
|
540
578
|
if (anthropicConfig.baseUrl !== expectedUrl) {
|
|
541
|
-
// Only auto-fix configs that were generated by xcsh
|
|
542
|
-
//
|
|
543
|
-
//
|
|
579
|
+
// Only auto-fix configs that were generated by xcsh. User-written configs
|
|
580
|
+
// with a custom proxy URL must never be silently overwritten.
|
|
581
|
+
// Recognize both env-var-ref configs and literal-key configs as auto-generated.
|
|
544
582
|
let isAutoGenerated = false;
|
|
545
583
|
try {
|
|
546
584
|
const content = fs.readFileSync(modelsPath, "utf-8");
|
|
547
|
-
isAutoGenerated = content.includes("apiKey: LITELLM_API_KEY");
|
|
585
|
+
isAutoGenerated = content.includes("Auto-generated by xcsh") || content.includes("apiKey: LITELLM_API_KEY");
|
|
548
586
|
} catch {
|
|
549
587
|
// File unreadable — skip, don't block startup
|
|
550
588
|
}
|
|
@@ -608,10 +646,18 @@ export async function probeAndUpgradeLiteLLMConfig(
|
|
|
608
646
|
modelsPath: string,
|
|
609
647
|
options?: { fetch?: typeof globalThis.fetch },
|
|
610
648
|
): Promise<boolean> {
|
|
611
|
-
|
|
649
|
+
// Try env vars first; fall back to literal key stored in the config
|
|
650
|
+
let baseUrl = getLiteLLMBaseUrl();
|
|
651
|
+
let apiKey = $env.LITELLM_API_KEY?.trim();
|
|
652
|
+
|
|
653
|
+
if (!baseUrl || !apiKey) {
|
|
654
|
+
const existing = readLiteLLMConfig(modelsPath);
|
|
655
|
+
if (existing) {
|
|
656
|
+
baseUrl = baseUrl || existing.baseUrl;
|
|
657
|
+
apiKey = apiKey || existing.apiKey;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
612
660
|
|
|
613
|
-
const baseUrl = getLiteLLMBaseUrl();
|
|
614
|
-
const apiKey = $env.LITELLM_API_KEY?.trim();
|
|
615
661
|
if (!baseUrl || !apiKey) return false;
|
|
616
662
|
|
|
617
663
|
let content: string;
|
|
@@ -624,7 +670,8 @@ export async function probeAndUpgradeLiteLLMConfig(
|
|
|
624
670
|
|
|
625
671
|
// Only upgrade configs that were auto-generated by xcsh. User-written configs
|
|
626
672
|
// with custom proxy URLs must never be silently overwritten by LiteLLM probing.
|
|
627
|
-
|
|
673
|
+
// Recognize both env-var-ref configs and literal-key configs as auto-generated.
|
|
674
|
+
if (!content.includes("Auto-generated by xcsh") && !content.includes("apiKey: LITELLM_API_KEY")) {
|
|
628
675
|
return false;
|
|
629
676
|
}
|
|
630
677
|
|
|
@@ -650,9 +697,13 @@ export async function probeAndUpgradeLiteLLMConfig(
|
|
|
650
697
|
return false; // Already correct
|
|
651
698
|
}
|
|
652
699
|
|
|
653
|
-
// Upgrade: backup and regenerate with correct base path
|
|
700
|
+
// Upgrade: backup and regenerate with correct base path, preserving literal keys
|
|
701
|
+
const existingLiteralKey = readApiKeyLiteral(modelsPath);
|
|
654
702
|
backupIfExists(modelsPath);
|
|
655
|
-
const newContent = generateModelsYml(baseUrl, {
|
|
703
|
+
const newContent = generateModelsYml(baseUrl, {
|
|
704
|
+
apiBasePath: probe.apiBasePath,
|
|
705
|
+
...(existingLiteralKey ? { apiKeyLiteral: existingLiteralKey } : {}),
|
|
706
|
+
});
|
|
656
707
|
if (!safeWrite(modelsPath, newContent)) {
|
|
657
708
|
return false;
|
|
658
709
|
}
|
|
@@ -461,12 +461,26 @@ type LlamaCppDiscoveredServerMetadata = {
|
|
|
461
461
|
* Resolve an API key config value to an actual key.
|
|
462
462
|
* Checks environment variable first, then treats as literal.
|
|
463
463
|
*/
|
|
464
|
-
function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
464
|
+
export function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
465
465
|
const envValue = Bun.env[keyConfig];
|
|
466
466
|
if (envValue) return envValue;
|
|
467
467
|
return keyConfig;
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+$/;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resolve an API key that came from YAML config (models.yml).
|
|
474
|
+
* Unlike resolveApiKeyConfig, this returns undefined for unresolved env var
|
|
475
|
+
* names to prevent sending literal names like "LITELLM_API_KEY" as Bearer tokens.
|
|
476
|
+
*/
|
|
477
|
+
export function resolveYamlApiKeyConfig(keyConfig: string): string | undefined {
|
|
478
|
+
const envValue = Bun.env[keyConfig];
|
|
479
|
+
if (envValue) return envValue;
|
|
480
|
+
if (ENV_VAR_NAME_RE.test(keyConfig)) return undefined;
|
|
481
|
+
return keyConfig;
|
|
482
|
+
}
|
|
483
|
+
|
|
470
484
|
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
471
485
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
472
486
|
return value;
|
|
@@ -67,6 +67,11 @@ async function validateModelConnection(model: Model | undefined, authStorage: Au
|
|
|
67
67
|
return { state: "auth_error", provider };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Detect unresolved env var names (e.g. "LITELLM_API_KEY" sent as literal)
|
|
71
|
+
if (/^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+$/.test(rawApiKey)) {
|
|
72
|
+
return { state: "auth_error", provider };
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
const baseUrl = model?.baseUrl;
|
|
71
76
|
if (!baseUrl) {
|
|
72
77
|
return { state: "auth_error", provider };
|
|
@@ -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,13 @@ async function executeSearch(
|
|
|
143
168
|
_toolCallId: string,
|
|
144
169
|
params: SearchQueryParams,
|
|
145
170
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
171
|
+
const hasDomainFilter = params.allowed_domains?.length || params.blocked_domains?.length;
|
|
172
|
+
const effectiveProvider =
|
|
173
|
+
hasDomainFilter && (!params.provider || params.provider === "auto") ? "anthropic" : params.provider;
|
|
146
174
|
const providers =
|
|
147
|
-
|
|
148
|
-
? (await getSearchProvider(
|
|
149
|
-
? [getSearchProvider(
|
|
175
|
+
effectiveProvider && effectiveProvider !== "auto"
|
|
176
|
+
? (await getSearchProvider(effectiveProvider).isAvailable())
|
|
177
|
+
? [getSearchProvider(effectiveProvider)]
|
|
150
178
|
: await resolveProviderChain("auto")
|
|
151
179
|
: await resolveProviderChain();
|
|
152
180
|
if (providers.length === 0) {
|
|
@@ -171,6 +199,10 @@ async function executeSearch(
|
|
|
171
199
|
maxOutputTokens: params.max_tokens,
|
|
172
200
|
numSearchResults: params.num_search_results,
|
|
173
201
|
temperature: params.temperature,
|
|
202
|
+
allowedDomains: params.allowed_domains,
|
|
203
|
+
blockedDomains: params.blocked_domains,
|
|
204
|
+
maxUses: params.max_uses,
|
|
205
|
+
userLocation: params.user_location,
|
|
174
206
|
});
|
|
175
207
|
|
|
176
208
|
const text = formatForLLM(response);
|
|
@@ -30,6 +30,14 @@ const DEFAULT_MAX_TOKENS = 4096;
|
|
|
30
30
|
const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
31
31
|
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
32
32
|
|
|
33
|
+
export interface AnthropicUserLocation {
|
|
34
|
+
type: "approximate";
|
|
35
|
+
city?: string;
|
|
36
|
+
region?: string;
|
|
37
|
+
country?: string;
|
|
38
|
+
timezone?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export interface AnthropicSearchParams {
|
|
34
42
|
query: string;
|
|
35
43
|
system_prompt?: string;
|
|
@@ -38,6 +46,31 @@ export interface AnthropicSearchParams {
|
|
|
38
46
|
max_tokens?: number;
|
|
39
47
|
/** Sampling temperature (0–1). Lower = more focused/factual. */
|
|
40
48
|
temperature?: number;
|
|
49
|
+
allowed_domains?: string[];
|
|
50
|
+
blocked_domains?: string[];
|
|
51
|
+
max_uses?: number;
|
|
52
|
+
user_location?: AnthropicUserLocation;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface WebSearchToolConfig {
|
|
56
|
+
allowed_domains?: string[];
|
|
57
|
+
blocked_domains?: string[];
|
|
58
|
+
max_uses?: number;
|
|
59
|
+
user_location?: AnthropicUserLocation;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function extractSiteOperators(query: string): { cleanQuery: string; domains: string[] } {
|
|
63
|
+
const sitePattern = /\bsite:(\S+)/gi;
|
|
64
|
+
const domains: string[] = [];
|
|
65
|
+
const cleanQuery = query
|
|
66
|
+
.replace(sitePattern, (_, domain) => {
|
|
67
|
+
domains.push(domain);
|
|
68
|
+
return "";
|
|
69
|
+
})
|
|
70
|
+
.replace(/\s{2,}/g, " ")
|
|
71
|
+
.trim();
|
|
72
|
+
const fallback = domains.length > 0 ? domains.join(" ") : query;
|
|
73
|
+
return { cleanQuery: cleanQuery || fallback, domains };
|
|
41
74
|
}
|
|
42
75
|
|
|
43
76
|
/**
|
|
@@ -86,22 +119,27 @@ async function callSearch(
|
|
|
86
119
|
systemPrompt?: string,
|
|
87
120
|
maxTokens?: number,
|
|
88
121
|
temperature?: number,
|
|
122
|
+
toolConfig?: WebSearchToolConfig,
|
|
89
123
|
): Promise<AnthropicApiResponse> {
|
|
90
124
|
const url = buildAnthropicUrl(auth);
|
|
91
125
|
const headers = buildAnthropicSearchHeaders(auth);
|
|
92
126
|
|
|
93
127
|
const systemBlocks = buildSystemBlocks(auth, model, systemPrompt);
|
|
94
128
|
|
|
129
|
+
const tool: Record<string, unknown> = {
|
|
130
|
+
type: WEB_SEARCH_TOOL_TYPE,
|
|
131
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
132
|
+
};
|
|
133
|
+
if (toolConfig?.allowed_domains?.length) tool.allowed_domains = toolConfig.allowed_domains;
|
|
134
|
+
if (toolConfig?.blocked_domains?.length) tool.blocked_domains = toolConfig.blocked_domains;
|
|
135
|
+
if (toolConfig?.max_uses) tool.max_uses = toolConfig.max_uses;
|
|
136
|
+
if (toolConfig?.user_location) tool.user_location = toolConfig.user_location;
|
|
137
|
+
|
|
95
138
|
const body: Record<string, unknown> = {
|
|
96
139
|
model,
|
|
97
140
|
max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
98
141
|
messages: [{ role: "user", content: query }],
|
|
99
|
-
tools: [
|
|
100
|
-
{
|
|
101
|
-
type: WEB_SEARCH_TOOL_TYPE,
|
|
102
|
-
name: WEB_SEARCH_TOOL_NAME,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
142
|
+
tools: [tool],
|
|
105
143
|
};
|
|
106
144
|
|
|
107
145
|
if (temperature !== undefined) {
|
|
@@ -246,13 +284,23 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<Se
|
|
|
246
284
|
}
|
|
247
285
|
|
|
248
286
|
const model = getModel();
|
|
287
|
+
|
|
288
|
+
const { cleanQuery, domains: siteDomains } = extractSiteOperators(params.query);
|
|
289
|
+
const allAllowedDomains = [...(params.allowed_domains ?? []), ...siteDomains];
|
|
290
|
+
|
|
249
291
|
const response = await callSearch(
|
|
250
292
|
auth,
|
|
251
293
|
model,
|
|
252
|
-
|
|
294
|
+
cleanQuery,
|
|
253
295
|
params.system_prompt,
|
|
254
296
|
params.max_tokens,
|
|
255
297
|
params.temperature,
|
|
298
|
+
{
|
|
299
|
+
allowed_domains: allAllowedDomains.length > 0 ? allAllowedDomains : undefined,
|
|
300
|
+
blocked_domains: params.blocked_domains,
|
|
301
|
+
max_uses: params.max_uses,
|
|
302
|
+
user_location: params.user_location,
|
|
303
|
+
},
|
|
256
304
|
);
|
|
257
305
|
|
|
258
306
|
const result = parseResponse(response);
|
|
@@ -281,6 +329,10 @@ export class AnthropicProvider extends SearchProvider {
|
|
|
281
329
|
num_results: params.numSearchResults ?? params.limit,
|
|
282
330
|
max_tokens: params.maxOutputTokens,
|
|
283
331
|
temperature: params.temperature,
|
|
332
|
+
allowed_domains: params.allowedDomains,
|
|
333
|
+
blocked_domains: params.blockedDomains,
|
|
334
|
+
max_uses: params.maxUses,
|
|
335
|
+
user_location: params.userLocation,
|
|
284
336
|
});
|
|
285
337
|
}
|
|
286
338
|
}
|
|
@@ -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>;
|