llm-cli-gateway 2.6.3 → 2.7.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/CHANGELOG.md +46 -0
- package/README.md +50 -8
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +45 -0
- package/dist/index.js +60 -0
- package/dist/provider-tool-capabilities.d.ts +97 -0
- package/dist/provider-tool-capabilities.js +1138 -0
- package/dist/resources.js +51 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +67 -6
- package/socket.yml +25 -2
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { parse as parseToml } from "smol-toml";
|
|
5
|
+
import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
|
|
6
|
+
import { getAvailableCliInfo } from "./model-registry.js";
|
|
7
|
+
import { CLI_TYPES } from "./session-manager.js";
|
|
8
|
+
import { isXaiProviderEnabled, loadProvidersConfig } from "./config.js";
|
|
9
|
+
const MAX_SKILLS_PER_DIR = 100;
|
|
10
|
+
const MAX_SKILL_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_CONFIG_BYTES = 128 * 1024;
|
|
12
|
+
const MAX_PROVIDER_TOOLS_PER_SKILL = 50;
|
|
13
|
+
const CAPABILITY_CACHE_TTL_MS = 60 * 1000;
|
|
14
|
+
const PROVIDER_CAPABILITY_IDS = [...CLI_TYPES, "grok_api"];
|
|
15
|
+
const KNOWN_PROVIDER_TOOLS = {
|
|
16
|
+
grok: [
|
|
17
|
+
"image_gen",
|
|
18
|
+
"image_edit",
|
|
19
|
+
"image_to_video",
|
|
20
|
+
"reference_to_video",
|
|
21
|
+
"run_in_background",
|
|
22
|
+
"wait_tasks",
|
|
23
|
+
"get_task_output",
|
|
24
|
+
"spawn_subagent",
|
|
25
|
+
"run_terminal_cmd",
|
|
26
|
+
"read_file",
|
|
27
|
+
"search_replace",
|
|
28
|
+
"todo_write",
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
const NOISE_TOOL_IDENTIFIERS = new Set([
|
|
32
|
+
"api_key",
|
|
33
|
+
"base_url",
|
|
34
|
+
"config_path",
|
|
35
|
+
"file_path",
|
|
36
|
+
"max_results",
|
|
37
|
+
"model_name",
|
|
38
|
+
"output_format",
|
|
39
|
+
"request_id",
|
|
40
|
+
"session_id",
|
|
41
|
+
"short_description",
|
|
42
|
+
]);
|
|
43
|
+
const TOOL_CONTROLS = {
|
|
44
|
+
claude: {
|
|
45
|
+
providerKind: "cli",
|
|
46
|
+
gatewayRequestTools: ["claude_request", "claude_request_async"],
|
|
47
|
+
summary: "Claude Code owns its runtime tool catalog; the gateway can pass permission and built-in tool restrictions through to the CLI.",
|
|
48
|
+
controls: {
|
|
49
|
+
allowlist: {
|
|
50
|
+
supported: true,
|
|
51
|
+
requestField: "allowedTools",
|
|
52
|
+
cliFlag: "--allowed-tools",
|
|
53
|
+
behavior: "Each entry is passed through as a Claude permission allow rule.",
|
|
54
|
+
},
|
|
55
|
+
denylist: {
|
|
56
|
+
supported: true,
|
|
57
|
+
requestField: "disallowedTools",
|
|
58
|
+
cliFlag: "--disallowed-tools",
|
|
59
|
+
behavior: "Each entry is passed through as a Claude permission deny rule.",
|
|
60
|
+
},
|
|
61
|
+
mcpServers: {
|
|
62
|
+
supported: true,
|
|
63
|
+
requestField: "mcpServers",
|
|
64
|
+
behavior: "Gateway can generate a Claude MCP config for selected gateway-known MCP servers.",
|
|
65
|
+
},
|
|
66
|
+
nativeSkills: {
|
|
67
|
+
supported: true,
|
|
68
|
+
behavior: "Gateway discovers local Claude skills from ~/.claude/skills for capability reporting.",
|
|
69
|
+
},
|
|
70
|
+
tools: {
|
|
71
|
+
supported: true,
|
|
72
|
+
requestField: "tools",
|
|
73
|
+
cliFlag: "--tools",
|
|
74
|
+
behavior: "Restricts Claude's available built-in tool catalog.",
|
|
75
|
+
},
|
|
76
|
+
permissionMode: {
|
|
77
|
+
supported: true,
|
|
78
|
+
requestField: "permissionMode",
|
|
79
|
+
cliFlag: "--permission-mode",
|
|
80
|
+
behavior: "Passes Claude permission-mode values through to the CLI.",
|
|
81
|
+
},
|
|
82
|
+
approvalStrategy: {
|
|
83
|
+
supported: true,
|
|
84
|
+
requestField: "approvalStrategy",
|
|
85
|
+
behavior: "Gateway approval strategy controls MCP-managed permission gating.",
|
|
86
|
+
},
|
|
87
|
+
approvalPolicy: {
|
|
88
|
+
supported: true,
|
|
89
|
+
requestField: "approvalPolicy",
|
|
90
|
+
behavior: "Gateway approval policy tunes MCP-managed review strictness.",
|
|
91
|
+
},
|
|
92
|
+
strictMcpConfig: {
|
|
93
|
+
supported: true,
|
|
94
|
+
requestField: "strictMcpConfig",
|
|
95
|
+
behavior: "Restricts Claude to the generated MCP config when mcpServers is used.",
|
|
96
|
+
},
|
|
97
|
+
agents: {
|
|
98
|
+
supported: true,
|
|
99
|
+
requestField: "agent/agents",
|
|
100
|
+
cliFlag: "--agent/--agents",
|
|
101
|
+
behavior: "Passes single-agent or inline multi-agent definitions to Claude.",
|
|
102
|
+
},
|
|
103
|
+
structuredOutput: {
|
|
104
|
+
supported: true,
|
|
105
|
+
requestField: "outputFormat/jsonSchema",
|
|
106
|
+
cliFlag: "--output-format/--json-schema",
|
|
107
|
+
behavior: "Supports text, json, stream-json, and optional JSON schema.",
|
|
108
|
+
},
|
|
109
|
+
workspace: {
|
|
110
|
+
supported: true,
|
|
111
|
+
requestField: "addDir/workspace/worktree",
|
|
112
|
+
behavior: "Gateway resolves additional directories, workspace aliases, and worktrees.",
|
|
113
|
+
},
|
|
114
|
+
session: {
|
|
115
|
+
supported: true,
|
|
116
|
+
requestField: "continueSession/sessionId/forkSession/noSessionPersistence/settings/settingSources",
|
|
117
|
+
behavior: "Supports Claude session continuation, forks, ephemeral runs, and settings.",
|
|
118
|
+
},
|
|
119
|
+
loopAndBudget: {
|
|
120
|
+
supported: true,
|
|
121
|
+
requestField: "maxTurns/maxBudgetUsd/effort/fallbackModel",
|
|
122
|
+
behavior: "Passes Claude loop, budget, effort, and fallback-model controls.",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
features: baseFeatures({
|
|
126
|
+
nativeSkills: true,
|
|
127
|
+
mcpServerConfiguration: true,
|
|
128
|
+
subagentsOrAgents: true,
|
|
129
|
+
structuredOutput: true,
|
|
130
|
+
sessionContinuity: true,
|
|
131
|
+
approvalAndSandboxControls: true,
|
|
132
|
+
costAndLoopControls: true,
|
|
133
|
+
workspaceAndWorktreeControls: true,
|
|
134
|
+
toolAllowDenyControls: true,
|
|
135
|
+
}),
|
|
136
|
+
unsupportedInputs: [
|
|
137
|
+
{
|
|
138
|
+
input: "dangerouslySkipPermissions",
|
|
139
|
+
behavior: "deprecated",
|
|
140
|
+
details: "Accepted for compatibility; prefer permissionMode=bypassPermissions.",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
codex: {
|
|
145
|
+
providerKind: "cli",
|
|
146
|
+
gatewayRequestTools: ["codex_request", "codex_request_async", "codex_fork_session"],
|
|
147
|
+
summary: "Codex owns its runtime tool catalog and MCP configuration; the gateway reports local skills and passes Codex execution controls.",
|
|
148
|
+
controls: {
|
|
149
|
+
allowlist: {
|
|
150
|
+
supported: false,
|
|
151
|
+
behavior: "codex_request has no allowedTools input; use Codex configuration/profiles.",
|
|
152
|
+
},
|
|
153
|
+
denylist: {
|
|
154
|
+
supported: false,
|
|
155
|
+
behavior: "codex_request has no disallowedTools input; use Codex configuration/profiles.",
|
|
156
|
+
},
|
|
157
|
+
mcpServers: {
|
|
158
|
+
supported: false,
|
|
159
|
+
requestField: "mcpServers",
|
|
160
|
+
behavior: "Accepted for approval tracking only; Codex manages its own MCP configuration outside the gateway.",
|
|
161
|
+
},
|
|
162
|
+
nativeSkills: {
|
|
163
|
+
supported: true,
|
|
164
|
+
behavior: "Gateway discovers local Codex skills from ~/.codex/skills for capability reporting.",
|
|
165
|
+
},
|
|
166
|
+
sandboxMode: {
|
|
167
|
+
supported: true,
|
|
168
|
+
requestField: "sandboxMode",
|
|
169
|
+
cliFlag: "--sandbox",
|
|
170
|
+
behavior: "Passes Codex sandbox mode through to the CLI.",
|
|
171
|
+
},
|
|
172
|
+
fullAuto: {
|
|
173
|
+
supported: true,
|
|
174
|
+
requestField: "fullAuto",
|
|
175
|
+
behavior: "Gateway convenience mode for autonomous Codex execution.",
|
|
176
|
+
},
|
|
177
|
+
askForApproval: {
|
|
178
|
+
supported: true,
|
|
179
|
+
requestField: "askForApproval",
|
|
180
|
+
cliFlag: "--ask-for-approval",
|
|
181
|
+
behavior: "Passes Codex approval prompting policy through to the CLI.",
|
|
182
|
+
},
|
|
183
|
+
bypassApprovalsAndSandbox: {
|
|
184
|
+
supported: true,
|
|
185
|
+
requestField: "dangerouslyBypassApprovalsAndSandbox",
|
|
186
|
+
behavior: "Explicit high-risk Codex bypass control.",
|
|
187
|
+
},
|
|
188
|
+
profileAndConfig: {
|
|
189
|
+
supported: true,
|
|
190
|
+
requestField: "profile/configOverrides/ignoreUserConfig/ignoreRules",
|
|
191
|
+
behavior: "Passes Codex profile and config override controls.",
|
|
192
|
+
},
|
|
193
|
+
structuredOutput: {
|
|
194
|
+
supported: true,
|
|
195
|
+
requestField: "outputFormat/outputSchema",
|
|
196
|
+
behavior: "Supports Codex JSON output and output schema.",
|
|
197
|
+
},
|
|
198
|
+
images: {
|
|
199
|
+
supported: true,
|
|
200
|
+
requestField: "images",
|
|
201
|
+
cliFlag: "-i",
|
|
202
|
+
behavior: "Passes image attachment paths to Codex after existence checks.",
|
|
203
|
+
},
|
|
204
|
+
workspace: {
|
|
205
|
+
supported: true,
|
|
206
|
+
requestField: "workingDir/addDir/workspace/worktree",
|
|
207
|
+
behavior: "Gateway resolves Codex working directories, writable dirs, and worktrees.",
|
|
208
|
+
},
|
|
209
|
+
session: {
|
|
210
|
+
supported: true,
|
|
211
|
+
requestField: "sessionId/resumeLatest/createNewSession/ephemeral",
|
|
212
|
+
behavior: "Supports Codex resume/latest/new-session and ephemeral controls.",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
features: baseFeatures({
|
|
216
|
+
nativeSkills: true,
|
|
217
|
+
mcpServerConfiguration: true,
|
|
218
|
+
multimodalInputs: true,
|
|
219
|
+
structuredOutput: true,
|
|
220
|
+
sessionContinuity: true,
|
|
221
|
+
approvalAndSandboxControls: true,
|
|
222
|
+
workspaceAndWorktreeControls: true,
|
|
223
|
+
}),
|
|
224
|
+
unsupportedInputs: [
|
|
225
|
+
{
|
|
226
|
+
input: "allowedTools",
|
|
227
|
+
behavior: "not_supported",
|
|
228
|
+
details: "codex_request has no gateway allowedTools input.",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
input: "disallowedTools",
|
|
232
|
+
behavior: "not_supported",
|
|
233
|
+
details: "codex_request has no gateway disallowedTools input.",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
input: "mcpServers",
|
|
237
|
+
behavior: "approval_tracking_only",
|
|
238
|
+
details: "Accepted only for gateway approval tracking; Codex owns MCP configuration.",
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
gemini: {
|
|
243
|
+
providerKind: "cli",
|
|
244
|
+
gatewayRequestTools: ["gemini_request", "gemini_request_async"],
|
|
245
|
+
summary: "Antigravity/Gemini owns its runtime tool catalog; this gateway rejects non-empty tool allow-list and MCP-server inputs for that CLI.",
|
|
246
|
+
controls: {
|
|
247
|
+
allowlist: {
|
|
248
|
+
supported: false,
|
|
249
|
+
requestField: "allowedTools",
|
|
250
|
+
behavior: "Non-empty values are rejected because agy has no non-interactive allowed-tools flag.",
|
|
251
|
+
},
|
|
252
|
+
denylist: {
|
|
253
|
+
supported: false,
|
|
254
|
+
behavior: "gemini_request has no disallowedTools input.",
|
|
255
|
+
},
|
|
256
|
+
mcpServers: {
|
|
257
|
+
supported: false,
|
|
258
|
+
requestField: "mcpServers",
|
|
259
|
+
behavior: "Non-empty values are rejected; Antigravity CLI manages tool access itself.",
|
|
260
|
+
},
|
|
261
|
+
nativeSkills: {
|
|
262
|
+
supported: true,
|
|
263
|
+
behavior: "Gateway discovers local Gemini skills from ~/.gemini/skills for capability reporting.",
|
|
264
|
+
},
|
|
265
|
+
approvalMode: {
|
|
266
|
+
supported: true,
|
|
267
|
+
requestField: "approvalMode/yolo",
|
|
268
|
+
behavior: "Passes Antigravity approval mode when supported by the gateway path.",
|
|
269
|
+
},
|
|
270
|
+
sandbox: {
|
|
271
|
+
supported: true,
|
|
272
|
+
requestField: "sandbox",
|
|
273
|
+
cliFlag: "-s",
|
|
274
|
+
behavior: "Runs Gemini/Antigravity in sandbox mode.",
|
|
275
|
+
},
|
|
276
|
+
workspace: {
|
|
277
|
+
supported: true,
|
|
278
|
+
requestField: "includeDirs/workspace/worktree",
|
|
279
|
+
behavior: "Gateway resolves include dirs, workspace aliases, and worktrees.",
|
|
280
|
+
},
|
|
281
|
+
session: {
|
|
282
|
+
supported: true,
|
|
283
|
+
requestField: "sessionId/resumeLatest/createNewSession",
|
|
284
|
+
behavior: "Supports Gemini/Antigravity session continuation controls.",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
features: baseFeatures({
|
|
288
|
+
nativeSkills: true,
|
|
289
|
+
sessionContinuity: true,
|
|
290
|
+
approvalAndSandboxControls: true,
|
|
291
|
+
workspaceAndWorktreeControls: true,
|
|
292
|
+
}),
|
|
293
|
+
unsupportedInputs: [
|
|
294
|
+
{
|
|
295
|
+
input: "allowedTools",
|
|
296
|
+
behavior: "reject",
|
|
297
|
+
details: "Non-empty allowedTools values are rejected for the current Antigravity path.",
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
input: "mcpServers",
|
|
301
|
+
behavior: "reject",
|
|
302
|
+
details: "Non-empty mcpServers values are rejected for the current Antigravity path.",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
input: "attachments",
|
|
306
|
+
behavior: "reject",
|
|
307
|
+
details: "Attachments are not supported by the current Antigravity request path.",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
input: "outputFormat=json/stream-json",
|
|
311
|
+
behavior: "reject",
|
|
312
|
+
details: "The current Antigravity print path accepts text output only.",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
input: "policyFiles/adminPolicyFiles/skipTrust",
|
|
316
|
+
behavior: "not_supported",
|
|
317
|
+
details: "Policy and trust-bypass files are not supported by this gateway path.",
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
grok: {
|
|
322
|
+
providerKind: "cli",
|
|
323
|
+
gatewayRequestTools: ["grok_request", "grok_request_async"],
|
|
324
|
+
summary: "Grok Build owns its runtime tool catalog; the gateway can pass Grok tool allow/deny controls and reports local Grok skills such as Imagine.",
|
|
325
|
+
controls: {
|
|
326
|
+
allowlist: {
|
|
327
|
+
supported: true,
|
|
328
|
+
requestField: "allowedTools",
|
|
329
|
+
cliFlag: "--tools",
|
|
330
|
+
behavior: "Non-empty entries are passed as a comma-separated Grok built-in tool allow-list.",
|
|
331
|
+
},
|
|
332
|
+
denylist: {
|
|
333
|
+
supported: true,
|
|
334
|
+
requestField: "disallowedTools",
|
|
335
|
+
cliFlag: "--disallowed-tools",
|
|
336
|
+
behavior: "Non-empty entries are passed as a comma-separated Grok built-in tool deny-list.",
|
|
337
|
+
},
|
|
338
|
+
mcpServers: {
|
|
339
|
+
supported: false,
|
|
340
|
+
requestField: "mcpServers",
|
|
341
|
+
behavior: "Accepted for approval tracking only; Grok manages its own MCP configuration via grok mcp.",
|
|
342
|
+
},
|
|
343
|
+
nativeSkills: {
|
|
344
|
+
supported: true,
|
|
345
|
+
behavior: "Gateway discovers local Grok skills from ~/.grok/skills and bundled Grok skills for capability reporting.",
|
|
346
|
+
},
|
|
347
|
+
allowAliases: {
|
|
348
|
+
supported: true,
|
|
349
|
+
requestField: "allow/deny",
|
|
350
|
+
cliFlag: "--allow/--deny",
|
|
351
|
+
behavior: "Passes Grok allow/deny aliases when provided by the request schema.",
|
|
352
|
+
},
|
|
353
|
+
alwaysApprove: {
|
|
354
|
+
supported: true,
|
|
355
|
+
requestField: "alwaysApprove",
|
|
356
|
+
cliFlag: "--always-approve",
|
|
357
|
+
behavior: "Asks Grok to auto-approve tool executions.",
|
|
358
|
+
},
|
|
359
|
+
permissionAndApproval: {
|
|
360
|
+
supported: true,
|
|
361
|
+
requestField: "permissionMode/approvalStrategy/approvalPolicy",
|
|
362
|
+
behavior: "Combines Grok CLI permission mode with gateway approval controls.",
|
|
363
|
+
},
|
|
364
|
+
agents: {
|
|
365
|
+
supported: true,
|
|
366
|
+
requestField: "agent/agents/bestOfN/check/todoGate/noSubagents",
|
|
367
|
+
behavior: "Surfaces Grok agent, evaluation, and subagent controls.",
|
|
368
|
+
},
|
|
369
|
+
webSearch: {
|
|
370
|
+
supported: true,
|
|
371
|
+
requestField: "disableWebSearch",
|
|
372
|
+
behavior: "Controls Grok web-search availability when supported by the CLI.",
|
|
373
|
+
},
|
|
374
|
+
memoryAndPlan: {
|
|
375
|
+
supported: true,
|
|
376
|
+
requestField: "experimentalMemory/noMemory/noPlan/noAltScreen",
|
|
377
|
+
behavior: "Surfaces Grok memory, planning, and alternate-screen controls.",
|
|
378
|
+
},
|
|
379
|
+
promptControl: {
|
|
380
|
+
supported: true,
|
|
381
|
+
requestField: "promptFile/promptJson/single/verbatim/systemPromptOverride/rules",
|
|
382
|
+
behavior: "Surfaces Grok prompt-file, JSON prompt, single-run, verbatim, and rules controls.",
|
|
383
|
+
},
|
|
384
|
+
outputFormat: {
|
|
385
|
+
supported: true,
|
|
386
|
+
requestField: "outputFormat",
|
|
387
|
+
behavior: "Supports Grok plain, json, and streaming-json output modes.",
|
|
388
|
+
},
|
|
389
|
+
workspace: {
|
|
390
|
+
supported: true,
|
|
391
|
+
requestField: "sandbox/workingDir/workspace/worktree/nativeWorktree",
|
|
392
|
+
behavior: "Surfaces Grok sandbox, gateway workspace/worktree, and native worktree controls.",
|
|
393
|
+
},
|
|
394
|
+
session: {
|
|
395
|
+
supported: true,
|
|
396
|
+
requestField: "sessionId/resumeLatest/createNewSession/restoreCode/leaderSocket",
|
|
397
|
+
behavior: "Surfaces Grok resume, new-session, restore-code, and leader-socket controls.",
|
|
398
|
+
},
|
|
399
|
+
loopAndCompaction: {
|
|
400
|
+
supported: true,
|
|
401
|
+
requestField: "maxTurns/effort/reasoningEffort/compactionMode/compactionDetail",
|
|
402
|
+
behavior: "Surfaces Grok loop, effort, reasoning, and compaction controls.",
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
features: baseFeatures({
|
|
406
|
+
nativeSkills: true,
|
|
407
|
+
providerNativeTools: true,
|
|
408
|
+
subagentsOrAgents: true,
|
|
409
|
+
webSearchOrRemoteRetrieval: true,
|
|
410
|
+
memoryControls: true,
|
|
411
|
+
sessionContinuity: true,
|
|
412
|
+
approvalAndSandboxControls: true,
|
|
413
|
+
costAndLoopControls: true,
|
|
414
|
+
workspaceAndWorktreeControls: true,
|
|
415
|
+
toolAllowDenyControls: true,
|
|
416
|
+
webSearchControl: true,
|
|
417
|
+
memoryControl: true,
|
|
418
|
+
promptControl: true,
|
|
419
|
+
compactionControls: true,
|
|
420
|
+
}),
|
|
421
|
+
unsupportedInputs: [
|
|
422
|
+
{
|
|
423
|
+
input: "mcpServers",
|
|
424
|
+
behavior: "approval_tracking_only",
|
|
425
|
+
details: "Accepted only for gateway approval tracking; Grok owns MCP configuration.",
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
mistral: {
|
|
430
|
+
providerKind: "cli",
|
|
431
|
+
gatewayRequestTools: ["mistral_request", "mistral_request_async"],
|
|
432
|
+
summary: "Mistral Vibe owns its runtime tool catalog; the gateway can pass Vibe enabled-tool controls and reports local skills if present.",
|
|
433
|
+
controls: {
|
|
434
|
+
allowlist: {
|
|
435
|
+
supported: true,
|
|
436
|
+
requestField: "allowedTools",
|
|
437
|
+
cliFlag: "--enabled-tools",
|
|
438
|
+
behavior: "Each entry is emitted as a separate Vibe enabled-tool flag.",
|
|
439
|
+
},
|
|
440
|
+
denylist: {
|
|
441
|
+
supported: false,
|
|
442
|
+
requestField: "disallowedTools",
|
|
443
|
+
behavior: "Accepted for caller parity but ignored because Vibe has no deny-list flag.",
|
|
444
|
+
},
|
|
445
|
+
mcpServers: {
|
|
446
|
+
supported: false,
|
|
447
|
+
requestField: "mcpServers",
|
|
448
|
+
behavior: "Accepted for approval tracking only; Vibe manages its own MCP configuration via vibe mcp.",
|
|
449
|
+
},
|
|
450
|
+
nativeSkills: {
|
|
451
|
+
supported: true,
|
|
452
|
+
behavior: "Gateway discovers local Vibe skills from ~/.vibe/skills when that directory exists.",
|
|
453
|
+
},
|
|
454
|
+
permissionMode: {
|
|
455
|
+
supported: true,
|
|
456
|
+
requestField: "permissionMode",
|
|
457
|
+
behavior: "Passes Vibe agent modes such as plan, auto-approve, chat, explore, and lean.",
|
|
458
|
+
},
|
|
459
|
+
outputFormat: {
|
|
460
|
+
supported: true,
|
|
461
|
+
requestField: "outputFormat",
|
|
462
|
+
behavior: "Supports text/plain, json, streaming, and stream-json aliases.",
|
|
463
|
+
},
|
|
464
|
+
trust: {
|
|
465
|
+
supported: true,
|
|
466
|
+
requestField: "trust",
|
|
467
|
+
cliFlag: "--trust",
|
|
468
|
+
behavior: "Passes Vibe trust mode for headless workspace runs.",
|
|
469
|
+
},
|
|
470
|
+
costAndLoop: {
|
|
471
|
+
supported: true,
|
|
472
|
+
requestField: "maxTurns/maxPrice/maxTokens",
|
|
473
|
+
behavior: "Surfaces Vibe loop, price, and token limits.",
|
|
474
|
+
},
|
|
475
|
+
workspace: {
|
|
476
|
+
supported: true,
|
|
477
|
+
requestField: "workingDir/addDir/workspace/worktree",
|
|
478
|
+
behavior: "Gateway resolves Vibe working dirs, add-dir entries, workspaces, and worktrees.",
|
|
479
|
+
},
|
|
480
|
+
session: {
|
|
481
|
+
supported: true,
|
|
482
|
+
requestField: "sessionId/resumeLatest/createNewSession",
|
|
483
|
+
behavior: "Supports Vibe session resume/latest/new-session controls.",
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
features: baseFeatures({
|
|
487
|
+
nativeSkills: true,
|
|
488
|
+
sessionContinuity: true,
|
|
489
|
+
approvalAndSandboxControls: true,
|
|
490
|
+
costAndLoopControls: true,
|
|
491
|
+
workspaceAndWorktreeControls: true,
|
|
492
|
+
enabledToolAllowlist: true,
|
|
493
|
+
trustControl: true,
|
|
494
|
+
}),
|
|
495
|
+
unsupportedInputs: [
|
|
496
|
+
{
|
|
497
|
+
input: "disallowedTools",
|
|
498
|
+
behavior: "ignored",
|
|
499
|
+
details: "Accepted for caller parity but ignored because Vibe has no deny-list flag.",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
input: "mcpServers",
|
|
503
|
+
behavior: "approval_tracking_only",
|
|
504
|
+
details: "Accepted only for gateway approval tracking; Vibe owns MCP configuration.",
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
input: "effort/reasoningEffort",
|
|
508
|
+
behavior: "not_supported",
|
|
509
|
+
details: "No Vibe reasoning-effort control is currently passed through by the gateway.",
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
grok_api: {
|
|
514
|
+
providerKind: "api",
|
|
515
|
+
gatewayRequestTools: ["grok_api_request"],
|
|
516
|
+
summary: "Optional xAI Grok Responses API provider. This is distinct from Grok CLI/Build and does not expose local Grok skills or Imagine tools.",
|
|
517
|
+
controls: {
|
|
518
|
+
allowlist: {
|
|
519
|
+
supported: false,
|
|
520
|
+
behavior: "grok_api_request has no CLI tool allow-list input.",
|
|
521
|
+
},
|
|
522
|
+
denylist: {
|
|
523
|
+
supported: false,
|
|
524
|
+
behavior: "grok_api_request has no CLI tool deny-list input.",
|
|
525
|
+
},
|
|
526
|
+
mcpServers: {
|
|
527
|
+
supported: false,
|
|
528
|
+
behavior: "grok_api_request does not configure or expose MCP servers.",
|
|
529
|
+
},
|
|
530
|
+
nativeSkills: {
|
|
531
|
+
supported: false,
|
|
532
|
+
behavior: "API requests do not read local provider skills.",
|
|
533
|
+
},
|
|
534
|
+
reasoningEffort: {
|
|
535
|
+
supported: true,
|
|
536
|
+
requestField: "reasoningEffort",
|
|
537
|
+
behavior: "Passed to the xAI Responses API reasoning.effort field.",
|
|
538
|
+
},
|
|
539
|
+
maxOutputTokens: {
|
|
540
|
+
supported: true,
|
|
541
|
+
requestField: "maxOutputTokens",
|
|
542
|
+
behavior: "Passed to the xAI Responses API max_output_tokens field.",
|
|
543
|
+
},
|
|
544
|
+
sampling: {
|
|
545
|
+
supported: true,
|
|
546
|
+
requestField: "temperature/topP",
|
|
547
|
+
behavior: "Sampling controls are passed through to the xAI Responses API.",
|
|
548
|
+
},
|
|
549
|
+
timeout: {
|
|
550
|
+
supported: true,
|
|
551
|
+
requestField: "timeoutMs",
|
|
552
|
+
behavior: "Bounds the xAI API HTTP request timeout.",
|
|
553
|
+
},
|
|
554
|
+
session: {
|
|
555
|
+
supported: true,
|
|
556
|
+
requestField: "sessionId/createNewSession",
|
|
557
|
+
behavior: "Gateway stores xAI previous_response_id in session metadata.",
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
features: baseFeatures({
|
|
561
|
+
apiProvider: true,
|
|
562
|
+
structuredOutput: true,
|
|
563
|
+
sessionContinuity: true,
|
|
564
|
+
structuredTextResponses: true,
|
|
565
|
+
}),
|
|
566
|
+
unsupportedInputs: [
|
|
567
|
+
{
|
|
568
|
+
input: "localSkills",
|
|
569
|
+
behavior: "not_supported",
|
|
570
|
+
details: "grok_api_request does not inspect local Grok CLI skills.",
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
input: "allowedTools/disallowedTools",
|
|
574
|
+
behavior: "not_supported",
|
|
575
|
+
details: "Tool allow/deny controls are CLI-only and are not routed to the xAI API.",
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
input: "workspace/worktree",
|
|
579
|
+
behavior: "not_supported",
|
|
580
|
+
details: "The xAI API provider has no local workspace or worktree controls.",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
input: "Grok Imagine image generation",
|
|
584
|
+
behavior: "not_supported",
|
|
585
|
+
details: "Image generation/editing is not routed through grok_api_request in the current gateway.",
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
let capabilityCache = new Map();
|
|
591
|
+
export function getProviderToolCapabilities(queryOrCli = {}) {
|
|
592
|
+
const query = normalizeQuery(queryOrCli);
|
|
593
|
+
const providers = query.cli ? [query.cli] : PROVIDER_CAPABILITY_IDS;
|
|
594
|
+
const entries = providers.map(provider => [
|
|
595
|
+
provider,
|
|
596
|
+
getOneProviderToolCapabilities(provider, query),
|
|
597
|
+
]);
|
|
598
|
+
return Object.fromEntries(entries);
|
|
599
|
+
}
|
|
600
|
+
export function getOneProviderToolCapabilities(cli, queryOrCli = {}) {
|
|
601
|
+
const query = normalizeQuery(typeof queryOrCli === "string" ? { cli: queryOrCli } : queryOrCli);
|
|
602
|
+
const cacheKey = capabilityCacheKey(cli, query);
|
|
603
|
+
const cached = capabilityCache.get(cacheKey);
|
|
604
|
+
if (!query.refresh && cached && Date.now() - cached.loadedAt < CAPABILITY_CACHE_TTL_MS) {
|
|
605
|
+
return cached.value;
|
|
606
|
+
}
|
|
607
|
+
const value = buildOneProviderToolCapabilities(cli, query);
|
|
608
|
+
capabilityCache.set(cacheKey, { loadedAt: Date.now(), value });
|
|
609
|
+
return value;
|
|
610
|
+
}
|
|
611
|
+
export function clearProviderToolCapabilitiesCache() {
|
|
612
|
+
capabilityCache = new Map();
|
|
613
|
+
}
|
|
614
|
+
export function providerCapabilityIds() {
|
|
615
|
+
return PROVIDER_CAPABILITY_IDS;
|
|
616
|
+
}
|
|
617
|
+
function buildOneProviderToolCapabilities(cli, query) {
|
|
618
|
+
const warnings = [];
|
|
619
|
+
const definition = TOOL_CONTROLS[cli];
|
|
620
|
+
const discoveredSkills = query.includeSkills && cli !== "grok_api" ? discoverSkills(cli, warnings, query) : [];
|
|
621
|
+
const discoveredProviderTools = query.includeProviderTools
|
|
622
|
+
? extractProviderTools(cli, discoveredSkills)
|
|
623
|
+
: [];
|
|
624
|
+
const features = { ...definition.features };
|
|
625
|
+
const gatewayRequestTools = cli === "grok_api" && !isXaiProviderEnabled(loadProvidersConfig())
|
|
626
|
+
? []
|
|
627
|
+
: [...definition.gatewayRequestTools];
|
|
628
|
+
if (cli === "grok") {
|
|
629
|
+
features.mediaGenerationOrEditing = {
|
|
630
|
+
supported: discoveredProviderTools.some(tool => ["image_gen", "image_edit", "image_to_video", "reference_to_video"].includes(tool.name)),
|
|
631
|
+
details: "True when Grok Imagine tools are discovered from local Grok skills.",
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
schemaVersion: "provider-tool-capabilities.v2",
|
|
636
|
+
generatedAt: new Date().toISOString(),
|
|
637
|
+
cli,
|
|
638
|
+
providerKind: definition.providerKind,
|
|
639
|
+
gatewayRequestTools,
|
|
640
|
+
gatewayRequestTool: gatewayRequestTools[0] ?? definition.gatewayRequestTools[0],
|
|
641
|
+
modelInfo: getModelInfo(cli, query.refresh),
|
|
642
|
+
summary: definition.summary,
|
|
643
|
+
controls: cloneControls(definition.controls),
|
|
644
|
+
features,
|
|
645
|
+
discoveredSkills,
|
|
646
|
+
discoveredProviderTools,
|
|
647
|
+
configSurfaces: discoverConfigSurfaces(cli, query, discoveredSkills),
|
|
648
|
+
unsupportedInputs: query.includeUnsupported ? [...definition.unsupportedInputs] : [],
|
|
649
|
+
warnings,
|
|
650
|
+
metadata: {
|
|
651
|
+
deprecatedFields: {
|
|
652
|
+
gatewayRequestTool: "Use gatewayRequestTools instead.",
|
|
653
|
+
},
|
|
654
|
+
cacheTtlMs: CAPABILITY_CACHE_TTL_MS,
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function discoverSkills(cli, warnings, query) {
|
|
659
|
+
const skills = [];
|
|
660
|
+
for (const root of skillRoots(cli)) {
|
|
661
|
+
if (!existsSync(root.path))
|
|
662
|
+
continue;
|
|
663
|
+
let entries;
|
|
664
|
+
try {
|
|
665
|
+
entries = readdirSync(root.path, { withFileTypes: true });
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
warnings.push(`Could not read skill directory ${formatPathForOutput(root.path, query)}: ${formatError(error)}`);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
for (const entry of entries.filter(item => item.isDirectory()).slice(0, MAX_SKILLS_PER_DIR)) {
|
|
672
|
+
const skillPath = path.join(root.path, entry.name, "SKILL.md");
|
|
673
|
+
if (!existsSync(skillPath))
|
|
674
|
+
continue;
|
|
675
|
+
const parsed = readSkill(cli, skillPath, entry.name, root.source, warnings, query);
|
|
676
|
+
if (parsed)
|
|
677
|
+
skills.push(parsed);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
681
|
+
}
|
|
682
|
+
function skillRoots(cli) {
|
|
683
|
+
const home = process.env.LLM_GATEWAY_TOOL_DISCOVERY_HOME || homedir();
|
|
684
|
+
switch (cli) {
|
|
685
|
+
case "claude":
|
|
686
|
+
return [{ path: path.join(home, ".claude", "skills"), source: "user" }];
|
|
687
|
+
case "codex":
|
|
688
|
+
return [{ path: path.join(home, ".codex", "skills"), source: "user" }];
|
|
689
|
+
case "gemini":
|
|
690
|
+
return [{ path: path.join(home, ".gemini", "skills"), source: "user" }];
|
|
691
|
+
case "grok":
|
|
692
|
+
return [
|
|
693
|
+
{ path: path.join(home, ".grok", "skills"), source: "user" },
|
|
694
|
+
{ path: path.join(home, ".grok", "bundled", "skills"), source: "bundled" },
|
|
695
|
+
];
|
|
696
|
+
case "mistral":
|
|
697
|
+
return [{ path: path.join(home, ".vibe", "skills"), source: "user" }];
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function readSkill(cli, skillPath, fallbackName, source, warnings, query) {
|
|
701
|
+
try {
|
|
702
|
+
const stat = statSync(skillPath);
|
|
703
|
+
if (!stat.isFile())
|
|
704
|
+
return null;
|
|
705
|
+
const content = readFileSync(skillPath, "utf8").slice(0, MAX_SKILL_BYTES);
|
|
706
|
+
const extractedTools = extractDeclaredTools(cli, content);
|
|
707
|
+
return {
|
|
708
|
+
name: extractFrontmatterValue(content, "name") ?? fallbackName,
|
|
709
|
+
source,
|
|
710
|
+
path: query.includePaths ? skillPath : undefined,
|
|
711
|
+
description: extractFrontmatterValue(content, "description") ??
|
|
712
|
+
extractFrontmatterValue(content, "metadata.short-description") ??
|
|
713
|
+
extractFrontmatterValue(content, "short-description") ??
|
|
714
|
+
extractFirstHeading(content),
|
|
715
|
+
declaredTools: extractedTools.tools,
|
|
716
|
+
declaredToolReasons: extractedTools.reasons,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
warnings.push(`Could not read skill ${formatPathForOutput(skillPath, query)}: ${formatError(error)}`);
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function normalizeQuery(queryOrCli) {
|
|
725
|
+
const query = typeof queryOrCli === "string" ? { cli: queryOrCli } : queryOrCli;
|
|
726
|
+
return {
|
|
727
|
+
cli: query.cli,
|
|
728
|
+
includeSkills: query.includeSkills ?? true,
|
|
729
|
+
includeProviderTools: query.includeProviderTools ?? true,
|
|
730
|
+
includeUnsupported: query.includeUnsupported ?? true,
|
|
731
|
+
includePaths: query.includePaths ?? false,
|
|
732
|
+
refresh: query.refresh ?? false,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function capabilityCacheKey(cli, query) {
|
|
736
|
+
return JSON.stringify({
|
|
737
|
+
cli,
|
|
738
|
+
includeSkills: query.includeSkills,
|
|
739
|
+
includeProviderTools: query.includeProviderTools,
|
|
740
|
+
includeUnsupported: query.includeUnsupported,
|
|
741
|
+
includePaths: query.includePaths,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function baseFeatures(overrides) {
|
|
745
|
+
const names = [
|
|
746
|
+
"gatewayRequestTools",
|
|
747
|
+
"modelDefaultsAndAliases",
|
|
748
|
+
"toolAllowDenyControls",
|
|
749
|
+
"mcpServerConfiguration",
|
|
750
|
+
"nativeSkills",
|
|
751
|
+
"providerNativeTools",
|
|
752
|
+
"multimodalInputs",
|
|
753
|
+
"mediaGenerationOrEditing",
|
|
754
|
+
"structuredOutput",
|
|
755
|
+
"subagentsOrAgents",
|
|
756
|
+
"webSearchOrRemoteRetrieval",
|
|
757
|
+
"memoryControls",
|
|
758
|
+
"workspaceAndWorktreeControls",
|
|
759
|
+
"sessionContinuity",
|
|
760
|
+
"approvalAndSandboxControls",
|
|
761
|
+
"costAndLoopControls",
|
|
762
|
+
"unsupportedOrDegradedInputs",
|
|
763
|
+
"apiProvider",
|
|
764
|
+
"structuredTextResponses",
|
|
765
|
+
"webSearchControl",
|
|
766
|
+
"memoryControl",
|
|
767
|
+
"promptControl",
|
|
768
|
+
"compactionControls",
|
|
769
|
+
"trustControl",
|
|
770
|
+
"enabledToolAllowlist",
|
|
771
|
+
];
|
|
772
|
+
return Object.fromEntries(names.map(name => [
|
|
773
|
+
name,
|
|
774
|
+
{
|
|
775
|
+
supported: name === "gatewayRequestTools" ||
|
|
776
|
+
name === "modelDefaultsAndAliases" ||
|
|
777
|
+
name === "unsupportedOrDegradedInputs" ||
|
|
778
|
+
Boolean(overrides[name]),
|
|
779
|
+
},
|
|
780
|
+
]));
|
|
781
|
+
}
|
|
782
|
+
function cloneControls(controls) {
|
|
783
|
+
return Object.fromEntries(Object.entries(controls).map(([name, control]) => [name, { name, ...control }]));
|
|
784
|
+
}
|
|
785
|
+
function getModelInfo(cli, refresh) {
|
|
786
|
+
if (cli !== "grok_api") {
|
|
787
|
+
return getAvailableCliInfo(refresh)[cli];
|
|
788
|
+
}
|
|
789
|
+
const providers = loadProvidersConfig();
|
|
790
|
+
const enabled = isXaiProviderEnabled(providers);
|
|
791
|
+
const defaultModel = providers.xai?.defaultModel;
|
|
792
|
+
return {
|
|
793
|
+
description: "xAI Grok Responses API provider configured through [providers.xai]; distinct from Grok CLI/Build.",
|
|
794
|
+
models: defaultModel ? { [defaultModel]: "Configured xAI Responses API default model" } : {},
|
|
795
|
+
defaultModel,
|
|
796
|
+
defaultModelSource: defaultModel ? "[providers.xai].default_model" : undefined,
|
|
797
|
+
warnings: enabled
|
|
798
|
+
? []
|
|
799
|
+
: ["[providers.xai] is not enabled or the configured API-key environment variable is unset."],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function discoverConfigSurfaces(cli, query, discoveredSkills) {
|
|
803
|
+
if (cli === "grok_api") {
|
|
804
|
+
const providers = loadProvidersConfig();
|
|
805
|
+
return [
|
|
806
|
+
{
|
|
807
|
+
name: "providers.xai",
|
|
808
|
+
kind: "gateway",
|
|
809
|
+
present: providers.xai !== null,
|
|
810
|
+
details: providers.xai
|
|
811
|
+
? "xAI API provider is configured; secret key material is read only from the named environment variable at request time."
|
|
812
|
+
: "Add [providers.xai] to the gateway config and set the configured API-key environment variable to enable grok_api_request.",
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
name: "xai_api_key_env",
|
|
816
|
+
kind: "env",
|
|
817
|
+
present: isXaiProviderEnabled(providers),
|
|
818
|
+
entries: providers.xai ? [providers.xai.apiKeyEnv] : [],
|
|
819
|
+
details: "Reports only the environment variable name and whether it is set; never the value.",
|
|
820
|
+
},
|
|
821
|
+
];
|
|
822
|
+
}
|
|
823
|
+
const surfaces = [];
|
|
824
|
+
addSkillSurfaces(surfaces, cli, query, discoveredSkills);
|
|
825
|
+
switch (cli) {
|
|
826
|
+
case "claude":
|
|
827
|
+
addFileSurface(surfaces, "claude_settings", providerHomePath(".claude", "settings.json"), query);
|
|
828
|
+
addFileSurface(surfaces, "claude_local_settings", providerHomePath(".claude", "settings.local.json"), query);
|
|
829
|
+
surfaces.push({
|
|
830
|
+
name: "gateway_mcp_config_generation",
|
|
831
|
+
kind: "gateway",
|
|
832
|
+
present: true,
|
|
833
|
+
entries: [...CLAUDE_MCP_SERVER_NAMES],
|
|
834
|
+
details: "Gateway can generate a Claude MCP config for gateway-known MCP servers.",
|
|
835
|
+
});
|
|
836
|
+
break;
|
|
837
|
+
case "codex":
|
|
838
|
+
addCodexConfigSurfaces(surfaces, query);
|
|
839
|
+
break;
|
|
840
|
+
case "gemini":
|
|
841
|
+
addFileSurface(surfaces, "gemini_settings", providerHomePath(".gemini", "settings.json"), query);
|
|
842
|
+
addFileSurface(surfaces, "gemini_trusted_folders", providerHomePath(".gemini", "trusted_folders.json"), query);
|
|
843
|
+
break;
|
|
844
|
+
case "grok":
|
|
845
|
+
addGrokConfigSurfaces(surfaces, query);
|
|
846
|
+
break;
|
|
847
|
+
case "mistral":
|
|
848
|
+
addVibeConfigSurfaces(surfaces, query);
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
return surfaces;
|
|
852
|
+
}
|
|
853
|
+
function extractProviderTools(cli, skills) {
|
|
854
|
+
if (cli === "grok_api")
|
|
855
|
+
return [];
|
|
856
|
+
const tools = new Map();
|
|
857
|
+
for (const skill of skills) {
|
|
858
|
+
for (const toolName of skill.declaredTools) {
|
|
859
|
+
const existing = tools.get(toolName);
|
|
860
|
+
if (existing)
|
|
861
|
+
continue;
|
|
862
|
+
tools.set(toolName, {
|
|
863
|
+
name: toolName,
|
|
864
|
+
source: skill.name === "imagine" && isKnownProviderTool(cli, toolName) ? "imagine" : cli,
|
|
865
|
+
skillName: skill.name,
|
|
866
|
+
path: skill.path,
|
|
867
|
+
confidence: providerToolConfidence(cli, toolName, skill.declaredToolReasons?.[toolName]),
|
|
868
|
+
reason: skill.declaredToolReasons?.[toolName] ?? "backtick-heuristic",
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return [...tools.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
873
|
+
}
|
|
874
|
+
function providerToolConfidence(cli, toolName, reason) {
|
|
875
|
+
if (isKnownProviderTool(cli, toolName))
|
|
876
|
+
return "high";
|
|
877
|
+
if (reason === "exact-tool-section")
|
|
878
|
+
return "medium";
|
|
879
|
+
if (reason === "backtick-heuristic")
|
|
880
|
+
return "medium";
|
|
881
|
+
return "low";
|
|
882
|
+
}
|
|
883
|
+
function isKnownProviderTool(cli, toolName) {
|
|
884
|
+
return KNOWN_PROVIDER_TOOLS[cli]?.includes(toolName) ?? false;
|
|
885
|
+
}
|
|
886
|
+
function formatPathForOutput(outputPath, query) {
|
|
887
|
+
if (query.includePaths)
|
|
888
|
+
return outputPath;
|
|
889
|
+
return redactHomePath(outputPath);
|
|
890
|
+
}
|
|
891
|
+
function redactHomePath(outputPath) {
|
|
892
|
+
const home = process.env.LLM_GATEWAY_TOOL_DISCOVERY_HOME || homedir();
|
|
893
|
+
const relative = path.relative(home, outputPath);
|
|
894
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
895
|
+
return path.join("~", relative);
|
|
896
|
+
}
|
|
897
|
+
return "<redacted-path>";
|
|
898
|
+
}
|
|
899
|
+
function providerHomePath(...parts) {
|
|
900
|
+
return path.join(process.env.LLM_GATEWAY_TOOL_DISCOVERY_HOME || homedir(), ...parts);
|
|
901
|
+
}
|
|
902
|
+
function addSkillSurfaces(surfaces, cli, query, discoveredSkills) {
|
|
903
|
+
for (const root of skillRoots(cli)) {
|
|
904
|
+
surfaces.push({
|
|
905
|
+
name: `${cli}_${root.source}_skills`,
|
|
906
|
+
kind: "directory",
|
|
907
|
+
present: existsSync(root.path),
|
|
908
|
+
path: query.includePaths ? root.path : undefined,
|
|
909
|
+
details: `skills=${discoveredSkills.filter(skill => skill.source === root.source).length}`,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function addFileSurface(surfaces, name, filePath, query, details, entries) {
|
|
914
|
+
surfaces.push({
|
|
915
|
+
name,
|
|
916
|
+
kind: "file",
|
|
917
|
+
present: existsSync(filePath),
|
|
918
|
+
path: query.includePaths ? filePath : undefined,
|
|
919
|
+
entries,
|
|
920
|
+
details,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function addCodexConfigSurfaces(surfaces, query) {
|
|
924
|
+
const configPath = providerHomePath(".codex", "config.toml");
|
|
925
|
+
const parsed = parseConfigToml(configPath);
|
|
926
|
+
addFileSurface(surfaces, "codex_config", configPath, query);
|
|
927
|
+
surfaces.push({
|
|
928
|
+
name: "codex_profiles",
|
|
929
|
+
kind: "provider",
|
|
930
|
+
present: parsed !== null && hasObjectKey(parsed, "profiles"),
|
|
931
|
+
entries: objectKeysAt(parsed, "profiles"),
|
|
932
|
+
details: "Profile names only; model values are sourced from modelInfo.",
|
|
933
|
+
});
|
|
934
|
+
surfaces.push({
|
|
935
|
+
name: "codex_mcp_servers",
|
|
936
|
+
kind: "provider",
|
|
937
|
+
present: parsed !== null && hasObjectKey(parsed, "mcp_servers"),
|
|
938
|
+
entries: objectKeysAt(parsed, "mcp_servers"),
|
|
939
|
+
details: "MCP server names only; command/env values are redacted.",
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function addGrokConfigSurfaces(surfaces, query) {
|
|
943
|
+
const configPath = providerHomePath(".grok", "config.toml");
|
|
944
|
+
const parsed = parseConfigToml(configPath);
|
|
945
|
+
addFileSurface(surfaces, "grok_config", configPath, query);
|
|
946
|
+
surfaces.push({
|
|
947
|
+
name: "grok_mcp_servers",
|
|
948
|
+
kind: "provider",
|
|
949
|
+
present: parsed !== null && hasObjectKey(parsed, "mcp_servers"),
|
|
950
|
+
entries: objectKeysAt(parsed, "mcp_servers"),
|
|
951
|
+
details: "Grok-owned MCP server names only where safely discoverable.",
|
|
952
|
+
});
|
|
953
|
+
addDirectoryPresence(surfaces, "grok_docs", providerHomePath(".grok", "docs"), query);
|
|
954
|
+
addDirectoryPresence(surfaces, "grok_help", providerHomePath(".grok", "help"), query);
|
|
955
|
+
}
|
|
956
|
+
function addVibeConfigSurfaces(surfaces, query) {
|
|
957
|
+
const configPath = providerHomePath(".vibe", "config.toml");
|
|
958
|
+
const parsed = parseConfigToml(configPath);
|
|
959
|
+
addFileSurface(surfaces, "vibe_config", configPath, query);
|
|
960
|
+
surfaces.push({
|
|
961
|
+
name: "vibe_session_logging",
|
|
962
|
+
kind: "provider",
|
|
963
|
+
present: parsed !== null && hasObjectKey(parsed, "session_logging"),
|
|
964
|
+
details: booleanTomlValue(parsed, ["session_logging", "enabled"]),
|
|
965
|
+
});
|
|
966
|
+
surfaces.push({
|
|
967
|
+
name: "vibe_trusted_folders",
|
|
968
|
+
kind: "provider",
|
|
969
|
+
present: parsed !== null && hasObjectKey(parsed, "trusted_folders"),
|
|
970
|
+
details: parsed !== null && hasObjectKey(parsed, "trusted_folders") ? "present" : "missing",
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
function addDirectoryPresence(surfaces, name, dirPath, query) {
|
|
974
|
+
surfaces.push({
|
|
975
|
+
name,
|
|
976
|
+
kind: "directory",
|
|
977
|
+
present: existsSync(dirPath),
|
|
978
|
+
path: query.includePaths ? dirPath : undefined,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function parseConfigToml(configPath) {
|
|
982
|
+
if (!existsSync(configPath))
|
|
983
|
+
return null;
|
|
984
|
+
try {
|
|
985
|
+
const stat = statSync(configPath);
|
|
986
|
+
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES)
|
|
987
|
+
return null;
|
|
988
|
+
return parseToml(readFileSync(configPath, "utf8"));
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function hasObjectKey(source, key) {
|
|
995
|
+
return !!source && typeof source[key] === "object" && source[key] !== null;
|
|
996
|
+
}
|
|
997
|
+
function objectKeysAt(source, key) {
|
|
998
|
+
if (!source || typeof source[key] !== "object" || source[key] === null)
|
|
999
|
+
return [];
|
|
1000
|
+
return Object.keys(source[key]).sort();
|
|
1001
|
+
}
|
|
1002
|
+
function booleanTomlValue(source, pathParts) {
|
|
1003
|
+
let current = source;
|
|
1004
|
+
for (const part of pathParts) {
|
|
1005
|
+
if (!current || typeof current !== "object")
|
|
1006
|
+
return "missing";
|
|
1007
|
+
current = current[part];
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof current === "boolean")
|
|
1010
|
+
return current ? "enabled" : "disabled";
|
|
1011
|
+
return "present";
|
|
1012
|
+
}
|
|
1013
|
+
function extractFrontmatterValue(content, key) {
|
|
1014
|
+
if (!content.startsWith("---"))
|
|
1015
|
+
return undefined;
|
|
1016
|
+
const end = content.indexOf("\n---", 3);
|
|
1017
|
+
if (end === -1)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
const frontmatter = content.slice(3, end);
|
|
1020
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
1021
|
+
if (key.includes(".")) {
|
|
1022
|
+
return extractNestedFrontmatterValue(lines, key);
|
|
1023
|
+
}
|
|
1024
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1025
|
+
const line = lines[index];
|
|
1026
|
+
const match = new RegExp(`^${escapeRegExp(key)}:\\s*(.*)$`).exec(line);
|
|
1027
|
+
if (!match)
|
|
1028
|
+
continue;
|
|
1029
|
+
const rawValue = match[1].trim();
|
|
1030
|
+
if (/^[>|][+-]?$/.test(rawValue)) {
|
|
1031
|
+
return extractBlockScalar(lines, index + 1, rawValue.startsWith(">"), indentationOf(line));
|
|
1032
|
+
}
|
|
1033
|
+
return rawValue.replace(/^["']|["']$/g, "").trim() || undefined;
|
|
1034
|
+
}
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
function extractNestedFrontmatterValue(lines, key) {
|
|
1038
|
+
const [parent, child] = key.split(".", 2);
|
|
1039
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1040
|
+
if (!new RegExp(`^${escapeRegExp(parent)}:\\s*$`).test(lines[index]))
|
|
1041
|
+
continue;
|
|
1042
|
+
for (let childIndex = index + 1; childIndex < lines.length; childIndex += 1) {
|
|
1043
|
+
const line = lines[childIndex];
|
|
1044
|
+
if (line.length > 0 && !/^\s/.test(line))
|
|
1045
|
+
break;
|
|
1046
|
+
const match = new RegExp(`^\\s+${escapeRegExp(child)}:\\s*(.*)$`).exec(line);
|
|
1047
|
+
if (!match)
|
|
1048
|
+
continue;
|
|
1049
|
+
const rawValue = match[1].trim();
|
|
1050
|
+
if (/^[>|][+-]?$/.test(rawValue)) {
|
|
1051
|
+
return extractBlockScalar(lines, childIndex + 1, rawValue.startsWith(">"), indentationOf(line));
|
|
1052
|
+
}
|
|
1053
|
+
return rawValue.replace(/^["']|["']$/g, "").trim() || undefined;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return undefined;
|
|
1057
|
+
}
|
|
1058
|
+
function extractBlockScalar(lines, startIndex, folded, parentIndent) {
|
|
1059
|
+
const block = [];
|
|
1060
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
1061
|
+
const blockLine = lines[index];
|
|
1062
|
+
if (blockLine.trim().length > 0 && indentationOf(blockLine) <= parentIndent)
|
|
1063
|
+
break;
|
|
1064
|
+
block.push(blockLine.trim());
|
|
1065
|
+
}
|
|
1066
|
+
return (block
|
|
1067
|
+
.filter(Boolean)
|
|
1068
|
+
.join(folded ? " " : "\n")
|
|
1069
|
+
.trim() || undefined);
|
|
1070
|
+
}
|
|
1071
|
+
function indentationOf(line) {
|
|
1072
|
+
return line.match(/^\s*/)?.[0].length ?? 0;
|
|
1073
|
+
}
|
|
1074
|
+
function extractFirstHeading(content) {
|
|
1075
|
+
const match = /^#\s+(.+)$/m.exec(content);
|
|
1076
|
+
return match?.[1]?.trim();
|
|
1077
|
+
}
|
|
1078
|
+
function extractDeclaredTools(cli, content) {
|
|
1079
|
+
const tools = new Map();
|
|
1080
|
+
for (const identifier of extractToolSectionIdentifiers(content)) {
|
|
1081
|
+
addToolHint(tools, cli, identifier, "exact-tool-section");
|
|
1082
|
+
}
|
|
1083
|
+
for (const knownTool of KNOWN_PROVIDER_TOOLS[cli] ?? []) {
|
|
1084
|
+
if (new RegExp(`\\b${escapeRegExp(knownTool)}\\b`).test(content)) {
|
|
1085
|
+
addToolHint(tools, cli, knownTool, "known-tool-name");
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const pattern = /`([A-Za-z][A-Za-z0-9_:-]{2,64})`/g;
|
|
1089
|
+
for (const match of content.matchAll(pattern)) {
|
|
1090
|
+
addToolHint(tools, cli, match[1], "backtick-heuristic");
|
|
1091
|
+
}
|
|
1092
|
+
const entries = [...tools.entries()]
|
|
1093
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1094
|
+
.slice(0, MAX_PROVIDER_TOOLS_PER_SKILL);
|
|
1095
|
+
return {
|
|
1096
|
+
tools: entries.map(([tool]) => tool),
|
|
1097
|
+
reasons: Object.fromEntries(entries),
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function extractToolSectionIdentifiers(content) {
|
|
1101
|
+
const identifiers = new Set();
|
|
1102
|
+
const sectionMatch = /^#{1,3}\s+(?:provider\s+tools|native\s+tools|available\s+tools|tools)\s*$/im.exec(content);
|
|
1103
|
+
if (!sectionMatch)
|
|
1104
|
+
return [];
|
|
1105
|
+
const sectionStart = sectionMatch.index + sectionMatch[0].length;
|
|
1106
|
+
const nextHeading = /^#{1,3}\s+/m.exec(content.slice(sectionStart));
|
|
1107
|
+
const section = content.slice(sectionStart, nextHeading ? sectionStart + nextHeading.index : undefined);
|
|
1108
|
+
const identifierPattern = /(?:`|^|\s|[-*])([a-z][a-z0-9]*(?:_[a-z0-9]+)+)(?:`|$|\s|[:,.)])/gm;
|
|
1109
|
+
for (const match of section.matchAll(identifierPattern)) {
|
|
1110
|
+
identifiers.add(match[1]);
|
|
1111
|
+
}
|
|
1112
|
+
return [...identifiers];
|
|
1113
|
+
}
|
|
1114
|
+
function addToolHint(tools, cli, identifier, reason) {
|
|
1115
|
+
if (!isPlausibleToolIdentifier(cli, identifier))
|
|
1116
|
+
return;
|
|
1117
|
+
const existing = tools.get(identifier);
|
|
1118
|
+
if (existing === "known-tool-name" || existing === "exact-tool-section")
|
|
1119
|
+
return;
|
|
1120
|
+
if (existing === "backtick-heuristic" && reason === "low-confidence")
|
|
1121
|
+
return;
|
|
1122
|
+
tools.set(identifier, reason);
|
|
1123
|
+
}
|
|
1124
|
+
function isPlausibleToolIdentifier(cli, identifier) {
|
|
1125
|
+
if (isKnownProviderTool(cli, identifier))
|
|
1126
|
+
return true;
|
|
1127
|
+
if (identifier.includes(":"))
|
|
1128
|
+
return false;
|
|
1129
|
+
if (NOISE_TOOL_IDENTIFIERS.has(identifier))
|
|
1130
|
+
return false;
|
|
1131
|
+
return /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/.test(identifier);
|
|
1132
|
+
}
|
|
1133
|
+
function escapeRegExp(value) {
|
|
1134
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1135
|
+
}
|
|
1136
|
+
function formatError(error) {
|
|
1137
|
+
return error instanceof Error ? error.message : String(error);
|
|
1138
|
+
}
|