llm-cli-gateway 1.4.0 → 1.5.4
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 +67 -1
- package/README.md +111 -8
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +24 -2
- package/dist/async-job-manager.js +71 -7
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +46 -0
- package/dist/cli-updater.d.ts +19 -2
- package/dist/cli-updater.js +110 -7
- package/dist/codex-json-parser.d.ts +34 -0
- package/dist/codex-json-parser.js +105 -0
- package/dist/doctor.d.ts +110 -0
- package/dist/doctor.js +280 -0
- package/dist/endpoint-exposure.d.ts +22 -0
- package/dist/endpoint-exposure.js +231 -0
- package/dist/executor.d.ts +2 -0
- package/dist/executor.js +2 -2
- package/dist/flight-recorder.d.ts +3 -1
- package/dist/flight-recorder.js +31 -2
- package/dist/gateway-server.d.ts +2 -0
- package/dist/gateway-server.js +1 -0
- package/dist/gemini-json-parser.d.ts +21 -0
- package/dist/gemini-json-parser.js +47 -0
- package/dist/health.d.ts +7 -0
- package/dist/health.js +22 -0
- package/dist/http-transport.d.ts +22 -0
- package/dist/http-transport.js +164 -0
- package/dist/index.d.ts +183 -2
- package/dist/index.js +2629 -1411
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +14 -0
- package/dist/model-registry.js +40 -6
- package/dist/provider-login-guidance.d.ts +21 -0
- package/dist/provider-login-guidance.js +98 -0
- package/dist/provider-status.d.ts +41 -0
- package/dist/provider-status.js +203 -0
- package/dist/request-helpers.d.ts +484 -4
- package/dist/request-helpers.js +613 -0
- package/dist/resources.js +44 -0
- package/dist/session-manager-pg.js +1 -0
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.js +2 -1
- package/dist/validation-normalizer.d.ts +23 -0
- package/dist/validation-normalizer.js +79 -0
- package/dist/validation-orchestrator.d.ts +47 -0
- package/dist/validation-orchestrator.js +145 -0
- package/dist/validation-prompts.d.ts +15 -0
- package/dist/validation-prompts.js +52 -0
- package/dist/validation-report.d.ts +57 -0
- package/dist/validation-report.js +129 -0
- package/dist/validation-tools.d.ts +7 -0
- package/dist/validation-tools.js +198 -0
- package/package.json +15 -5
- package/setup/status.schema.json +271 -0
package/dist/request-helpers.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Pure, side-effect-free helpers for request argument planning.
|
|
3
3
|
* Zero I/O, zero dependencies on index-scoped collaborators.
|
|
4
4
|
*/
|
|
5
|
+
import { existsSync, unlinkSync, writeFileSync } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { join, isAbsolute } from "path";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { z } from "zod";
|
|
5
10
|
/** Prefix for gateway-generated session IDs. Enforces provenance structurally. */
|
|
6
11
|
export const GATEWAY_SESSION_PREFIX = "gw-";
|
|
7
12
|
/**
|
|
@@ -92,3 +97,611 @@ export function resolveGrokSessionArgs(opts) {
|
|
|
92
97
|
}
|
|
93
98
|
return { resumeArgs: [], effectiveSessionId: undefined, userProvidedSession: false };
|
|
94
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Mistral Vibe-specific resume args.
|
|
102
|
+
*
|
|
103
|
+
* Vibe persists sessions only when `[session_logging] enabled = true` is set in
|
|
104
|
+
* `~/.vibe/config.toml`. The doctor checks for that toggle and surfaces an
|
|
105
|
+
* actionable error when it is missing; this pure helper just emits the args.
|
|
106
|
+
*
|
|
107
|
+
* The args shape mirrors Grok (`--continue` for latest, `--resume <id>` for a
|
|
108
|
+
* specific session) because Vibe exposes the same surface for its session log.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveMistralSessionArgs(opts) {
|
|
111
|
+
if (opts.createNewSession) {
|
|
112
|
+
return { resumeArgs: [], effectiveSessionId: undefined, userProvidedSession: false };
|
|
113
|
+
}
|
|
114
|
+
if (opts.resumeLatest && !opts.sessionId) {
|
|
115
|
+
return {
|
|
116
|
+
resumeArgs: ["--continue"],
|
|
117
|
+
effectiveSessionId: undefined,
|
|
118
|
+
userProvidedSession: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (opts.sessionId) {
|
|
122
|
+
validateSessionId(opts.sessionId);
|
|
123
|
+
return {
|
|
124
|
+
resumeArgs: ["--resume", opts.sessionId],
|
|
125
|
+
effectiveSessionId: opts.sessionId,
|
|
126
|
+
userProvidedSession: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { resumeArgs: [], effectiveSessionId: undefined, userProvidedSession: false };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Vibe-specific permission mode mapping. Vibe replaces Grok's `--always-approve`
|
|
133
|
+
* with an `--agent <mode>` enum. When the caller does not set a permissionMode,
|
|
134
|
+
* the gateway emits `--agent auto-approve` explicitly: omitting the flag would
|
|
135
|
+
* let Vibe pick its own default which may not be auto-approve, surprising
|
|
136
|
+
* programmatic callers.
|
|
137
|
+
*/
|
|
138
|
+
export const MISTRAL_AGENT_MODES = [
|
|
139
|
+
"default",
|
|
140
|
+
"plan",
|
|
141
|
+
"accept-edits",
|
|
142
|
+
"auto-approve",
|
|
143
|
+
"chat",
|
|
144
|
+
"explore",
|
|
145
|
+
"lean",
|
|
146
|
+
];
|
|
147
|
+
export const MISTRAL_DEFAULT_AGENT_MODE = "auto-approve";
|
|
148
|
+
/**
|
|
149
|
+
* Pure helper that builds Vibe's argv and env.
|
|
150
|
+
*
|
|
151
|
+
* - Model is selected via `VIBE_ACTIVE_MODEL` env var (NOT a `--model` flag).
|
|
152
|
+
* - Permission mode emits `--agent <mode>` (defaults to `auto-approve` when unset).
|
|
153
|
+
* - Allowed tools emit `--enabled-tools <tool>` once per tool (allowlist only).
|
|
154
|
+
* - Disallowed tools are accepted but ignored at the CLI boundary.
|
|
155
|
+
*/
|
|
156
|
+
export function prepareMistralRequest(input) {
|
|
157
|
+
const args = ["-p", input.prompt];
|
|
158
|
+
const env = {};
|
|
159
|
+
if (input.resolvedModel) {
|
|
160
|
+
env.VIBE_ACTIVE_MODEL = input.resolvedModel;
|
|
161
|
+
}
|
|
162
|
+
if (input.outputFormat) {
|
|
163
|
+
args.push("--output-format", input.outputFormat);
|
|
164
|
+
}
|
|
165
|
+
const mode = input.permissionMode ?? MISTRAL_DEFAULT_AGENT_MODE;
|
|
166
|
+
args.push("--agent", mode);
|
|
167
|
+
if (input.effort) {
|
|
168
|
+
args.push("--effort", input.effort);
|
|
169
|
+
}
|
|
170
|
+
if (input.reasoningEffort) {
|
|
171
|
+
args.push("--reasoning-effort", input.reasoningEffort);
|
|
172
|
+
}
|
|
173
|
+
if (input.allowedTools && input.allowedTools.length > 0) {
|
|
174
|
+
sanitizeCliArgValues(input.allowedTools, "allowedTools");
|
|
175
|
+
for (const tool of input.allowedTools) {
|
|
176
|
+
args.push("--enabled-tools", tool);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const ignoredDisallowedTools = Boolean(input.disallowedTools && input.disallowedTools.length > 0);
|
|
180
|
+
return { args, env, ignoredDisallowedTools };
|
|
181
|
+
}
|
|
182
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
// U24: Permission / approval mode parity helpers
|
|
184
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Claude `--permission-mode` values. `default` is a no-op (no flag emitted) —
|
|
187
|
+
* matches the CLI's behavior when the flag is absent, and avoids hard-coding an
|
|
188
|
+
* undocumented literal.
|
|
189
|
+
*/
|
|
190
|
+
export const CLAUDE_PERMISSION_MODES = [
|
|
191
|
+
"default",
|
|
192
|
+
"acceptEdits",
|
|
193
|
+
"plan",
|
|
194
|
+
"auto",
|
|
195
|
+
"dontAsk",
|
|
196
|
+
"bypassPermissions",
|
|
197
|
+
];
|
|
198
|
+
/**
|
|
199
|
+
* Resolve Claude's `--permission-mode` args.
|
|
200
|
+
*
|
|
201
|
+
* Precedence:
|
|
202
|
+
* 1. If `permissionMode` is set, it wins. A warning is returned when
|
|
203
|
+
* `dangerouslySkipPermissions: true` is also set (legacy + new conflict).
|
|
204
|
+
* 2. Else if `dangerouslySkipPermissions: true`, emit `--permission-mode
|
|
205
|
+
* bypassPermissions`.
|
|
206
|
+
* 3. Else (or `permissionMode === "default"`) emit nothing.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveClaudePermissionFlags(input) {
|
|
209
|
+
const { permissionMode, dangerouslySkipPermissions } = input;
|
|
210
|
+
let warning;
|
|
211
|
+
if (permissionMode) {
|
|
212
|
+
if (dangerouslySkipPermissions) {
|
|
213
|
+
warning =
|
|
214
|
+
"Both permissionMode and dangerouslySkipPermissions were provided; permissionMode wins. dangerouslySkipPermissions is deprecated.";
|
|
215
|
+
}
|
|
216
|
+
if (permissionMode === "default") {
|
|
217
|
+
return { args: [], warning };
|
|
218
|
+
}
|
|
219
|
+
return { args: ["--permission-mode", permissionMode], warning };
|
|
220
|
+
}
|
|
221
|
+
if (dangerouslySkipPermissions) {
|
|
222
|
+
return { args: ["--permission-mode", "bypassPermissions"] };
|
|
223
|
+
}
|
|
224
|
+
return { args: [] };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Gemini `--approval-mode` values. Preserves existing values (`default`,
|
|
228
|
+
* `auto_edit`, `yolo`) and adds `plan` for parity with Claude's plan mode.
|
|
229
|
+
*/
|
|
230
|
+
export const GEMINI_APPROVAL_MODES = ["default", "auto_edit", "yolo", "plan"];
|
|
231
|
+
/**
|
|
232
|
+
* Codex sandbox modes (for `--sandbox <mode>`).
|
|
233
|
+
*/
|
|
234
|
+
export const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
235
|
+
/**
|
|
236
|
+
* Codex approval modes (for `--ask-for-approval <mode>`).
|
|
237
|
+
*/
|
|
238
|
+
export const CODEX_ASK_FOR_APPROVAL_MODES = ["untrusted", "on-request", "never"];
|
|
239
|
+
/**
|
|
240
|
+
* Resolve Codex `--sandbox` / `--ask-for-approval` args from the modern
|
|
241
|
+
* params + legacy `fullAuto` shorthand.
|
|
242
|
+
*
|
|
243
|
+
* Precedence:
|
|
244
|
+
* 1. If `useLegacyFullAutoFlag && fullAuto`, emit `--full-auto` directly
|
|
245
|
+
* (escape hatch; deprecated).
|
|
246
|
+
* 2. Else explicit `sandboxMode` / `askForApproval` always emit their
|
|
247
|
+
* flags. If `fullAuto: true` is set alongside, a warning is attached
|
|
248
|
+
* and the explicit values win.
|
|
249
|
+
* 3. Else if `fullAuto: true`, expand to
|
|
250
|
+
* `--sandbox workspace-write --ask-for-approval never`.
|
|
251
|
+
* 4. Else emit nothing.
|
|
252
|
+
*/
|
|
253
|
+
export function resolveCodexSandboxFlags(input) {
|
|
254
|
+
const { sandboxMode, askForApproval, fullAuto, useLegacyFullAutoFlag } = input;
|
|
255
|
+
// deprecated: prefer sandboxMode + askForApproval; will be removed after Mistral GA.
|
|
256
|
+
if (useLegacyFullAutoFlag && fullAuto) {
|
|
257
|
+
return { args: ["--full-auto"] };
|
|
258
|
+
}
|
|
259
|
+
const explicit = Boolean(sandboxMode || askForApproval);
|
|
260
|
+
if (explicit) {
|
|
261
|
+
const args = [];
|
|
262
|
+
if (sandboxMode)
|
|
263
|
+
args.push("--sandbox", sandboxMode);
|
|
264
|
+
if (askForApproval)
|
|
265
|
+
args.push("--ask-for-approval", askForApproval);
|
|
266
|
+
const warning = fullAuto
|
|
267
|
+
? "fullAuto was set alongside explicit sandboxMode/askForApproval; explicit values win. fullAuto is deprecated."
|
|
268
|
+
: undefined;
|
|
269
|
+
return { args, warning };
|
|
270
|
+
}
|
|
271
|
+
if (fullAuto) {
|
|
272
|
+
return {
|
|
273
|
+
args: ["--sandbox", "workspace-write", "--ask-for-approval", "never"],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return { args: [] };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Flags that `codex exec resume` rejects (the original session's policy is
|
|
280
|
+
* inherited). Callers must drop these when building resume argv.
|
|
281
|
+
*
|
|
282
|
+
* U26 expands this list with `--add-dir`, `-C`, `--output-schema`, and
|
|
283
|
+
* `--search`, all of which `codex exec resume --help` rejects at the audit
|
|
284
|
+
* date.
|
|
285
|
+
*/
|
|
286
|
+
export const CODEX_RESUME_FILTERED_FLAGS = new Set([
|
|
287
|
+
"--full-auto",
|
|
288
|
+
"--sandbox",
|
|
289
|
+
"--ask-for-approval",
|
|
290
|
+
"--add-dir",
|
|
291
|
+
"-C",
|
|
292
|
+
"--output-schema",
|
|
293
|
+
"--search",
|
|
294
|
+
]);
|
|
295
|
+
/**
|
|
296
|
+
* Codex flags that take exactly one value (consumed together with the flag).
|
|
297
|
+
* `--full-auto` and `--search` are bare booleans and intentionally absent.
|
|
298
|
+
*/
|
|
299
|
+
const CODEX_RESUME_FILTERED_FLAGS_WITH_VALUE = new Set([
|
|
300
|
+
"--sandbox",
|
|
301
|
+
"--ask-for-approval",
|
|
302
|
+
"--add-dir",
|
|
303
|
+
"-C",
|
|
304
|
+
"--output-schema",
|
|
305
|
+
]);
|
|
306
|
+
/**
|
|
307
|
+
* Strip resume-incompatible flag/value pairs from a Codex argv segment.
|
|
308
|
+
*
|
|
309
|
+
* Bare flags (`--full-auto`, `--search`) drop without consuming a value.
|
|
310
|
+
* Value-taking flags (`--sandbox`, `--ask-for-approval`, `--add-dir`, `-C`,
|
|
311
|
+
* `--output-schema`) drop together with their immediately-following value.
|
|
312
|
+
*/
|
|
313
|
+
export function filterCodexResumeFlags(args) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (let i = 0; i < args.length; i++) {
|
|
316
|
+
const a = args[i];
|
|
317
|
+
if (!CODEX_RESUME_FILTERED_FLAGS.has(a)) {
|
|
318
|
+
out.push(a);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (CODEX_RESUME_FILTERED_FLAGS_WITH_VALUE.has(a)) {
|
|
322
|
+
i += 1; // also skip the value
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
328
|
+
// U25: Claude high-impact features
|
|
329
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
330
|
+
/**
|
|
331
|
+
* Claude `--effort` enum values. Mirrors the model-side effort axis.
|
|
332
|
+
*/
|
|
333
|
+
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
334
|
+
/**
|
|
335
|
+
* Standalone Zod object for U25's high-impact param subset. Enforces the
|
|
336
|
+
* `systemPrompt` / `appendSystemPrompt` mutual-exclusion via `.refine(...)`.
|
|
337
|
+
*
|
|
338
|
+
* The MCP SDK's `server.tool` takes a raw shape (no top-level refine), so the
|
|
339
|
+
* tool callback re-checks the constraint and returns an error response. This
|
|
340
|
+
* exported schema is what tests use to verify Zod-level enforcement.
|
|
341
|
+
*/
|
|
342
|
+
export const CLAUDE_HIGH_IMPACT_PARAMS_SCHEMA = z
|
|
343
|
+
.object({
|
|
344
|
+
agent: z.string().optional(),
|
|
345
|
+
agents: z.record(z.record(z.unknown())).optional(),
|
|
346
|
+
forkSession: z.boolean().optional(),
|
|
347
|
+
systemPrompt: z.string().optional(),
|
|
348
|
+
appendSystemPrompt: z.string().optional(),
|
|
349
|
+
maxBudgetUsd: z.number().positive().optional(),
|
|
350
|
+
maxTurns: z.number().int().positive().optional(),
|
|
351
|
+
effort: z.enum(CLAUDE_EFFORT_LEVELS).optional(),
|
|
352
|
+
excludeDynamicSystemPromptSections: z.boolean().optional(),
|
|
353
|
+
})
|
|
354
|
+
.refine(data => !(data.systemPrompt !== undefined && data.appendSystemPrompt !== undefined), {
|
|
355
|
+
message: "systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both).",
|
|
356
|
+
path: ["appendSystemPrompt"],
|
|
357
|
+
});
|
|
358
|
+
/**
|
|
359
|
+
* Minimal Anthropic agent-definition schema. Mirrors the shape expected by
|
|
360
|
+
* Claude CLI's `--agents` inline JSON argument. We validate the *required*
|
|
361
|
+
* keys (`description`, `prompt`) up-front so a malformed payload fails fast
|
|
362
|
+
* with an actionable error instead of producing an opaque CLI exit.
|
|
363
|
+
*/
|
|
364
|
+
export const CLAUDE_AGENT_DEFINITION_SCHEMA = z
|
|
365
|
+
.object({
|
|
366
|
+
description: z.string().min(1, "agent.description must be a non-empty string"),
|
|
367
|
+
prompt: z.string().min(1, "agent.prompt must be a non-empty string"),
|
|
368
|
+
tools: z.array(z.string()).optional(),
|
|
369
|
+
model: z.string().optional(),
|
|
370
|
+
})
|
|
371
|
+
.passthrough();
|
|
372
|
+
/**
|
|
373
|
+
* Validate an `agents` map against {@link CLAUDE_AGENT_DEFINITION_SCHEMA}.
|
|
374
|
+
*
|
|
375
|
+
* Returns `{ ok: true, value }` on success and `{ ok: false, agentKey, message }`
|
|
376
|
+
* on the first failing entry. The caller is responsible for turning the failure
|
|
377
|
+
* into a tool-level error response (e.g. via `createErrorResponse`).
|
|
378
|
+
*/
|
|
379
|
+
export function validateClaudeAgentsMap(agents) {
|
|
380
|
+
const validated = {};
|
|
381
|
+
for (const [key, raw] of Object.entries(agents)) {
|
|
382
|
+
const parsed = CLAUDE_AGENT_DEFINITION_SCHEMA.safeParse(raw);
|
|
383
|
+
if (!parsed.success) {
|
|
384
|
+
const issue = parsed.error.issues[0];
|
|
385
|
+
const path = issue?.path?.length ? `.${issue.path.join(".")}` : "";
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
agentKey: key,
|
|
389
|
+
message: `Invalid agent definition for "${key}"${path}: ${issue?.message ?? "schema validation failed"}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
validated[key] = parsed.data;
|
|
393
|
+
}
|
|
394
|
+
return { ok: true, value: validated };
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Emit Claude high-impact feature flags (U25) as a flat argv segment.
|
|
398
|
+
*
|
|
399
|
+
* Mutual-exclusion of `systemPrompt`/`appendSystemPrompt` is enforced upstream
|
|
400
|
+
* at the Zod schema (`.refine(...)`); this helper does *not* re-check it, so
|
|
401
|
+
* tests can exercise either flag in isolation.
|
|
402
|
+
*/
|
|
403
|
+
export function prepareClaudeHighImpactFlags(input) {
|
|
404
|
+
const args = [];
|
|
405
|
+
if (input.agent) {
|
|
406
|
+
args.push("--agent", input.agent);
|
|
407
|
+
}
|
|
408
|
+
if (input.agents && Object.keys(input.agents).length > 0) {
|
|
409
|
+
args.push("--agents", JSON.stringify(input.agents));
|
|
410
|
+
}
|
|
411
|
+
if (input.forkSession) {
|
|
412
|
+
args.push("--fork-session");
|
|
413
|
+
}
|
|
414
|
+
if (input.systemPrompt !== undefined) {
|
|
415
|
+
args.push("--system-prompt", input.systemPrompt);
|
|
416
|
+
}
|
|
417
|
+
if (input.appendSystemPrompt !== undefined) {
|
|
418
|
+
args.push("--append-system-prompt", input.appendSystemPrompt);
|
|
419
|
+
}
|
|
420
|
+
if (input.maxBudgetUsd !== undefined) {
|
|
421
|
+
args.push("--max-budget-usd", String(input.maxBudgetUsd));
|
|
422
|
+
}
|
|
423
|
+
if (input.maxTurns !== undefined) {
|
|
424
|
+
args.push("--max-turns", String(input.maxTurns));
|
|
425
|
+
}
|
|
426
|
+
if (input.effort) {
|
|
427
|
+
args.push("--effort", input.effort);
|
|
428
|
+
}
|
|
429
|
+
if (input.excludeDynamicSystemPromptSections) {
|
|
430
|
+
args.push("--exclude-dynamic-system-prompt-sections");
|
|
431
|
+
}
|
|
432
|
+
return args;
|
|
433
|
+
}
|
|
434
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
435
|
+
// U26: Codex high-impact features
|
|
436
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
437
|
+
/**
|
|
438
|
+
* Zod schema for Codex `configOverrides` map.
|
|
439
|
+
*
|
|
440
|
+
* Hard requirements (argv-injection prevention):
|
|
441
|
+
* - Keys MUST match /^[a-zA-Z0-9._]+$/ (no whitespace, no equals, no flag-like prefixes).
|
|
442
|
+
* - Values MUST NOT contain CR or LF — newlines could be re-interpreted by the
|
|
443
|
+
* CLI's TOML parser as new keys.
|
|
444
|
+
*
|
|
445
|
+
* The CLI consumes overrides as `-c key=value`. We rely on `spawn(..., args)`
|
|
446
|
+
* passing argv directly without a shell, so we forbid shape-breaking
|
|
447
|
+
* characters rather than shell-escaping values.
|
|
448
|
+
*/
|
|
449
|
+
export const CODEX_CONFIG_OVERRIDES_SCHEMA = z
|
|
450
|
+
.record(z
|
|
451
|
+
.string()
|
|
452
|
+
.regex(/^[a-zA-Z0-9._]+$/, "configOverrides keys must match /^[a-zA-Z0-9._]+$/ (no whitespace, '=', or flag-like prefixes)"), z.string().refine(v => !/[\n\r]/.test(v), {
|
|
453
|
+
message: "configOverrides values must not contain CR or LF characters",
|
|
454
|
+
}))
|
|
455
|
+
.optional();
|
|
456
|
+
/**
|
|
457
|
+
* Emit `-c key=value` pairs for each override. Caller MUST have validated the
|
|
458
|
+
* map with {@link CODEX_CONFIG_OVERRIDES_SCHEMA} first.
|
|
459
|
+
*/
|
|
460
|
+
export function emitCodexConfigOverrideArgs(overrides) {
|
|
461
|
+
if (!overrides)
|
|
462
|
+
return [];
|
|
463
|
+
const args = [];
|
|
464
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
465
|
+
args.push("-c", `${key}=${value}`);
|
|
466
|
+
}
|
|
467
|
+
return args;
|
|
468
|
+
}
|
|
469
|
+
export function prepareCodexOutputSchema(outputSchema) {
|
|
470
|
+
if (outputSchema === undefined)
|
|
471
|
+
return null;
|
|
472
|
+
if (typeof outputSchema === "string") {
|
|
473
|
+
return { path: outputSchema, cleanup: () => { } };
|
|
474
|
+
}
|
|
475
|
+
const filename = `codex-schema-${randomUUID()}.json`;
|
|
476
|
+
const path = join(tmpdir(), filename);
|
|
477
|
+
writeFileSync(path, JSON.stringify(outputSchema), { mode: 0o600 });
|
|
478
|
+
let cleaned = false;
|
|
479
|
+
const cleanup = () => {
|
|
480
|
+
if (cleaned)
|
|
481
|
+
return;
|
|
482
|
+
cleaned = true;
|
|
483
|
+
try {
|
|
484
|
+
unlinkSync(path);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// Best-effort: if the file is already gone, ignore.
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
return { path, cleanup };
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Validate that every image path exists on disk. Returns the first missing
|
|
494
|
+
* path on failure; `null` on success.
|
|
495
|
+
*/
|
|
496
|
+
export function findMissingImagePath(images) {
|
|
497
|
+
if (!images || images.length === 0)
|
|
498
|
+
return null;
|
|
499
|
+
for (const p of images) {
|
|
500
|
+
if (!existsSync(p))
|
|
501
|
+
return p;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Zod schema for the U26 Codex high-impact feature subset. Used by the
|
|
507
|
+
* `codex_request` / `codex_request_async` tool schemas to validate the new
|
|
508
|
+
* params before they reach `prepareCodexRequest`.
|
|
509
|
+
*/
|
|
510
|
+
export const CODEX_HIGH_IMPACT_PARAMS_SCHEMA = z.object({
|
|
511
|
+
outputSchema: z.union([z.string(), z.record(z.unknown())]).optional(),
|
|
512
|
+
search: z.boolean().optional(),
|
|
513
|
+
profile: z.string().optional(),
|
|
514
|
+
configOverrides: CODEX_CONFIG_OVERRIDES_SCHEMA,
|
|
515
|
+
ephemeral: z.boolean().optional(),
|
|
516
|
+
images: z.array(z.string()).optional(),
|
|
517
|
+
ignoreUserConfig: z.boolean().optional(),
|
|
518
|
+
ignoreRules: z.boolean().optional(),
|
|
519
|
+
});
|
|
520
|
+
/**
|
|
521
|
+
* Build the U26 argv segment AND any required side-effect handles.
|
|
522
|
+
*
|
|
523
|
+
* IMPORTANT: When this function writes a temp file for `outputSchema`, the
|
|
524
|
+
* returned `cleanup` function MUST be invoked by the caller (typically in a
|
|
525
|
+
* `finally` block around the spawn). Failing to do so leaks `0o600` temp
|
|
526
|
+
* files into `os.tmpdir()`.
|
|
527
|
+
*/
|
|
528
|
+
export function prepareCodexHighImpactFlags(input) {
|
|
529
|
+
const missingImagePath = findMissingImagePath(input.images);
|
|
530
|
+
if (missingImagePath) {
|
|
531
|
+
return { args: [], cleanup: () => { }, missingImagePath };
|
|
532
|
+
}
|
|
533
|
+
const args = [];
|
|
534
|
+
let cleanup = () => { };
|
|
535
|
+
const schema = prepareCodexOutputSchema(input.outputSchema);
|
|
536
|
+
if (schema) {
|
|
537
|
+
args.push("--output-schema", schema.path);
|
|
538
|
+
cleanup = schema.cleanup;
|
|
539
|
+
}
|
|
540
|
+
if (input.search) {
|
|
541
|
+
args.push("--search");
|
|
542
|
+
}
|
|
543
|
+
if (input.profile) {
|
|
544
|
+
args.push("--profile", input.profile);
|
|
545
|
+
}
|
|
546
|
+
args.push(...emitCodexConfigOverrideArgs(input.configOverrides));
|
|
547
|
+
if (input.ephemeral) {
|
|
548
|
+
args.push("--ephemeral");
|
|
549
|
+
}
|
|
550
|
+
if (input.images) {
|
|
551
|
+
for (const img of input.images) {
|
|
552
|
+
args.push("-i", img);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (input.ignoreUserConfig) {
|
|
556
|
+
args.push("--ignore-user-config");
|
|
557
|
+
}
|
|
558
|
+
if (input.ignoreRules) {
|
|
559
|
+
args.push("--ignore-rules");
|
|
560
|
+
}
|
|
561
|
+
return { args, cleanup, missingImagePath: null };
|
|
562
|
+
}
|
|
563
|
+
export function prepareCodexForkRequest(input) {
|
|
564
|
+
const { prompt, sessionId, forkLast } = input;
|
|
565
|
+
const bothSet = Boolean(sessionId) && Boolean(forkLast);
|
|
566
|
+
const neitherSet = !sessionId && !forkLast;
|
|
567
|
+
if (bothSet) {
|
|
568
|
+
throw new Error("codex_fork_session: sessionId and forkLast are mutually exclusive");
|
|
569
|
+
}
|
|
570
|
+
if (neitherSet) {
|
|
571
|
+
throw new Error("codex_fork_session: one of sessionId or forkLast is required");
|
|
572
|
+
}
|
|
573
|
+
if (forkLast) {
|
|
574
|
+
return { args: ["fork", "--last", prompt] };
|
|
575
|
+
}
|
|
576
|
+
// sessionId path
|
|
577
|
+
validateSessionId(sessionId);
|
|
578
|
+
return { args: ["fork", sessionId, prompt] };
|
|
579
|
+
}
|
|
580
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
581
|
+
// U27: Gemini high-impact features
|
|
582
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
583
|
+
/**
|
|
584
|
+
* Strict UUID v4 regex. Gemini's CLI is reportedly stricter about session id
|
|
585
|
+
* shape than the gateway's internal handles, so caller-supplied IDs (and IDs
|
|
586
|
+
* generated by `crypto.randomUUID()`) are validated against this regex before
|
|
587
|
+
* being emitted as `--session-id <uuid>`.
|
|
588
|
+
*/
|
|
589
|
+
export const GEMINI_SESSION_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
590
|
+
export function isValidGeminiSessionId(id) {
|
|
591
|
+
return GEMINI_SESSION_ID_REGEX.test(id);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Prepend `@<abs-path>` tokens to a Gemini prompt so the CLI's attachment
|
|
595
|
+
* resolver picks them up. Each path MUST be absolute and exist on disk.
|
|
596
|
+
*
|
|
597
|
+
* Returns the mutated prompt. Throws on validation failure so the caller can
|
|
598
|
+
* convert to a `createErrorResponse`.
|
|
599
|
+
*/
|
|
600
|
+
export function prependGeminiAttachments(prompt, attachments) {
|
|
601
|
+
if (!attachments || attachments.length === 0)
|
|
602
|
+
return prompt;
|
|
603
|
+
for (const p of attachments) {
|
|
604
|
+
if (!isAbsolute(p)) {
|
|
605
|
+
throw new Error(`attachments: path is not absolute: ${p}`);
|
|
606
|
+
}
|
|
607
|
+
if (!existsSync(p)) {
|
|
608
|
+
throw new Error(`attachments: path does not exist: ${p}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const tokens = attachments.map(p => `@${p}`).join(" ");
|
|
612
|
+
return `${tokens} ${prompt}`;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Zod schema for the U27 Gemini high-impact feature subset. Used by the
|
|
616
|
+
* `gemini_request` / `gemini_request_async` tool schemas to validate the new
|
|
617
|
+
* params before they reach `prepareGeminiRequest`.
|
|
618
|
+
*
|
|
619
|
+
* `attachments` paths are validated to be absolute at the Zod layer; existence
|
|
620
|
+
* is enforced at execution time via `prependGeminiAttachments`.
|
|
621
|
+
*/
|
|
622
|
+
export const GEMINI_HIGH_IMPACT_PARAMS_SCHEMA = z.object({
|
|
623
|
+
sandbox: z.boolean().optional(),
|
|
624
|
+
policyFiles: z.array(z.string()).optional(),
|
|
625
|
+
adminPolicyFiles: z.array(z.string()).optional(),
|
|
626
|
+
attachments: z
|
|
627
|
+
.array(z.string().refine(p => isAbsolute(p), {
|
|
628
|
+
message: "attachments paths must be absolute",
|
|
629
|
+
}))
|
|
630
|
+
.optional(),
|
|
631
|
+
});
|
|
632
|
+
/**
|
|
633
|
+
* Emit Gemini U27 high-impact flags. Policy paths are existence-checked here
|
|
634
|
+
* so a missing file fails fast with an actionable error rather than producing
|
|
635
|
+
* an opaque CLI exit.
|
|
636
|
+
*
|
|
637
|
+
* Does NOT handle `attachments` — those are mutated into the prompt string
|
|
638
|
+
* via {@link prependGeminiAttachments} BEFORE the `-p <prompt>` pair is
|
|
639
|
+
* emitted, preserving the U21 `-p` ordering invariant.
|
|
640
|
+
*/
|
|
641
|
+
export function prepareGeminiHighImpactFlags(input) {
|
|
642
|
+
if (input.policyFiles) {
|
|
643
|
+
for (const p of input.policyFiles) {
|
|
644
|
+
if (!existsSync(p)) {
|
|
645
|
+
return { args: [], missingPolicyPath: p, missingPolicyField: "policyFiles" };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (input.adminPolicyFiles) {
|
|
650
|
+
for (const p of input.adminPolicyFiles) {
|
|
651
|
+
if (!existsSync(p)) {
|
|
652
|
+
return {
|
|
653
|
+
args: [],
|
|
654
|
+
missingPolicyPath: p,
|
|
655
|
+
missingPolicyField: "adminPolicyFiles",
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const args = [];
|
|
661
|
+
if (input.sandbox) {
|
|
662
|
+
args.push("-s");
|
|
663
|
+
}
|
|
664
|
+
if (input.policyFiles) {
|
|
665
|
+
for (const p of input.policyFiles) {
|
|
666
|
+
args.push("--policy", p);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (input.adminPolicyFiles) {
|
|
670
|
+
for (const p of input.adminPolicyFiles) {
|
|
671
|
+
args.push("--admin-policy", p);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { args, missingPolicyPath: null, missingPolicyField: null };
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Resolve Gemini session-id args. When a fresh session is being established
|
|
678
|
+
* (either `createNewSession: true`, or no sessionId/resumeLatest set), emit
|
|
679
|
+
* `--session-id <uuid>` so the gateway and Gemini agree on the session
|
|
680
|
+
* identifier from the first turn.
|
|
681
|
+
*
|
|
682
|
+
* Falls back to `--resume <id>` when the caller supplies a sessionId, and
|
|
683
|
+
* `--resume latest` for `resumeLatest` (existing behavior preserved).
|
|
684
|
+
*/
|
|
685
|
+
export function resolveGeminiSessionPlan(opts) {
|
|
686
|
+
const gen = opts.generateId ?? randomUUID;
|
|
687
|
+
if (opts.sessionId && !opts.createNewSession) {
|
|
688
|
+
validateSessionId(opts.sessionId);
|
|
689
|
+
return {
|
|
690
|
+
args: ["--resume", opts.sessionId],
|
|
691
|
+
resumed: true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (opts.resumeLatest && !opts.createNewSession) {
|
|
695
|
+
return { args: ["--resume", "latest"], resumed: false };
|
|
696
|
+
}
|
|
697
|
+
// Fresh session — emit deterministic --session-id
|
|
698
|
+
const candidate = gen();
|
|
699
|
+
if (!isValidGeminiSessionId(candidate)) {
|
|
700
|
+
throw new Error(`Generated session id "${candidate}" does not match Gemini's UUID v4 format`);
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
args: ["--session-id", candidate],
|
|
704
|
+
emittedSessionId: candidate,
|
|
705
|
+
resumed: false,
|
|
706
|
+
};
|
|
707
|
+
}
|
package/dist/resources.js
CHANGED
|
@@ -65,6 +65,17 @@ export class ResourceProvider {
|
|
|
65
65
|
priority: 0.6,
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
uri: "sessions://mistral",
|
|
70
|
+
name: "Mistral Sessions",
|
|
71
|
+
title: "🌬 Mistral Sessions",
|
|
72
|
+
description: "List of Mistral Vibe conversation sessions",
|
|
73
|
+
mimeType: "application/json",
|
|
74
|
+
annotations: {
|
|
75
|
+
audience: ["user", "assistant"],
|
|
76
|
+
priority: 0.6,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
68
79
|
{
|
|
69
80
|
uri: "models://claude",
|
|
70
81
|
name: "Claude Models",
|
|
@@ -109,6 +120,17 @@ export class ResourceProvider {
|
|
|
109
120
|
priority: 0.8,
|
|
110
121
|
},
|
|
111
122
|
},
|
|
123
|
+
{
|
|
124
|
+
uri: "models://mistral",
|
|
125
|
+
name: "Mistral Models",
|
|
126
|
+
title: "🌬 Mistral Models & Capabilities",
|
|
127
|
+
description: "Available Mistral Vibe models and their capabilities",
|
|
128
|
+
mimeType: "application/json",
|
|
129
|
+
annotations: {
|
|
130
|
+
audience: ["user", "assistant"],
|
|
131
|
+
priority: 0.8,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
112
134
|
{
|
|
113
135
|
uri: "metrics://performance",
|
|
114
136
|
name: "Performance Metrics",
|
|
@@ -144,6 +166,7 @@ export class ResourceProvider {
|
|
|
144
166
|
codex: (await this.sessionManager.getActiveSession("codex"))?.id || null,
|
|
145
167
|
gemini: (await this.sessionManager.getActiveSession("gemini"))?.id || null,
|
|
146
168
|
grok: (await this.sessionManager.getActiveSession("grok"))?.id || null,
|
|
169
|
+
mistral: (await this.sessionManager.getActiveSession("mistral"))?.id || null,
|
|
147
170
|
},
|
|
148
171
|
}, null, 2),
|
|
149
172
|
};
|
|
@@ -200,6 +223,19 @@ export class ResourceProvider {
|
|
|
200
223
|
}, null, 2),
|
|
201
224
|
};
|
|
202
225
|
}
|
|
226
|
+
if (uri === "sessions://mistral") {
|
|
227
|
+
const sessions = await this.sessionManager.listSessions("mistral");
|
|
228
|
+
return {
|
|
229
|
+
uri,
|
|
230
|
+
mimeType: "application/json",
|
|
231
|
+
text: JSON.stringify({
|
|
232
|
+
cli: "mistral",
|
|
233
|
+
total: sessions.length,
|
|
234
|
+
sessions,
|
|
235
|
+
activeSession: (await this.sessionManager.getActiveSession("mistral"))?.id || null,
|
|
236
|
+
}, null, 2),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
203
239
|
// Model capability resources
|
|
204
240
|
if (uri === "models://claude") {
|
|
205
241
|
const cliInfo = getCliInfo();
|
|
@@ -233,6 +269,14 @@ export class ResourceProvider {
|
|
|
233
269
|
text: JSON.stringify(cliInfo.grok, null, 2),
|
|
234
270
|
};
|
|
235
271
|
}
|
|
272
|
+
if (uri === "models://mistral") {
|
|
273
|
+
const cliInfo = getCliInfo();
|
|
274
|
+
return {
|
|
275
|
+
uri,
|
|
276
|
+
mimeType: "application/json",
|
|
277
|
+
text: JSON.stringify(cliInfo.mistral, null, 2),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
236
280
|
if (uri === "metrics://performance") {
|
|
237
281
|
return {
|
|
238
282
|
uri,
|