llm-cli-gateway 1.6.1 → 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 +181 -0
- package/dist/async-job-manager.d.ts +70 -2
- package/dist/async-job-manager.js +166 -6
- package/dist/codex-json-parser.js +4 -1
- package/dist/index.d.ts +32 -0
- package/dist/index.js +152 -36
- package/dist/job-store.d.ts +43 -4
- package/dist/job-store.js +28 -2
- 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
|
@@ -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",
|