llm-cli-gateway 2.3.0 → 2.5.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/dist/executor.js CHANGED
@@ -269,30 +269,26 @@ export function killAllProcessGroups() {
269
269
  });
270
270
  }
271
271
  export function killProcessGroup(proc, signal) {
272
- if (proc.pid) {
273
- if (process.platform === "win32") {
274
- return killWindowsProcessTree(proc.pid);
275
- }
276
- try {
277
- process.kill(-proc.pid, signal);
278
- return true;
279
- }
280
- catch (err) {
281
- if (err.code !== "ESRCH") {
282
- try {
283
- return proc.kill(signal);
284
- }
285
- catch {
286
- return false;
287
- }
288
- }
289
- return false;
290
- }
272
+ const pid = proc.pid;
273
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
274
+ return false;
275
+ }
276
+ if (process.platform === "win32") {
277
+ return killWindowsProcessTree(pid);
291
278
  }
292
279
  try {
293
- return proc.kill(signal);
280
+ process.kill(-pid, signal);
281
+ return true;
294
282
  }
295
- catch {
283
+ catch (err) {
284
+ if (err.code !== "ESRCH") {
285
+ try {
286
+ return proc.kill(signal);
287
+ }
288
+ catch {
289
+ return false;
290
+ }
291
+ }
296
292
  return false;
297
293
  }
298
294
  }
@@ -1,6 +1,7 @@
1
+ import type { ProviderType } from "./session-manager.js";
1
2
  export interface FlightLogStart {
2
3
  correlationId: string;
3
- cli: "claude" | "codex" | "gemini" | "grok" | "mistral";
4
+ cli: ProviderType;
4
5
  model: string;
5
6
  prompt: string;
6
7
  system?: string;
@@ -3,31 +3,42 @@ import { randomUUID } from "node:crypto";
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { authorizeBearerRequest, getRequiredBearerToken, writeAuthFailure } from "./auth.js";
6
+ import { loadRemoteOAuthConfig } from "./config.js";
7
+ import { OAuthServer, oauthBaseUrlFromRequest } from "./oauth.js";
8
+ import { runWithRequestContext } from "./request-context.js";
6
9
  const noopLogger = {
7
10
  info: (..._args) => { },
8
11
  error: (..._args) => { },
9
12
  debug: (..._args) => { },
10
13
  };
11
- function readBody(req) {
14
+ function firstHeader(value) {
15
+ return Array.isArray(value) ? value[0] : value;
16
+ }
17
+ function readRawBody(req) {
12
18
  return new Promise((resolve, reject) => {
13
19
  const chunks = [];
14
20
  req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
15
21
  req.on("error", reject);
16
22
  req.on("end", () => {
17
23
  if (chunks.length === 0) {
18
- resolve(undefined);
24
+ resolve("");
19
25
  return;
20
26
  }
21
- const raw = Buffer.concat(chunks).toString("utf8");
22
- try {
23
- resolve(JSON.parse(raw));
24
- }
25
- catch (error) {
26
- reject(error);
27
- }
27
+ resolve(Buffer.concat(chunks).toString("utf8"));
28
28
  });
29
29
  });
30
30
  }
31
+ async function readBody(req) {
32
+ const raw = await readRawBody(req);
33
+ if (!raw)
34
+ return undefined;
35
+ try {
36
+ return JSON.parse(raw);
37
+ }
38
+ catch (error) {
39
+ throw error;
40
+ }
41
+ }
31
42
  function methodNotAllowed(res) {
32
43
  res.writeHead(405, { allow: "GET, POST, DELETE", "content-type": "application/json" });
33
44
  res.end(JSON.stringify({ error: "Method not allowed" }));
@@ -36,6 +47,10 @@ function jsonError(res, status, message) {
36
47
  res.writeHead(status, { "content-type": "application/json" });
37
48
  res.end(JSON.stringify({ error: message }));
38
49
  }
50
+ function jsonResponse(res, status, body) {
51
+ res.writeHead(status, { "content-type": "application/json" });
52
+ res.end(JSON.stringify(body));
53
+ }
39
54
  function parseNoAuthPaths(raw, protectedPath) {
40
55
  const paths = new Set();
41
56
  for (const value of (raw ?? "").split(/[,;\s]+/)) {
@@ -51,6 +66,25 @@ function parseNoAuthPaths(raw, protectedPath) {
51
66
  }
52
67
  return paths;
53
68
  }
69
+ function isLocalHost(host) {
70
+ const hostname = host.split(":")[0]?.toLowerCase() ?? "";
71
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
72
+ }
73
+ function requestBaseUrl(req) {
74
+ const configured = process.env.LLM_GATEWAY_PUBLIC_URL;
75
+ if (configured) {
76
+ try {
77
+ return new URL(configured).origin;
78
+ }
79
+ catch {
80
+ }
81
+ }
82
+ const host = firstHeader(req.headers.host) ?? "127.0.0.1:3333";
83
+ const forwardedProto = firstHeader(req.headers["x-forwarded-proto"]);
84
+ const proto = forwardedProto ??
85
+ (host.startsWith("127.0.0.1") || host.startsWith("localhost") ? "http" : "https");
86
+ return `${proto}://${host}`;
87
+ }
54
88
  export async function startHttpGateway(options) {
55
89
  const host = options.host ?? process.env.LLM_GATEWAY_HTTP_HOST ?? "127.0.0.1";
56
90
  const port = options.port ?? Number(process.env.LLM_GATEWAY_HTTP_PORT ?? 3333);
@@ -59,6 +93,10 @@ export async function startHttpGateway(options) {
59
93
  const logger = options.logger ?? noopLogger;
60
94
  const sessions = new Map();
61
95
  const token = getRequiredBearerToken();
96
+ const oauthConfig = loadRemoteOAuthConfig(logger);
97
+ const oauthServer = oauthConfig.enabled
98
+ ? new OAuthServer({ protectedPath: path, config: oauthConfig, logger })
99
+ : null;
62
100
  async function closeSession(sessionId) {
63
101
  const entry = sessions.get(sessionId);
64
102
  if (!entry)
@@ -89,22 +127,46 @@ export async function startHttpGateway(options) {
89
127
  const httpServer = createServer(async (req, res) => {
90
128
  try {
91
129
  const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
130
+ const baseUrl = requestBaseUrl(req);
131
+ const oauthOrigin = oauthServer ? oauthBaseUrlFromRequest(req, oauthConfig) : null;
132
+ const effectiveOAuthBaseUrl = oauthOrigin ?? baseUrl;
133
+ const resourceMetadataUrl = oauthServer && oauthOrigin ? oauthServer.resourceMetadataUrl(oauthOrigin) : undefined;
92
134
  if (url.pathname === "/healthz") {
93
135
  res.writeHead(200, { "content-type": "application/json" });
94
136
  res.end(JSON.stringify({ ok: true, sessions: sessions.size }));
95
137
  return;
96
138
  }
139
+ if (oauthServer) {
140
+ if (oauthServer.isOAuthPath(url.pathname) && !oauthOrigin) {
141
+ jsonError(res, 503, "LLM_GATEWAY_PUBLIC_URL is required for public OAuth issuer metadata");
142
+ return;
143
+ }
144
+ if (await oauthServer.handle({
145
+ req,
146
+ res,
147
+ url,
148
+ baseUrl: effectiveOAuthBaseUrl,
149
+ })) {
150
+ return;
151
+ }
152
+ }
97
153
  const noAuthPath = noAuthPaths.has(url.pathname);
98
154
  if (url.pathname !== path && !noAuthPath) {
99
155
  jsonError(res, 404, "Not found");
100
156
  return;
101
157
  }
158
+ let requestContext = { authScopes: [] };
102
159
  if (!noAuthPath) {
103
160
  const auth = authorizeBearerRequest(req, token);
104
161
  if (!auth.ok) {
105
- writeAuthFailure(res, auth);
162
+ writeAuthFailure(res, auth, resourceMetadataUrl ? { resourceMetadataUrl } : {});
106
163
  return;
107
164
  }
165
+ requestContext = {
166
+ authKind: auth.kind,
167
+ authScopes: auth.scopes ?? [],
168
+ authClientId: auth.clientId,
169
+ };
108
170
  }
109
171
  if (req.method !== "GET" && req.method !== "POST" && req.method !== "DELETE") {
110
172
  methodNotAllowed(res);
@@ -129,7 +191,7 @@ export async function startHttpGateway(options) {
129
191
  return;
130
192
  }
131
193
  const body = req.method === "POST" ? await readBody(req) : undefined;
132
- await entry.transport.handleRequest(req, res, body);
194
+ await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
133
195
  return;
134
196
  }
135
197
  if (req.method !== "POST") {
@@ -146,7 +208,7 @@ export async function startHttpGateway(options) {
146
208
  return;
147
209
  }
148
210
  const entry = await createSession();
149
- await entry.transport.handleRequest(req, res, body);
211
+ await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
150
212
  }
151
213
  catch (error) {
152
214
  logger.error("HTTP transport request failed", error);
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { z } from "zod/v3";
4
- import { ISessionManager } from "./session-manager.js";
4
+ import { ISessionManager, type ProviderType } from "./session-manager.js";
5
5
  import { ResourceProvider } from "./resources.js";
6
6
  import { PerformanceMetrics } from "./metrics.js";
7
- import { type PersistenceConfig, type CacheAwarenessConfig } from "./config.js";
7
+ import { type PersistenceConfig, type CacheAwarenessConfig, type ProvidersConfig } from "./config.js";
8
+ import { type XaiReasoningEffort } from "./xai-api-provider.js";
8
9
  import { DatabaseConnection } from "./db.js";
9
10
  import { AsyncJobManager } from "./async-job-manager.js";
10
11
  import { ApprovalManager, ApprovalRecord } from "./approval-manager.js";
@@ -13,6 +14,7 @@ import { ClaudeMcpConfigResult, ClaudeMcpServerName } from "./claude-mcp-config.
13
14
  import { type MistralAgentMode, type ClaudePermissionMode, type CodexSandboxMode, type CodexAskForApproval, type ClaudeEffortLevel } from "./request-helpers.js";
14
15
  import { FlightRecorderLike } from "./flight-recorder.js";
15
16
  import { type PromptParts } from "./prompt-parts.js";
17
+ import { type WorkspaceRegistry } from "./workspace-registry.js";
16
18
  export interface WarningEntry {
17
19
  code: string;
18
20
  message?: string;
@@ -44,7 +46,7 @@ declare const logger: {
44
46
  debug: (message: string, ...args: any[]) => void;
45
47
  };
46
48
  type GatewayLogger = typeof logger;
47
- export declare function buildServerInstructions(asyncJobsEnabled: boolean): string;
49
+ export declare function buildServerInstructions(asyncJobsEnabled: boolean, grokApiToolsEnabled?: boolean): string;
48
50
  export declare const MAX_TURNS_SCHEMA: z.ZodNumber;
49
51
  export declare const MAX_TOKENS_SCHEMA: z.ZodNumber;
50
52
  export declare const MAX_PRICE_SCHEMA: z.ZodNumber;
@@ -58,9 +60,10 @@ export declare const WORKTREE_SCHEMA: z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
58
60
  name?: string | undefined;
59
61
  ref?: string | undefined;
60
62
  }>]>;
61
- export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral"];
62
- export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral"]>;
63
- export type SessionProvider = (typeof SESSION_PROVIDER_VALUES)[number];
63
+ export declare const WORKSPACE_ALIAS_SCHEMA: z.ZodString;
64
+ export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral", "grok-api"];
65
+ export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral", "grok-api"]>;
66
+ export type SessionProvider = ProviderType;
64
67
  export interface GatewayServerDeps {
65
68
  sessionManager?: ISessionManager;
66
69
  resourceProvider?: ResourceProvider;
@@ -72,6 +75,8 @@ export interface GatewayServerDeps {
72
75
  logger?: GatewayLogger;
73
76
  persistence?: PersistenceConfig;
74
77
  cacheAwareness?: CacheAwarenessConfig;
78
+ providers?: ProvidersConfig;
79
+ workspaces?: WorkspaceRegistry;
75
80
  }
76
81
  export interface GatewayServerRuntime {
77
82
  sessionManager: ISessionManager;
@@ -84,18 +89,27 @@ export interface GatewayServerRuntime {
84
89
  logger: GatewayLogger;
85
90
  persistence: PersistenceConfig;
86
91
  cacheAwareness: CacheAwarenessConfig;
92
+ providers: ProvidersConfig;
93
+ workspaces: WorkspaceRegistry;
87
94
  }
88
95
  export declare function resolveGatewayServerRuntime(deps?: GatewayServerDeps, options?: {
89
96
  isolateState?: boolean;
90
97
  }): GatewayServerRuntime;
98
+ export declare function shouldRegisterGrokApiTools(providers: ProvidersConfig): boolean;
91
99
  export interface ResolvedWorktree {
92
100
  cwd?: string;
93
101
  worktreePath?: string;
102
+ workspaceAlias?: string;
103
+ workspaceRoot?: string;
94
104
  }
95
105
  export declare function resolveWorktreeForRequest(worktreeOpt: boolean | {
96
106
  name?: string;
97
107
  ref?: string;
98
- } | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime): Promise<ResolvedWorktree>;
108
+ } | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime, options?: {
109
+ repoRoot?: string;
110
+ workspaceAlias?: string;
111
+ workspaceRoot?: string;
112
+ }): Promise<ResolvedWorktree>;
99
113
  export declare function formatWorktreePrefix(worktreePath?: string): string;
100
114
  export declare function extractUsageAndCost(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", output: string, outputFormat?: string, ctx?: {
101
115
  sessionId?: string;
@@ -285,6 +299,22 @@ export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams,
285
299
  env: Record<string, string>;
286
300
  ignoredDisallowedTools: boolean;
287
301
  };
302
+ export interface GrokApiRequestParams {
303
+ prompt?: string;
304
+ promptParts?: PromptParts;
305
+ model?: string;
306
+ sessionId?: string;
307
+ createNewSession?: boolean;
308
+ correlationId?: string;
309
+ optimizePrompt: boolean;
310
+ optimizeResponse?: boolean;
311
+ maxOutputTokens?: number;
312
+ temperature?: number;
313
+ topP?: number;
314
+ reasoningEffort?: XaiReasoningEffort;
315
+ timeoutMs?: number;
316
+ }
317
+ export declare function handleGrokApiRequest(deps: HandlerDeps, params: GrokApiRequestParams): Promise<ExtendedToolResponse>;
288
318
  export interface GeminiRequestParams {
289
319
  prompt?: string;
290
320
  promptParts?: PromptParts;
@@ -310,6 +340,7 @@ export interface GeminiRequestParams {
310
340
  attachments?: string[];
311
341
  skipTrust?: boolean;
312
342
  yolo?: boolean;
343
+ workspace?: string;
313
344
  worktree?: boolean | {
314
345
  name?: string;
315
346
  ref?: string;
@@ -323,6 +354,7 @@ export interface HandlerDeps {
323
354
  error: (...args: any[]) => void;
324
355
  debug: (...args: any[]) => void;
325
356
  };
357
+ workspaces?: WorkspaceRegistry;
326
358
  runtime?: GatewayServerRuntime;
327
359
  }
328
360
  export interface AsyncHandlerDeps extends HandlerDeps {
@@ -380,6 +412,7 @@ export interface GrokRequestParams {
380
412
  restoreCode?: boolean;
381
413
  leaderSocket?: string;
382
414
  nativeWorktree?: boolean | string;
415
+ workspace?: string;
383
416
  worktree?: boolean | {
384
417
  name?: string;
385
418
  ref?: string;
@@ -412,6 +445,7 @@ export interface MistralRequestParams {
412
445
  maxTokens?: number;
413
446
  workingDir?: string;
414
447
  addDir?: string[];
448
+ workspace?: string;
415
449
  worktree?: boolean | {
416
450
  name?: string;
417
451
  ref?: string;
@@ -449,6 +483,7 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
449
483
  ignoreRules?: boolean;
450
484
  workingDir?: string;
451
485
  addDir?: string[];
486
+ workspace?: string;
452
487
  worktree?: boolean | {
453
488
  name?: string;
454
489
  ref?: string;