llm-cli-gateway 1.7.0 → 1.8.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 +72 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +73 -20
- package/dist/mistral-meta-json-parser.d.ts +6 -0
- package/dist/mistral-meta-json-parser.js +175 -0
- package/dist/request-helpers.d.ts +14 -5
- package/dist/request-helpers.js +8 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the llm-cli-gateway project.
|
|
4
4
|
|
|
5
|
+
## [1.8.0] - 2026-05-27 — Phase 4 openers (codex resume fix, mistral telemetry, headless trust flags)
|
|
6
|
+
|
|
7
|
+
Ships the first three slices of the Phase 4 provider-modernisation
|
|
8
|
+
backlog, one bug fix and two small features. Multi-LLM review surfaced
|
|
9
|
+
five additional bug classes during the cycle (path traversal, UUID→dir
|
|
10
|
+
resolution gap, sync usage ctx drop, retry-path flag drop, symlink
|
|
11
|
+
boundary bypass); all are addressed in the two follow-up fix commits.
|
|
12
|
+
|
|
13
|
+
### Fixed — Codex `--output-schema` + `-c/--config` on `exec resume`
|
|
14
|
+
|
|
15
|
+
- `prepareCodexRequest` previously dropped `outputSchema` and
|
|
16
|
+
`configOverrides` on the resume branch because the U26 audit assumed
|
|
17
|
+
`codex exec resume` rejected both flags. Live re-verification against
|
|
18
|
+
`codex exec resume --help` (codex-cli 0.133.0) confirms both ARE
|
|
19
|
+
accepted on resume; only `--search` remains resume-incompatible. The
|
|
20
|
+
resume branch now threads both fields through, reusing the existing
|
|
21
|
+
outputSchema temp-file materialisation + cleanup contract.
|
|
22
|
+
`CODEX_RESUME_FILTERED_FLAGS` no longer strips `--output-schema`.
|
|
23
|
+
|
|
24
|
+
### Added — Mistral Vibe `meta.json` usage / cost telemetry
|
|
25
|
+
|
|
26
|
+
- New `src/mistral-meta-json-parser.ts` reads
|
|
27
|
+
`~/.vibe/logs/session/session_<YYYYMMDD>_<HHMMSS>_<first8hex>/meta.json`
|
|
28
|
+
(the actual filename — an earlier TODO at `src/index.ts:750` said
|
|
29
|
+
`metadata.json`, which was incorrect). Maps `stats.session_prompt_tokens`,
|
|
30
|
+
`stats.session_completion_tokens`, and `stats.session_cost` onto the
|
|
31
|
+
gateway's `inputTokens`/`outputTokens`/`costUsd` flight-recorder
|
|
32
|
+
columns. Cache-token surfaces stay undefined — Vibe doesn't expose
|
|
33
|
+
them today.
|
|
34
|
+
- The gateway's mistral sessionId surface accepts the full UUID (to match
|
|
35
|
+
`vibe --resume <uuid>`), but Vibe persists telemetry under
|
|
36
|
+
`session_<ts>_<first8>` directories. The new resolver globs by the
|
|
37
|
+
leading 8-hex prefix and verifies each candidate's `session_id` field
|
|
38
|
+
before returning — required for every UUID input including
|
|
39
|
+
single-match cases, so two UUIDs sharing the leading 8 hex chars never
|
|
40
|
+
cross-attribute usage.
|
|
41
|
+
- `extractUsageAndCost` and `buildAsyncFlightRecorderHandoff` thread a
|
|
42
|
+
primitives-only `{ sessionId, home }` context so the AsyncJobRecord
|
|
43
|
+
retention stays O(constant). `buildCliResponse` passes the same ctx so
|
|
44
|
+
sync `mistral_request` resume calls populate structured usage in their
|
|
45
|
+
response (not just the flight-recorder row).
|
|
46
|
+
|
|
47
|
+
### Added — Headless trust-prompt bypass for Gemini + Mistral
|
|
48
|
+
|
|
49
|
+
- New optional `skipTrust?: boolean` field on `gemini_request` and
|
|
50
|
+
`gemini_request_async`, defaulting `false`. When set, emits
|
|
51
|
+
`--skip-trust` so fresh workspaces don't block headless invocations on
|
|
52
|
+
Gemini's interactive trust prompt.
|
|
53
|
+
- New optional `trust?: boolean` field on `mistral_request` and
|
|
54
|
+
`mistral_request_async`, defaulting `false`. When set, emits `--trust`
|
|
55
|
+
(per-invocation only, not persisted to `trusted_folders.toml`) so
|
|
56
|
+
fresh workspaces don't block headless Vibe runs. Preserved on the
|
|
57
|
+
stale-model recovery retry path so a fresh untrusted workspace can't
|
|
58
|
+
deadlock on the second attempt.
|
|
59
|
+
- Default `false` preserves existing prompt behaviour for legacy
|
|
60
|
+
callers.
|
|
61
|
+
|
|
62
|
+
### Security
|
|
63
|
+
|
|
64
|
+
- `parseVibeMetaJson` enforces a strict input charset (UUID-shape OR
|
|
65
|
+
`^session_\d{8}_\d{6}_[0-9a-f]{8}$` Vibe dir basename) before any
|
|
66
|
+
filesystem access.
|
|
67
|
+
- New `readInBase(realBase, candidate)` helper realpath-resolves both
|
|
68
|
+
ends and rejects targets whose final inode lives outside the session
|
|
69
|
+
log root. Both the resolver's disambiguation reads and the final
|
|
70
|
+
parser read route through it, so an in-tree symlink to an
|
|
71
|
+
out-of-tree directory (or symlinked meta.json) cannot leak file
|
|
72
|
+
contents outside `~/.vibe/logs/session/`.
|
|
73
|
+
- Test coverage: traversal inputs (`../`, absolute, control-char,
|
|
74
|
+
embedded `../`), single-candidate prefix-collision rejection,
|
|
75
|
+
symlink-to-outside-baseDir rejection.
|
|
76
|
+
|
|
5
77
|
## [1.7.0] - 2026-05-26 — cache-awareness slice 1.5 (async-path flight recorder + codex parser fix)
|
|
6
78
|
|
|
7
79
|
Closes the two telemetry gaps that v1.6.0 explicitly deferred: async-path
|
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,23 @@ interface GatewayServerRuntime {
|
|
|
81
81
|
persistence: PersistenceConfig;
|
|
82
82
|
cacheAwareness: CacheAwarenessConfig;
|
|
83
83
|
}
|
|
84
|
+
export declare function extractUsageAndCost(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", output: string, outputFormat?: string,
|
|
85
|
+
/**
|
|
86
|
+
* Optional context for off-stdout telemetry sources. Today only Mistral
|
|
87
|
+
* uses this — its meta.json lives on disk keyed by sessionId. Threading
|
|
88
|
+
* this in keeps the closure built by `buildAsyncFlightRecorderHandoff`
|
|
89
|
+
* primitives-only (no `params`/`prep` retention on AsyncJobRecord).
|
|
90
|
+
*/
|
|
91
|
+
ctx?: {
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
home?: string;
|
|
94
|
+
}): {
|
|
95
|
+
inputTokens?: number;
|
|
96
|
+
outputTokens?: number;
|
|
97
|
+
cacheReadTokens?: number;
|
|
98
|
+
cacheCreationTokens?: number;
|
|
99
|
+
costUsd?: number;
|
|
100
|
+
};
|
|
84
101
|
interface CliRequestPrep {
|
|
85
102
|
corrId: string;
|
|
86
103
|
effectivePrompt: string;
|
|
@@ -191,6 +208,12 @@ export declare function prepareGeminiRequest(params: {
|
|
|
191
208
|
policyFiles?: string[];
|
|
192
209
|
adminPolicyFiles?: string[];
|
|
193
210
|
attachments?: string[];
|
|
211
|
+
/**
|
|
212
|
+
* Phase 4 slice γ: emit `--skip-trust` so first-run workspaces don't
|
|
213
|
+
* block headless invocations on the interactive trust prompt. Default
|
|
214
|
+
* is undefined (preserves current prompt behaviour for legacy callers).
|
|
215
|
+
*/
|
|
216
|
+
skipTrust?: boolean;
|
|
194
217
|
}, runtime?: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
|
|
195
218
|
export declare function prepareMistralRequest(params: {
|
|
196
219
|
prompt?: string;
|
|
@@ -208,6 +231,11 @@ export declare function prepareMistralRequest(params: {
|
|
|
208
231
|
correlationId?: string;
|
|
209
232
|
optimizePrompt: boolean;
|
|
210
233
|
operation: string;
|
|
234
|
+
/**
|
|
235
|
+
* Phase 4 slice γ: emit `--trust` to bypass Vibe's interactive trust
|
|
236
|
+
* prompt for this invocation only (not persisted). Default undefined.
|
|
237
|
+
*/
|
|
238
|
+
trust?: boolean;
|
|
211
239
|
}, runtime?: GatewayServerRuntime): (CliRequestPrep & {
|
|
212
240
|
mistralEnv: Record<string, string>;
|
|
213
241
|
}) | ExtendedToolResponse;
|
|
@@ -235,6 +263,8 @@ export interface GeminiRequestParams {
|
|
|
235
263
|
policyFiles?: string[];
|
|
236
264
|
adminPolicyFiles?: string[];
|
|
237
265
|
attachments?: string[];
|
|
266
|
+
/** Phase 4 slice γ: emit `--skip-trust` for fresh-workspace headless runs. */
|
|
267
|
+
skipTrust?: boolean;
|
|
238
268
|
}
|
|
239
269
|
export interface HandlerDeps {
|
|
240
270
|
sessionManager: ISessionManager;
|
|
@@ -297,6 +327,8 @@ export interface MistralRequestParams {
|
|
|
297
327
|
optimizeResponse?: boolean;
|
|
298
328
|
idleTimeoutMs?: number;
|
|
299
329
|
forceRefresh?: boolean;
|
|
330
|
+
/** Phase 4 slice γ: emit `--trust` for fresh-workspace headless runs. */
|
|
331
|
+
trust?: boolean;
|
|
300
332
|
}
|
|
301
333
|
export declare function handleMistralRequest(deps: HandlerDeps, params: MistralRequestParams): Promise<ExtendedToolResponse>;
|
|
302
334
|
export declare function handleMistralRequestAsync(deps: AsyncHandlerDeps, params: Omit<MistralRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { executeCli, killAllProcessGroups } from "./executor.js";
|
|
|
10
10
|
import { parseStreamJson } from "./stream-json-parser.js";
|
|
11
11
|
import { parseCodexJsonStream } from "./codex-json-parser.js";
|
|
12
12
|
import { parseGeminiJson } from "./gemini-json-parser.js";
|
|
13
|
+
import { parseVibeMetaJson } from "./mistral-meta-json-parser.js";
|
|
14
|
+
import { homedir } from "os";
|
|
13
15
|
import { createSessionManager } from "./session-manager.js";
|
|
14
16
|
import { ResourceProvider } from "./resources.js";
|
|
15
17
|
import { PerformanceMetrics } from "./metrics.js";
|
|
@@ -477,7 +479,14 @@ function createErrorResponse(cli, code, stderr, correlationId, error) {
|
|
|
477
479
|
},
|
|
478
480
|
};
|
|
479
481
|
}
|
|
480
|
-
function extractUsageAndCost(cli, output, outputFormat
|
|
482
|
+
export function extractUsageAndCost(cli, output, outputFormat,
|
|
483
|
+
/**
|
|
484
|
+
* Optional context for off-stdout telemetry sources. Today only Mistral
|
|
485
|
+
* uses this — its meta.json lives on disk keyed by sessionId. Threading
|
|
486
|
+
* this in keeps the closure built by `buildAsyncFlightRecorderHandoff`
|
|
487
|
+
* primitives-only (no `params`/`prep` retention on AsyncJobRecord).
|
|
488
|
+
*/
|
|
489
|
+
ctx) {
|
|
481
490
|
if (cli === "claude" && outputFormat === "stream-json") {
|
|
482
491
|
const parsed = parseStreamJson(output);
|
|
483
492
|
if (!parsed.usage) {
|
|
@@ -515,9 +524,14 @@ function extractUsageAndCost(cli, output, outputFormat) {
|
|
|
515
524
|
cacheReadTokens: parsed.usage.cache_read_tokens,
|
|
516
525
|
};
|
|
517
526
|
}
|
|
518
|
-
// Mistral/Vibe:
|
|
519
|
-
//
|
|
520
|
-
//
|
|
527
|
+
// Mistral/Vibe: usage/cost live on disk in `~/.vibe/logs/session/<id>/meta.json`
|
|
528
|
+
// (Phase 4 slice β). Best-effort: if we don't know the sessionId (fresh
|
|
529
|
+
// session whose Vibe-assigned UUID we never observed) or the file is
|
|
530
|
+
// missing/malformed, the parser returns `{}` and the FR row simply lacks
|
|
531
|
+
// usage data — matching pre-slice behaviour. No stdout fallback exists.
|
|
532
|
+
if (cli === "mistral") {
|
|
533
|
+
return parseVibeMetaJson(ctx?.home ?? homedir(), ctx?.sessionId);
|
|
534
|
+
}
|
|
521
535
|
return {};
|
|
522
536
|
}
|
|
523
537
|
/**
|
|
@@ -530,9 +544,13 @@ function extractUsageAndCost(cli, output, outputFormat) {
|
|
|
530
544
|
function buildAsyncFlightRecorderHandoff(cliName, prep, sessionId, outputFormat) {
|
|
531
545
|
// Extract primitives BEFORE building the closure — capturing `prep` or
|
|
532
546
|
// `params` directly would pin large attachments / promptParts on the
|
|
533
|
-
// AsyncJobRecord for JOB_TTL_MS.
|
|
547
|
+
// AsyncJobRecord for JOB_TTL_MS. Phase 4 slice β: `sid` and `home` are
|
|
548
|
+
// primitives too, threaded through so the Mistral branch of
|
|
549
|
+
// extractUsageAndCost can read `~/.vibe/logs/session/<id>/meta.json`.
|
|
534
550
|
const cli = cliName;
|
|
535
551
|
const fmt = outputFormat;
|
|
552
|
+
const sid = sessionId;
|
|
553
|
+
const home = homedir();
|
|
536
554
|
return {
|
|
537
555
|
flightRecorderEntry: {
|
|
538
556
|
model: prep.resolvedModel || "default",
|
|
@@ -541,7 +559,7 @@ function buildAsyncFlightRecorderHandoff(cliName, prep, sessionId, outputFormat)
|
|
|
541
559
|
stablePrefixHash: prep.stablePrefixHash ?? undefined,
|
|
542
560
|
stablePrefixTokens: prep.stablePrefixTokens ?? undefined,
|
|
543
561
|
},
|
|
544
|
-
extractUsage: (stdout) => extractUsageAndCost(cli, stdout, fmt),
|
|
562
|
+
extractUsage: (stdout) => extractUsageAndCost(cli, stdout, fmt, { sessionId: sid, home }),
|
|
545
563
|
};
|
|
546
564
|
}
|
|
547
565
|
function safeFlightStart(entry, runtime = resolveGatewayServerRuntime()) {
|
|
@@ -1081,11 +1099,12 @@ export function prepareCodexRequest(params, runtime = resolveGatewayServerRuntim
|
|
|
1081
1099
|
args.push("--json");
|
|
1082
1100
|
}
|
|
1083
1101
|
args.push("--skip-git-repo-check");
|
|
1084
|
-
// U26: High-impact feature flags.
|
|
1085
|
-
//
|
|
1086
|
-
// only emit
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1102
|
+
// U26: High-impact feature flags. `--search` is rejected by
|
|
1103
|
+
// `codex exec resume` (resume inherits the original session's web-search
|
|
1104
|
+
// state), so we only emit it on a NEW session. `--output-schema`,
|
|
1105
|
+
// `-c key=value`, profile, ephemeral, images, and the ignore-* flags are
|
|
1106
|
+
// all accepted on resume per `codex exec resume --help` (codex-cli 0.133.0)
|
|
1107
|
+
// and are emitted in both branches.
|
|
1089
1108
|
let highImpactCleanup;
|
|
1090
1109
|
if (sessionPlan.mode === "new") {
|
|
1091
1110
|
const high = prepareCodexHighImpactFlags({
|
|
@@ -1105,12 +1124,10 @@ export function prepareCodexRequest(params, runtime = resolveGatewayServerRuntim
|
|
|
1105
1124
|
highImpactCleanup = high.cleanup;
|
|
1106
1125
|
}
|
|
1107
1126
|
else {
|
|
1108
|
-
// On resume, emit only the resume-safe subset (profile, ephemeral,
|
|
1109
|
-
// images, ignoreUserConfig, ignoreRules). outputSchema, search, and
|
|
1110
|
-
// configOverrides are dropped silently to mirror existing behavior for
|
|
1111
|
-
// sandbox/ask-for-approval on resume.
|
|
1112
1127
|
const high = prepareCodexHighImpactFlags({
|
|
1128
|
+
outputSchema: params.outputSchema,
|
|
1113
1129
|
profile: params.profile,
|
|
1130
|
+
configOverrides: params.configOverrides,
|
|
1114
1131
|
ephemeral: params.ephemeral,
|
|
1115
1132
|
images: params.images,
|
|
1116
1133
|
ignoreUserConfig: params.ignoreUserConfig,
|
|
@@ -1240,6 +1257,10 @@ export function prepareGeminiRequest(params, runtime = resolveGatewayServerRunti
|
|
|
1240
1257
|
if (params.outputFormat === "json") {
|
|
1241
1258
|
args.push("-o", "json");
|
|
1242
1259
|
}
|
|
1260
|
+
// Phase 4 slice γ: opt-in trust-prompt bypass for fresh workspaces.
|
|
1261
|
+
if (params.skipTrust) {
|
|
1262
|
+
args.push("--skip-trust");
|
|
1263
|
+
}
|
|
1243
1264
|
return {
|
|
1244
1265
|
corrId,
|
|
1245
1266
|
effectivePrompt,
|
|
@@ -1411,6 +1432,7 @@ export function prepareMistralRequest(params, runtime = resolveGatewayServerRunt
|
|
|
1411
1432
|
reasoningEffort: params.reasoningEffort,
|
|
1412
1433
|
allowedTools: params.allowedTools,
|
|
1413
1434
|
disallowedTools: params.disallowedTools,
|
|
1435
|
+
trust: params.trust,
|
|
1414
1436
|
});
|
|
1415
1437
|
if (prep.ignoredDisallowedTools) {
|
|
1416
1438
|
runtime.logger.info(`[${corrId}] Mistral does not support disallowedTools; ignoring (caller passed ${params.disallowedTools?.length ?? 0} entries)`);
|
|
@@ -1466,7 +1488,10 @@ function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep
|
|
|
1466
1488
|
correlationId: corrId,
|
|
1467
1489
|
sessionId: sessionId || null,
|
|
1468
1490
|
durationMs,
|
|
1469
|
-
|
|
1491
|
+
// Phase 4 slice β: thread sessionId + home so the Mistral branch of
|
|
1492
|
+
// extractUsageAndCost can read `~/.vibe/logs/session/<dir>/meta.json`.
|
|
1493
|
+
// Other CLIs ignore the ctx (their usage source is stdout).
|
|
1494
|
+
...extractUsageAndCost(cli, stdout, outputFormat, { sessionId, home: homedir() }),
|
|
1470
1495
|
exitCode: 0,
|
|
1471
1496
|
retryCount: 0,
|
|
1472
1497
|
},
|
|
@@ -1564,6 +1589,7 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
1564
1589
|
policyFiles: params.policyFiles,
|
|
1565
1590
|
adminPolicyFiles: params.adminPolicyFiles,
|
|
1566
1591
|
attachments: params.attachments,
|
|
1592
|
+
skipTrust: params.skipTrust,
|
|
1567
1593
|
}, runtime);
|
|
1568
1594
|
if (!("args" in prep))
|
|
1569
1595
|
return prep;
|
|
@@ -1692,6 +1718,7 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
1692
1718
|
policyFiles: params.policyFiles,
|
|
1693
1719
|
adminPolicyFiles: params.adminPolicyFiles,
|
|
1694
1720
|
attachments: params.attachments,
|
|
1721
|
+
skipTrust: params.skipTrust,
|
|
1695
1722
|
}, runtime);
|
|
1696
1723
|
if (!("args" in prep))
|
|
1697
1724
|
return prep;
|
|
@@ -1975,6 +2002,7 @@ export async function handleMistralRequest(deps, params) {
|
|
|
1975
2002
|
correlationId: params.correlationId,
|
|
1976
2003
|
optimizePrompt: params.optimizePrompt,
|
|
1977
2004
|
operation: "mistral_request",
|
|
2005
|
+
trust: params.trust,
|
|
1978
2006
|
}, runtime);
|
|
1979
2007
|
if (!("args" in prep))
|
|
1980
2008
|
return prep;
|
|
@@ -2018,6 +2046,10 @@ export async function handleMistralRequest(deps, params) {
|
|
|
2018
2046
|
reasoningEffort: params.reasoningEffort,
|
|
2019
2047
|
allowedTools: params.allowedTools,
|
|
2020
2048
|
disallowedTools: params.disallowedTools,
|
|
2049
|
+
// Phase 4 slice γ: preserve --trust on the model-selection retry
|
|
2050
|
+
// so a fresh untrusted workspace doesn't block headlessly on the
|
|
2051
|
+
// second attempt after surviving the first.
|
|
2052
|
+
trust: params.trust,
|
|
2021
2053
|
});
|
|
2022
2054
|
const retryArgs = [...retryPrep.args, ...sessionResult.resumeArgs];
|
|
2023
2055
|
// Reuse the FR handoff built above — the retry preserves corrId,
|
|
@@ -2118,6 +2150,7 @@ export async function handleMistralRequestAsync(deps, params) {
|
|
|
2118
2150
|
correlationId: params.correlationId,
|
|
2119
2151
|
optimizePrompt: params.optimizePrompt,
|
|
2120
2152
|
operation: "mistral_request_async",
|
|
2153
|
+
trust: params.trust,
|
|
2121
2154
|
}, runtime);
|
|
2122
2155
|
if (!("args" in prep))
|
|
2123
2156
|
return prep;
|
|
@@ -3006,7 +3039,11 @@ export function createGatewayServer(deps = {}) {
|
|
|
3006
3039
|
policyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.policyFiles.describe("Policy file paths (--policy <path>, one per file). Paths must exist."),
|
|
3007
3040
|
adminPolicyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.adminPolicyFiles.describe("Admin policy file paths (--admin-policy <path>, one per file). Paths must exist."),
|
|
3008
3041
|
attachments: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.attachments.describe("Absolute file paths prepended as @<path> tokens to the prompt"),
|
|
3009
|
-
|
|
3042
|
+
skipTrust: z
|
|
3043
|
+
.boolean()
|
|
3044
|
+
.default(false)
|
|
3045
|
+
.describe("Emit `--skip-trust` so Gemini trusts the workspace for this session and skips the interactive trust prompt (Phase 4 slice γ). Required for headless runs in fresh workspaces."),
|
|
3046
|
+
}, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, }) => {
|
|
3010
3047
|
return handleGeminiRequest({ sessionManager, logger, runtime }, {
|
|
3011
3048
|
prompt,
|
|
3012
3049
|
promptParts,
|
|
@@ -3030,6 +3067,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3030
3067
|
policyFiles,
|
|
3031
3068
|
adminPolicyFiles,
|
|
3032
3069
|
attachments,
|
|
3070
|
+
skipTrust,
|
|
3033
3071
|
});
|
|
3034
3072
|
});
|
|
3035
3073
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -3200,7 +3238,11 @@ export function createGatewayServer(deps = {}) {
|
|
|
3200
3238
|
.boolean()
|
|
3201
3239
|
.default(false)
|
|
3202
3240
|
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
3203
|
-
|
|
3241
|
+
trust: z
|
|
3242
|
+
.boolean()
|
|
3243
|
+
.default(false)
|
|
3244
|
+
.describe("Emit `--trust` so Vibe trusts the cwd for this invocation only (not persisted to trusted_folders.toml) and skips the interactive trust prompt (Phase 4 slice γ)."),
|
|
3245
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, }) => {
|
|
3204
3246
|
return handleMistralRequest({ sessionManager, logger, runtime }, {
|
|
3205
3247
|
prompt,
|
|
3206
3248
|
promptParts,
|
|
@@ -3222,6 +3264,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3222
3264
|
optimizeResponse,
|
|
3223
3265
|
idleTimeoutMs,
|
|
3224
3266
|
forceRefresh,
|
|
3267
|
+
trust,
|
|
3225
3268
|
});
|
|
3226
3269
|
});
|
|
3227
3270
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -3612,7 +3655,11 @@ export function createGatewayServer(deps = {}) {
|
|
|
3612
3655
|
policyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.policyFiles.describe("Policy file paths (--policy <path>, one per file). Paths must exist."),
|
|
3613
3656
|
adminPolicyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.adminPolicyFiles.describe("Admin policy file paths (--admin-policy <path>, one per file). Paths must exist."),
|
|
3614
3657
|
attachments: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.attachments.describe("Absolute file paths prepended as @<path> tokens to the prompt"),
|
|
3615
|
-
|
|
3658
|
+
skipTrust: z
|
|
3659
|
+
.boolean()
|
|
3660
|
+
.default(false)
|
|
3661
|
+
.describe("Emit `--skip-trust` so Gemini trusts the workspace for this session and skips the interactive trust prompt (Phase 4 slice γ). Required for headless runs in fresh workspaces."),
|
|
3662
|
+
}, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, }) => {
|
|
3616
3663
|
return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
3617
3664
|
prompt,
|
|
3618
3665
|
promptParts,
|
|
@@ -3635,6 +3682,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3635
3682
|
policyFiles,
|
|
3636
3683
|
adminPolicyFiles,
|
|
3637
3684
|
attachments,
|
|
3685
|
+
skipTrust,
|
|
3638
3686
|
});
|
|
3639
3687
|
});
|
|
3640
3688
|
server.tool("grok_request_async", {
|
|
@@ -3796,7 +3844,11 @@ export function createGatewayServer(deps = {}) {
|
|
|
3796
3844
|
.boolean()
|
|
3797
3845
|
.default(false)
|
|
3798
3846
|
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
3799
|
-
|
|
3847
|
+
trust: z
|
|
3848
|
+
.boolean()
|
|
3849
|
+
.default(false)
|
|
3850
|
+
.describe("Emit `--trust` so Vibe trusts the cwd for this invocation only (not persisted to trusted_folders.toml) and skips the interactive trust prompt (Phase 4 slice γ)."),
|
|
3851
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, }) => {
|
|
3800
3852
|
return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
3801
3853
|
prompt,
|
|
3802
3854
|
promptParts,
|
|
@@ -3817,6 +3869,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3817
3869
|
optimizePrompt,
|
|
3818
3870
|
idleTimeoutMs,
|
|
3819
3871
|
forceRefresh,
|
|
3872
|
+
trust,
|
|
3820
3873
|
});
|
|
3821
3874
|
});
|
|
3822
3875
|
server.tool("llm_job_status", {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 slice β — Mistral Vibe `meta.json` parser.
|
|
3
|
+
*
|
|
4
|
+
* Vibe writes per-session telemetry to
|
|
5
|
+
*
|
|
6
|
+
* ~/.vibe/logs/session/session_<YYYYMMDD>_<HHMMSS>_<first8hex>/meta.json
|
|
7
|
+
*
|
|
8
|
+
* where `<first8hex>` is the first 8 lowercase hex characters of the full
|
|
9
|
+
* session UUID. Inside the file:
|
|
10
|
+
*
|
|
11
|
+
* {
|
|
12
|
+
* "session_id": "<full-uuid>",
|
|
13
|
+
* "stats": {
|
|
14
|
+
* "session_prompt_tokens": <number> → inputTokens
|
|
15
|
+
* "session_completion_tokens": <number> → outputTokens
|
|
16
|
+
* "session_cost": <number> → costUsd
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* The gateway's mistral session-id surface accepts the full UUID (so does
|
|
21
|
+
* `vibe --resume <uuid>`). To find the right directory we glob for
|
|
22
|
+
* `session_*_<first8>` and disambiguate by reading each candidate's
|
|
23
|
+
* `session_id` field. If callers happen to pass the directory basename
|
|
24
|
+
* itself we still honour that — useful for tests and for forward-compat if
|
|
25
|
+
* Vibe ever changes its dir naming scheme.
|
|
26
|
+
*
|
|
27
|
+
* Cache-token surfaces are not exposed by Vibe today, so `cacheReadTokens`
|
|
28
|
+
* and `cacheCreationTokens` are intentionally absent.
|
|
29
|
+
*
|
|
30
|
+
* Best-effort by design: any failure (missing file, bad JSON, missing
|
|
31
|
+
* fields, gateway-generated `gw-*` sessionId, unresolvable UUID, path
|
|
32
|
+
* outside the session log root) returns `{}` so the flight-recorder row
|
|
33
|
+
* simply lacks usage data.
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
36
|
+
import { join, resolve, sep } from "path";
|
|
37
|
+
import { GATEWAY_SESSION_PREFIX } from "./request-helpers.js";
|
|
38
|
+
function asPositiveNumber(value) {
|
|
39
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read a file only if its realpath lives under `realBase`. Returns undefined
|
|
46
|
+
* on any error, missing file, or out-of-tree symlink target. This is the one
|
|
47
|
+
* place that calls `readFileSync` for meta.json content — the rest of the
|
|
48
|
+
* module routes through it so the security boundary is uniform.
|
|
49
|
+
*/
|
|
50
|
+
function readInBase(realBase, candidate) {
|
|
51
|
+
if (!existsSync(candidate))
|
|
52
|
+
return undefined;
|
|
53
|
+
let realCandidate;
|
|
54
|
+
try {
|
|
55
|
+
realCandidate = realpathSync(candidate);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const realBaseWithSep = realBase.endsWith(sep) ? realBase : realBase + sep;
|
|
61
|
+
if (!realCandidate.startsWith(realBaseWithSep))
|
|
62
|
+
return undefined;
|
|
63
|
+
try {
|
|
64
|
+
return readFileSync(realCandidate, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// UUID v4-ish (Vibe's own session UUIDs are not strictly v4, so we
|
|
71
|
+
// validate against the broader 8-4-4-4-12 lowercase-hex shape) OR
|
|
72
|
+
// Vibe's session_<digits>_<digits>_<first8> directory basename.
|
|
73
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
74
|
+
const DIRNAME_RE = /^session_\d{8}_\d{6}_[0-9a-f]{8}$/;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the session-log directory basename for a given gateway sessionId.
|
|
77
|
+
* Returns undefined when no candidate can be found or the input is
|
|
78
|
+
* unsuitable. Pure with respect to side-effects on the caller — only reads
|
|
79
|
+
* the filesystem.
|
|
80
|
+
*
|
|
81
|
+
* Security invariants enforced here:
|
|
82
|
+
* - Inputs are charset-gated (UUID or DIRNAME) before any filesystem read.
|
|
83
|
+
* - For UUID input, the chosen candidate's meta.json MUST advertise the
|
|
84
|
+
* same `session_id` — single-candidate is NOT trusted, because two
|
|
85
|
+
* UUIDs sharing the first 8 hex chars would otherwise cross-attribute
|
|
86
|
+
* usage (and leak telemetry to the caller of the other session).
|
|
87
|
+
*/
|
|
88
|
+
function resolveVibeSessionDirname(baseDir, realBase, sessionId) {
|
|
89
|
+
// 1. Caller already supplied the directory name verbatim.
|
|
90
|
+
if (DIRNAME_RE.test(sessionId) && existsSync(join(baseDir, sessionId, "meta.json"))) {
|
|
91
|
+
return sessionId;
|
|
92
|
+
}
|
|
93
|
+
// 2. Treat the input as a full session UUID.
|
|
94
|
+
if (!UUID_RE.test(sessionId))
|
|
95
|
+
return undefined;
|
|
96
|
+
const short = sessionId.slice(0, 8).toLowerCase();
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = readdirSync(baseDir);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
// Filter to candidates matching `session_*_<short>`. Sort newest-first
|
|
105
|
+
// by mtime; we still require an exact session_id match below.
|
|
106
|
+
const candidates = entries
|
|
107
|
+
.filter(name => DIRNAME_RE.test(name) && name.endsWith(`_${short}`))
|
|
108
|
+
.map(name => {
|
|
109
|
+
let mtimeMs = 0;
|
|
110
|
+
try {
|
|
111
|
+
mtimeMs = statSync(join(baseDir, name)).mtimeMs;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* ignore */
|
|
115
|
+
}
|
|
116
|
+
return { name, mtimeMs };
|
|
117
|
+
})
|
|
118
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
119
|
+
for (const { name } of candidates) {
|
|
120
|
+
const text = readInBase(realBase, join(baseDir, name, "meta.json"));
|
|
121
|
+
if (text === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(text);
|
|
125
|
+
if (typeof parsed.session_id === "string" && parsed.session_id === sessionId) {
|
|
126
|
+
return name;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* ignore and continue */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
export function parseVibeMetaJson(home, sessionId) {
|
|
136
|
+
if (!sessionId)
|
|
137
|
+
return {};
|
|
138
|
+
if (sessionId.startsWith(GATEWAY_SESSION_PREFIX)) {
|
|
139
|
+
// gw-* IDs are gateway internal — Vibe never wrote a meta.json under that name.
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
const baseDir = resolve(join(home, ".vibe", "logs", "session"));
|
|
143
|
+
let realBase;
|
|
144
|
+
try {
|
|
145
|
+
realBase = realpathSync(baseDir);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
const dirname = resolveVibeSessionDirname(baseDir, realBase, sessionId);
|
|
151
|
+
if (!dirname)
|
|
152
|
+
return {};
|
|
153
|
+
// `readInBase` is the security boundary: it realpath-resolves the file
|
|
154
|
+
// and rejects anything whose target lives outside `realBase`. Re-routing
|
|
155
|
+
// the final read through it (instead of a bespoke readFileSync) keeps
|
|
156
|
+
// the in-tree-only invariant in one place.
|
|
157
|
+
const text = readInBase(realBase, join(baseDir, dirname, "meta.json"));
|
|
158
|
+
if (text === undefined)
|
|
159
|
+
return {};
|
|
160
|
+
let raw;
|
|
161
|
+
try {
|
|
162
|
+
raw = JSON.parse(text);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
const stats = raw?.stats;
|
|
168
|
+
if (!stats || typeof stats !== "object")
|
|
169
|
+
return {};
|
|
170
|
+
return {
|
|
171
|
+
inputTokens: asPositiveNumber(stats.session_prompt_tokens),
|
|
172
|
+
outputTokens: asPositiveNumber(stats.session_completion_tokens),
|
|
173
|
+
costUsd: asPositiveNumber(stats.session_cost),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -107,6 +107,13 @@ export interface PrepareMistralRequestInput {
|
|
|
107
107
|
* emit a `logger.warn` when this is non-empty.
|
|
108
108
|
*/
|
|
109
109
|
disallowedTools?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* Phase 4 slice γ: emit `--trust` so non-interactive runs in fresh
|
|
112
|
+
* workspaces skip Vibe's interactive trust prompt for this invocation
|
|
113
|
+
* only (not persisted to `trusted_folders.toml`). Default undefined →
|
|
114
|
+
* Vibe's prompt behaviour is preserved for existing callers.
|
|
115
|
+
*/
|
|
116
|
+
trust?: boolean;
|
|
110
117
|
}
|
|
111
118
|
export interface PrepareMistralRequestResult {
|
|
112
119
|
args: string[];
|
|
@@ -204,9 +211,11 @@ export declare function resolveCodexSandboxFlags(input: CodexSandboxFlagsInput):
|
|
|
204
211
|
* Flags that `codex exec resume` rejects (the original session's policy is
|
|
205
212
|
* inherited). Callers must drop these when building resume argv.
|
|
206
213
|
*
|
|
207
|
-
*
|
|
208
|
-
* `--
|
|
209
|
-
*
|
|
214
|
+
* Verified against `codex exec resume --help` (codex-cli 0.133.0):
|
|
215
|
+
* `--full-auto`, `--sandbox`, `--ask-for-approval`, `--add-dir`, `-C`, and
|
|
216
|
+
* `--search` are rejected. `--output-schema` and `-c key=value` ARE accepted
|
|
217
|
+
* on resume and therefore are NOT in this filter (Phase 4 slice α restored
|
|
218
|
+
* the previously-silent drop of those two).
|
|
210
219
|
*/
|
|
211
220
|
export declare const CODEX_RESUME_FILTERED_FLAGS: ReadonlySet<string>;
|
|
212
221
|
/**
|
|
@@ -398,8 +407,8 @@ export declare const CODEX_HIGH_IMPACT_PARAMS_SCHEMA: z.ZodObject<{
|
|
|
398
407
|
ignoreRules: z.ZodOptional<z.ZodBoolean>;
|
|
399
408
|
}, "strip", z.ZodTypeAny, {
|
|
400
409
|
search?: boolean | undefined;
|
|
401
|
-
profile?: string | undefined;
|
|
402
410
|
outputSchema?: string | Record<string, unknown> | undefined;
|
|
411
|
+
profile?: string | undefined;
|
|
403
412
|
configOverrides?: Record<string, string> | undefined;
|
|
404
413
|
ephemeral?: boolean | undefined;
|
|
405
414
|
images?: string[] | undefined;
|
|
@@ -407,8 +416,8 @@ export declare const CODEX_HIGH_IMPACT_PARAMS_SCHEMA: z.ZodObject<{
|
|
|
407
416
|
ignoreRules?: boolean | undefined;
|
|
408
417
|
}, {
|
|
409
418
|
search?: boolean | undefined;
|
|
410
|
-
profile?: string | undefined;
|
|
411
419
|
outputSchema?: string | Record<string, unknown> | undefined;
|
|
420
|
+
profile?: string | undefined;
|
|
412
421
|
configOverrides?: Record<string, string> | undefined;
|
|
413
422
|
ephemeral?: boolean | undefined;
|
|
414
423
|
images?: string[] | undefined;
|
package/dist/request-helpers.js
CHANGED
|
@@ -176,6 +176,9 @@ export function prepareMistralRequest(input) {
|
|
|
176
176
|
args.push("--enabled-tools", tool);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
if (input.trust) {
|
|
180
|
+
args.push("--trust");
|
|
181
|
+
}
|
|
179
182
|
const ignoredDisallowedTools = Boolean(input.disallowedTools && input.disallowedTools.length > 0);
|
|
180
183
|
return { args, env, ignoredDisallowedTools };
|
|
181
184
|
}
|
|
@@ -279,9 +282,11 @@ export function resolveCodexSandboxFlags(input) {
|
|
|
279
282
|
* Flags that `codex exec resume` rejects (the original session's policy is
|
|
280
283
|
* inherited). Callers must drop these when building resume argv.
|
|
281
284
|
*
|
|
282
|
-
*
|
|
283
|
-
* `--
|
|
284
|
-
*
|
|
285
|
+
* Verified against `codex exec resume --help` (codex-cli 0.133.0):
|
|
286
|
+
* `--full-auto`, `--sandbox`, `--ask-for-approval`, `--add-dir`, `-C`, and
|
|
287
|
+
* `--search` are rejected. `--output-schema` and `-c key=value` ARE accepted
|
|
288
|
+
* on resume and therefore are NOT in this filter (Phase 4 slice α restored
|
|
289
|
+
* the previously-silent drop of those two).
|
|
285
290
|
*/
|
|
286
291
|
export const CODEX_RESUME_FILTERED_FLAGS = new Set([
|
|
287
292
|
"--full-auto",
|
|
@@ -289,7 +294,6 @@ export const CODEX_RESUME_FILTERED_FLAGS = new Set([
|
|
|
289
294
|
"--ask-for-approval",
|
|
290
295
|
"--add-dir",
|
|
291
296
|
"-C",
|
|
292
|
-
"--output-schema",
|
|
293
297
|
"--search",
|
|
294
298
|
]);
|
|
295
299
|
/**
|
|
@@ -301,7 +305,6 @@ const CODEX_RESUME_FILTERED_FLAGS_WITH_VALUE = new Set([
|
|
|
301
305
|
"--ask-for-approval",
|
|
302
306
|
"--add-dir",
|
|
303
307
|
"-C",
|
|
304
|
-
"--output-schema",
|
|
305
308
|
]);
|
|
306
309
|
/**
|
|
307
310
|
* Strip resume-incompatible flag/value pairs from a Codex argv segment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"mcpName": "io.github.verivus-oss/llm-cli-gateway",
|
|
5
5
|
"description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
|
|
6
6
|
"license": "MIT",
|