llm-cli-gateway 1.14.0 → 1.15.1
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 +249 -46
- package/README.md +139 -29
- package/dist/async-job-manager.js +20 -8
- package/dist/executor.js +65 -8
- package/dist/index.d.ts +101 -0
- package/dist/index.js +311 -26
- package/dist/request-helpers.js +12 -0
- package/dist/session-manager.d.ts +20 -2
- package/dist/session-manager.js +28 -3
- package/dist/worktree-manager.d.ts +41 -0
- package/dist/worktree-manager.js +214 -0
- package/package.json +1 -1
|
@@ -207,15 +207,21 @@ export class AsyncJobManager {
|
|
|
207
207
|
* (sorted keys → JSON-stringified). This prevents two Mistral requests with the
|
|
208
208
|
* same argv but different `VIBE_ACTIVE_MODEL` from deduping onto each other.
|
|
209
209
|
*/
|
|
210
|
-
buildRequestKey(cli, args, env, stdin) {
|
|
210
|
+
buildRequestKey(cli, args, env, stdin, cwd) {
|
|
211
211
|
// Slice κ: stdin participates in the dedup key. Two Claude requests
|
|
212
212
|
// with identical argv but different cache_control content blocks
|
|
213
213
|
// would otherwise collide on dedup and the second caller would get
|
|
214
214
|
// the wrong response. The legacy "no stdin" code path passes
|
|
215
215
|
// stdin=undefined, which serialises to the same empty marker the
|
|
216
216
|
// previous version emitted — non-κ dedup is unchanged.
|
|
217
|
+
// Slice λ: cwd participates similarly. Two requests with identical
|
|
218
|
+
// argv but different worktrees would otherwise collide on dedup and
|
|
219
|
+
// the second caller would receive a response executed in the wrong
|
|
220
|
+
// worktree. cwd=undefined preserves the pre-λ key shape — non-λ
|
|
221
|
+
// dedup is unchanged.
|
|
217
222
|
const extraEnv = canonicaliseEnvForKey(env);
|
|
218
|
-
const
|
|
223
|
+
const withStdin = stdin === undefined ? extraEnv : `${extraEnv}|stdin:${stdin}`;
|
|
224
|
+
const extra = cwd === undefined ? withStdin : `${withStdin}|cwd:${cwd}`;
|
|
219
225
|
return computeRequestKey(cli, args, extra);
|
|
220
226
|
}
|
|
221
227
|
fireOnComplete(job) {
|
|
@@ -449,7 +455,7 @@ export class AsyncJobManager {
|
|
|
449
455
|
*/
|
|
450
456
|
startJobWithDedup(cli, args, correlationId, opts = {}) {
|
|
451
457
|
const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, } = opts;
|
|
452
|
-
const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin);
|
|
458
|
+
const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd);
|
|
453
459
|
if (!forceRefresh && this.store) {
|
|
454
460
|
try {
|
|
455
461
|
const existing = this.store.findByRequestKey(requestKey);
|
|
@@ -810,8 +816,9 @@ export class AsyncJobManager {
|
|
|
810
816
|
job.error = "Output exceeded maximum size (50MB)";
|
|
811
817
|
job.finishedAt = new Date().toISOString();
|
|
812
818
|
job.clearIdleTimer?.();
|
|
813
|
-
if (job.process)
|
|
819
|
+
if (job.process) {
|
|
814
820
|
killProcessGroup(job.process, "SIGTERM");
|
|
821
|
+
}
|
|
815
822
|
this.logger.info(`Job ${job.id} killed due to output overflow`, {
|
|
816
823
|
correlationId: job.correlationId,
|
|
817
824
|
});
|
|
@@ -819,11 +826,16 @@ export class AsyncJobManager {
|
|
|
819
826
|
this.persistComplete(job);
|
|
820
827
|
this.writeFlightComplete(job, "failed", "Output exceeded maximum size (50MB)");
|
|
821
828
|
this.fireOnComplete(job);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
829
|
+
if (job.process) {
|
|
830
|
+
setTimeout(() => {
|
|
831
|
+
if (!job.exited && job.process)
|
|
832
|
+
killProcessGroup(job.process, "SIGKILL");
|
|
833
|
+
job.cleanupGroup?.();
|
|
834
|
+
}, 5000);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
825
837
|
job.cleanupGroup?.();
|
|
826
|
-
}
|
|
838
|
+
}
|
|
827
839
|
}
|
|
828
840
|
return;
|
|
829
841
|
}
|
package/dist/executor.js
CHANGED
|
@@ -139,18 +139,48 @@ export function resolveCommandForSpawn(command, args, options = {}) {
|
|
|
139
139
|
if ([".cmd", ".bat"].includes(extname(resolved).toLowerCase())) {
|
|
140
140
|
return {
|
|
141
141
|
command: "cmd.exe",
|
|
142
|
-
args: [
|
|
142
|
+
args: [
|
|
143
|
+
"/d",
|
|
144
|
+
"/s",
|
|
145
|
+
"/c",
|
|
146
|
+
// Windows .cmd/.bat shims require cmd.exe. `buildWindowsCmdCommand`
|
|
147
|
+
// applies CommandLineToArgvW quoting and cmd metacharacter escaping
|
|
148
|
+
// to every dynamic segment before it reaches this shell boundary.
|
|
149
|
+
//
|
|
150
|
+
// codeql[js/shell-command-constructed-from-input]
|
|
151
|
+
`"${buildWindowsCmdCommand(resolved, args)}"`,
|
|
152
|
+
],
|
|
143
153
|
windowsVerbatimArguments: true,
|
|
144
154
|
};
|
|
145
155
|
}
|
|
146
156
|
return { command: resolved, args };
|
|
147
157
|
}
|
|
148
158
|
function buildWindowsCmdCommand(command, args) {
|
|
159
|
+
// codeql[js/shell-command-constructed-from-input]
|
|
149
160
|
return [escapeWindowsCmdCommand(command), ...args.map(escapeWindowsCmdArgument)].join(" ");
|
|
150
161
|
}
|
|
151
|
-
const WINDOWS_CMD_META_CHARS =
|
|
162
|
+
const WINDOWS_CMD_META_CHARS = new Set([
|
|
163
|
+
"(",
|
|
164
|
+
")",
|
|
165
|
+
"]",
|
|
166
|
+
"[",
|
|
167
|
+
"%",
|
|
168
|
+
"!",
|
|
169
|
+
"^",
|
|
170
|
+
'"',
|
|
171
|
+
"`",
|
|
172
|
+
"<",
|
|
173
|
+
">",
|
|
174
|
+
"&",
|
|
175
|
+
"|",
|
|
176
|
+
";",
|
|
177
|
+
",",
|
|
178
|
+
" ",
|
|
179
|
+
"*",
|
|
180
|
+
"?",
|
|
181
|
+
]);
|
|
152
182
|
function escapeWindowsCmdCommand(value) {
|
|
153
|
-
return win32.normalize(value)
|
|
183
|
+
return escapeWindowsCmdMetaChars(win32.normalize(value));
|
|
154
184
|
}
|
|
155
185
|
// CommandLineToArgvW rules: a run of N backslashes before a literal " must be
|
|
156
186
|
// doubled and followed by \" (yielding 2N+1 backslashes total, so the parser
|
|
@@ -158,11 +188,38 @@ function escapeWindowsCmdCommand(value) {
|
|
|
158
188
|
// before the closing " must be doubled (2N) so the quote still terminates the
|
|
159
189
|
// arg. Then wrap in quotes and caret-escape cmd.exe metacharacters.
|
|
160
190
|
function escapeWindowsCmdArgument(value) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
return escapeWindowsCmdMetaChars(quoteWindowsArgForCommandLineToArgv(`${value}`));
|
|
192
|
+
}
|
|
193
|
+
function quoteWindowsArgForCommandLineToArgv(value) {
|
|
194
|
+
let encoded = "";
|
|
195
|
+
let backslashes = 0;
|
|
196
|
+
for (const ch of value) {
|
|
197
|
+
if (ch === "\\") {
|
|
198
|
+
backslashes += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (ch === '"') {
|
|
202
|
+
encoded += "\\".repeat(backslashes * 2 + 1);
|
|
203
|
+
encoded += '"';
|
|
204
|
+
backslashes = 0;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
encoded += "\\".repeat(backslashes);
|
|
208
|
+
backslashes = 0;
|
|
209
|
+
encoded += ch;
|
|
210
|
+
}
|
|
211
|
+
encoded += "\\".repeat(backslashes * 2);
|
|
212
|
+
return `"${encoded}"`;
|
|
213
|
+
}
|
|
214
|
+
function escapeWindowsCmdMetaChars(value) {
|
|
215
|
+
let escaped = "";
|
|
216
|
+
for (const ch of value) {
|
|
217
|
+
if (WINDOWS_CMD_META_CHARS.has(ch)) {
|
|
218
|
+
escaped += "^";
|
|
219
|
+
}
|
|
220
|
+
escaped += ch;
|
|
221
|
+
}
|
|
222
|
+
return escaped;
|
|
166
223
|
}
|
|
167
224
|
function resolveWindowsCommandPath(command, envPath) {
|
|
168
225
|
if (/[\\/]/.test(command)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,36 @@ type GatewayLogger = typeof logger;
|
|
|
67
67
|
*/
|
|
68
68
|
export declare const MAX_TURNS_SCHEMA: z.ZodNumber;
|
|
69
69
|
export declare const MAX_PRICE_SCHEMA: z.ZodNumber;
|
|
70
|
+
/**
|
|
71
|
+
* Slice λ: shared worktree directive for all 10 `*_request` / `*_request_async`
|
|
72
|
+
* tools. `true` creates a fresh worktree under `<repoRoot>/.worktrees/<uuid>`
|
|
73
|
+
* branched from HEAD. `{ name?, ref? }` lets the caller supply a sanitized
|
|
74
|
+
* name and/or git ref (default ref: HEAD).
|
|
75
|
+
*
|
|
76
|
+
* Lifecycle is gateway-owned: the gateway pre-creates the worktree via
|
|
77
|
+
* `git worktree add`, then spawns the child CLI with `cwd: <worktree-path>`.
|
|
78
|
+
* No `-w` / `--worktree` flag is ever emitted to the underlying CLI. When
|
|
79
|
+
* the request carries a sessionId and the session already has a worktree,
|
|
80
|
+
* that worktree is reused. On session_delete or TTL eviction the gateway
|
|
81
|
+
* runs `git worktree remove --force`.
|
|
82
|
+
*
|
|
83
|
+
* Tool response: when a worktree was used, the successful response stdout
|
|
84
|
+
* is prefixed with `[gateway] worktree=<absolute-path>\n` so callers can
|
|
85
|
+
* parse/use the path without a schema change (slice λ §1.d).
|
|
86
|
+
*
|
|
87
|
+
* NOTE: callers should `.gitignore` the `.worktrees/` directory in their
|
|
88
|
+
* repo (the gateway does NOT auto-gitignore — see slice λ spec Q4).
|
|
89
|
+
*/
|
|
90
|
+
export declare const WORKTREE_SCHEMA: z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
|
|
91
|
+
name: z.ZodOptional<z.ZodString>;
|
|
92
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
93
|
+
}, "strict", z.ZodTypeAny, {
|
|
94
|
+
name?: string | undefined;
|
|
95
|
+
ref?: string | undefined;
|
|
96
|
+
}, {
|
|
97
|
+
name?: string | undefined;
|
|
98
|
+
ref?: string | undefined;
|
|
99
|
+
}>]>;
|
|
70
100
|
export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral"];
|
|
71
101
|
export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral"]>;
|
|
72
102
|
export type SessionProvider = (typeof SESSION_PROVIDER_VALUES)[number];
|
|
@@ -97,6 +127,57 @@ export interface GatewayServerRuntime {
|
|
|
97
127
|
export declare function resolveGatewayServerRuntime(deps?: GatewayServerDeps, options?: {
|
|
98
128
|
isolateState?: boolean;
|
|
99
129
|
}): GatewayServerRuntime;
|
|
130
|
+
/**
|
|
131
|
+
* Slice λ: shape returned by `resolveWorktreeForRequest`. `cwd` is what
|
|
132
|
+
* the spawn helpers (`executeCli`, `startJobWithDedup`) consume;
|
|
133
|
+
* `worktreePath` is what the tool handler embeds in the response prefix
|
|
134
|
+
* so callers can discover the path.
|
|
135
|
+
*/
|
|
136
|
+
export interface ResolvedWorktree {
|
|
137
|
+
cwd?: string;
|
|
138
|
+
worktreePath?: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Slice λ: resolve a request's worktree directive into a spawn cwd.
|
|
142
|
+
*
|
|
143
|
+
* - `worktreeOpt` is the Zod-validated input value (boolean |
|
|
144
|
+
* `{ name?, ref? }` | undefined).
|
|
145
|
+
* - When the request has a session AND the session already has a
|
|
146
|
+
* `metadata.worktreePath`, that path is reused (resume semantics).
|
|
147
|
+
* The reused path is returned without touching git; if the directory
|
|
148
|
+
* was externally removed between requests, the next CLI invocation
|
|
149
|
+
* will surface the error naturally.
|
|
150
|
+
* - When no reusable worktree exists, `createWorktree` runs; on success
|
|
151
|
+
* the new path is written to `session.metadata` (only when a session
|
|
152
|
+
* exists — request-scoped worktrees do NOT persist).
|
|
153
|
+
* - Returns `{}` when `worktreeOpt` is undefined/false (preserves
|
|
154
|
+
* pre-λ behaviour at non-worktree call sites).
|
|
155
|
+
* - Errors propagate as `WorktreeError`/`Error`; the caller wraps them
|
|
156
|
+
* in a `createErrorResponse` envelope. Do NOT swallow.
|
|
157
|
+
*
|
|
158
|
+
* Spec: docs/plans/slice-lambda.spec.md §"Implementation surface to
|
|
159
|
+
* verify" §5.
|
|
160
|
+
*/
|
|
161
|
+
export declare function resolveWorktreeForRequest(worktreeOpt: boolean | {
|
|
162
|
+
name?: string;
|
|
163
|
+
ref?: string;
|
|
164
|
+
} | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime): Promise<ResolvedWorktree>;
|
|
165
|
+
/**
|
|
166
|
+
* Slice λ §1.d: response-envelope shape decision for `worktreePath`.
|
|
167
|
+
*
|
|
168
|
+
* We surface the worktree path inline as a stdout prefix
|
|
169
|
+
* (`[gateway] worktree=<absolute-path>\n`) rather than as a
|
|
170
|
+
* structuredContent field or JSON wrapper. Rationale:
|
|
171
|
+
* - zero schema change across all 10 tools and their downstream parsers
|
|
172
|
+
* - matches how other slice features (session warnings, cache_state
|
|
173
|
+
* aggregates) surface side-channel metadata today
|
|
174
|
+
* - callers that want the path can split on the first newline; callers
|
|
175
|
+
* that don't care see a single ignorable header line
|
|
176
|
+
*
|
|
177
|
+
* Use `formatWorktreePrefix(resolution.worktreePath)` once per tool, at
|
|
178
|
+
* the moment a successful response is constructed.
|
|
179
|
+
*/
|
|
180
|
+
export declare function formatWorktreePrefix(worktreePath?: string): string;
|
|
100
181
|
export declare function extractUsageAndCost(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", output: string, outputFormat?: string,
|
|
101
182
|
/**
|
|
102
183
|
* Optional context for off-stdout telemetry sources. Today only Mistral
|
|
@@ -384,6 +465,11 @@ export interface GeminiRequestParams {
|
|
|
384
465
|
attachments?: string[];
|
|
385
466
|
/** Phase 4 slice γ: emit `--skip-trust` for fresh-workspace headless runs. */
|
|
386
467
|
skipTrust?: boolean;
|
|
468
|
+
/** Slice λ: run this request inside a gateway-owned git worktree. */
|
|
469
|
+
worktree?: boolean | {
|
|
470
|
+
name?: string;
|
|
471
|
+
ref?: string;
|
|
472
|
+
};
|
|
387
473
|
}
|
|
388
474
|
export interface HandlerDeps {
|
|
389
475
|
sessionManager: ISessionManager;
|
|
@@ -436,6 +522,11 @@ export interface GrokRequestParams {
|
|
|
436
522
|
allow?: string[];
|
|
437
523
|
/** Phase 4 slice θ: Grok `--deny <RULE>` (repeatable; one entry per --deny instance). */
|
|
438
524
|
deny?: string[];
|
|
525
|
+
/** Slice λ: run this request inside a gateway-owned git worktree. */
|
|
526
|
+
worktree?: boolean | {
|
|
527
|
+
name?: string;
|
|
528
|
+
ref?: string;
|
|
529
|
+
};
|
|
439
530
|
}
|
|
440
531
|
export declare function handleGrokRequest(deps: HandlerDeps, params: GrokRequestParams): Promise<ExtendedToolResponse>;
|
|
441
532
|
export declare function handleGrokRequestAsync(deps: AsyncHandlerDeps, params: Omit<GrokRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
|
@@ -470,6 +561,11 @@ export interface MistralRequestParams {
|
|
|
470
561
|
workingDir?: string;
|
|
471
562
|
/** Phase 4 slice ζ: Vibe `--add-dir <DIR>` repeatable add-dir parity. */
|
|
472
563
|
addDir?: string[];
|
|
564
|
+
/** Slice λ: run this request inside a gateway-owned git worktree. */
|
|
565
|
+
worktree?: boolean | {
|
|
566
|
+
name?: string;
|
|
567
|
+
ref?: string;
|
|
568
|
+
};
|
|
473
569
|
}
|
|
474
570
|
export declare function handleMistralRequest(deps: HandlerDeps, params: MistralRequestParams): Promise<ExtendedToolResponse>;
|
|
475
571
|
export declare function handleMistralRequestAsync(deps: AsyncHandlerDeps, params: Omit<MistralRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
|
@@ -504,6 +600,11 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
|
|
|
504
600
|
ignoreRules?: boolean;
|
|
505
601
|
workingDir?: string;
|
|
506
602
|
addDir?: string[];
|
|
603
|
+
/** Slice λ: run this request inside a gateway-owned git worktree. */
|
|
604
|
+
worktree?: boolean | {
|
|
605
|
+
name?: string;
|
|
606
|
+
ref?: string;
|
|
607
|
+
};
|
|
507
608
|
}): Promise<ExtendedToolResponse>;
|
|
508
609
|
export declare function createGatewayServer(deps?: GatewayServerDeps): McpServer;
|
|
509
610
|
export {};
|