llm-cli-gateway 2.4.0 → 2.6.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 +45 -0
- package/README.md +18 -18
- package/dist/async-job-manager.d.ts +2 -0
- package/dist/async-job-manager.js +43 -3
- package/dist/auth.d.ts +44 -1
- package/dist/auth.js +60 -13
- package/dist/cli-updater.js +22 -13
- package/dist/config.d.ts +2 -0
- package/dist/config.js +151 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +22 -11
- package/dist/executor.d.ts +1 -0
- package/dist/executor.js +7 -0
- package/dist/http-transport.js +74 -12
- package/dist/index.d.ts +16 -1
- package/dist/index.js +643 -306
- package/dist/oauth.d.ts +38 -0
- package/dist/oauth.js +441 -0
- package/dist/provider-codegen.d.ts +27 -0
- package/dist/provider-codegen.js +335 -0
- package/dist/provider-login-guidance.js +9 -9
- package/dist/provider-status.js +5 -5
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +8 -0
- package/dist/request-helpers.js +2 -2
- package/dist/upstream-contracts.js +95 -116
- package/dist/workspace-registry.d.ts +63 -0
- package/dist/workspace-registry.js +417 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +42 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, openSync, closeSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { z } from "zod/v3";
|
|
7
|
+
import { CLI_TYPES } from "./session-manager.js";
|
|
8
|
+
import { defaultGatewayConfigPath } from "./config.js";
|
|
9
|
+
import { logWarn, noopLogger } from "./logger.js";
|
|
10
|
+
const ALIAS_PATTERN = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
|
|
11
|
+
const SAFE_SEGMENT_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
12
|
+
const DENIED_NAMES = new Set([
|
|
13
|
+
".llm-cli-gateway",
|
|
14
|
+
".ssh",
|
|
15
|
+
".aws",
|
|
16
|
+
".azure",
|
|
17
|
+
".config",
|
|
18
|
+
".gnupg",
|
|
19
|
+
".kube",
|
|
20
|
+
".password-store",
|
|
21
|
+
]);
|
|
22
|
+
const WorkspaceRepoSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
alias: z.string().min(1),
|
|
25
|
+
path: z.string().min(1),
|
|
26
|
+
providers: z.array(z.enum(CLI_TYPES)).default([...CLI_TYPES]),
|
|
27
|
+
allow_worktree: z.boolean().default(true),
|
|
28
|
+
allow_add_dir: z.boolean().default(false),
|
|
29
|
+
kind: z.enum(["git", "folder"]).default("git"),
|
|
30
|
+
operator_entry: z.boolean().default(true),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const WorkspaceAllowedRootSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
alias: z.string().min(1).optional(),
|
|
36
|
+
path: z.string().min(1),
|
|
37
|
+
allow_register_existing_git_repos: z.boolean().default(false),
|
|
38
|
+
allow_create_directories: z.boolean().default(false),
|
|
39
|
+
allow_init_git_repos: z.boolean().default(false),
|
|
40
|
+
max_create_depth: z.number().int().min(1).max(8).default(2),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
const WorkspacesSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
default: z.string().optional(),
|
|
46
|
+
allow_unregistered_working_dir: z.boolean().default(false),
|
|
47
|
+
repos: z.array(WorkspaceRepoSchema).default([]),
|
|
48
|
+
allowed_roots: z.array(WorkspaceAllowedRootSchema).default([]),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export class WorkspaceRegistryError extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "WorkspaceRegistryError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function expandHome(p) {
|
|
58
|
+
return p === "~" ? homedir() : p.startsWith("~/") ? path.join(homedir(), p.slice(2)) : p;
|
|
59
|
+
}
|
|
60
|
+
function readToml(configPath) {
|
|
61
|
+
if (!existsSync(configPath))
|
|
62
|
+
return {};
|
|
63
|
+
const require = createRequire(import.meta.url);
|
|
64
|
+
const TOML = require("smol-toml");
|
|
65
|
+
return TOML.parse(readFileSync(configPath, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
function writeToml(configPath, data) {
|
|
68
|
+
const require = createRequire(import.meta.url);
|
|
69
|
+
const TOML = require("smol-toml");
|
|
70
|
+
const dir = path.dirname(configPath);
|
|
71
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
+
const tmp = path.join(dir, `.config.toml.tmp.${process.pid}.${Date.now()}`);
|
|
73
|
+
writeFileSync(tmp, TOML.stringify(data), { mode: 0o600 });
|
|
74
|
+
const fd = openSync(tmp, "r+");
|
|
75
|
+
closeSync(fd);
|
|
76
|
+
renameSync(tmp, configPath);
|
|
77
|
+
chmodSync(configPath, 0o600);
|
|
78
|
+
}
|
|
79
|
+
export function validateWorkspaceAlias(alias) {
|
|
80
|
+
if (!ALIAS_PATTERN.test(alias) || alias === "." || alias === ".." || alias.includes("..")) {
|
|
81
|
+
throw new WorkspaceRegistryError(`Invalid workspace alias "${alias}" (allowed: A-Z a-z 0-9 . _ -, must start with a letter)`);
|
|
82
|
+
}
|
|
83
|
+
return alias;
|
|
84
|
+
}
|
|
85
|
+
function defaultRootAlias(rootPath, existing) {
|
|
86
|
+
const base = path.basename(rootPath).replace(/[^A-Za-z0-9._-]/g, "-") || "root";
|
|
87
|
+
let candidate = ALIAS_PATTERN.test(base) ? base : `root-${base}`;
|
|
88
|
+
let i = 2;
|
|
89
|
+
while (existing.has(candidate)) {
|
|
90
|
+
candidate = `${base}-${i}`;
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
function assertNotDeniedPath(realPath) {
|
|
96
|
+
const names = realPath.split(path.sep).filter(Boolean);
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
if (DENIED_NAMES.has(name)) {
|
|
99
|
+
throw new WorkspaceRegistryError(`Workspace path targets denied directory "${name}"`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function realExistingPath(p) {
|
|
104
|
+
if (!path.isAbsolute(p)) {
|
|
105
|
+
throw new WorkspaceRegistryError(`Workspace path must be absolute: ${p}`);
|
|
106
|
+
}
|
|
107
|
+
if (!existsSync(p)) {
|
|
108
|
+
throw new WorkspaceRegistryError(`Workspace path does not exist: ${p}`);
|
|
109
|
+
}
|
|
110
|
+
const real = realpathSync(p);
|
|
111
|
+
assertNotDeniedPath(real);
|
|
112
|
+
return real;
|
|
113
|
+
}
|
|
114
|
+
function isGitRepo(p) {
|
|
115
|
+
return existsSync(path.join(p, ".git"));
|
|
116
|
+
}
|
|
117
|
+
function isUnder(parent, child) {
|
|
118
|
+
const relative = path.relative(parent, child);
|
|
119
|
+
return (relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative)));
|
|
120
|
+
}
|
|
121
|
+
function normalizeSlug(slug, maxDepth) {
|
|
122
|
+
if (path.isAbsolute(slug)) {
|
|
123
|
+
throw new WorkspaceRegistryError("Workspace slug must be relative");
|
|
124
|
+
}
|
|
125
|
+
const normalized = path.normalize(slug).replace(/\\/g, "/");
|
|
126
|
+
if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
127
|
+
throw new WorkspaceRegistryError("Workspace slug must not traverse outside the allowed root");
|
|
128
|
+
}
|
|
129
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
130
|
+
if (segments.length === 0 || segments.length > maxDepth) {
|
|
131
|
+
throw new WorkspaceRegistryError(`Workspace slug must contain 1-${maxDepth} path segment(s)`);
|
|
132
|
+
}
|
|
133
|
+
for (const segment of segments) {
|
|
134
|
+
if (segment === "." ||
|
|
135
|
+
segment === ".." ||
|
|
136
|
+
segment.startsWith(".") ||
|
|
137
|
+
segment.includes("..") ||
|
|
138
|
+
!SAFE_SEGMENT_PATTERN.test(segment) ||
|
|
139
|
+
DENIED_NAMES.has(segment)) {
|
|
140
|
+
throw new WorkspaceRegistryError(`Workspace slug segment "${segment}" is not allowed`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return segments;
|
|
144
|
+
}
|
|
145
|
+
export function loadWorkspaceRegistry(logger = noopLogger, configPath = defaultGatewayConfigPath()) {
|
|
146
|
+
const sourcePath = existsSync(configPath) ? configPath : null;
|
|
147
|
+
let parsed;
|
|
148
|
+
try {
|
|
149
|
+
parsed = readToml(configPath);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
logWarn(logger, "Invalid gateway config; workspace registry disabled", {
|
|
153
|
+
error: err instanceof Error ? err.message : String(err),
|
|
154
|
+
});
|
|
155
|
+
return disabledRegistry(sourcePath);
|
|
156
|
+
}
|
|
157
|
+
const raw = parsed.workspaces ?? {};
|
|
158
|
+
const result = WorkspacesSchema.safeParse(raw);
|
|
159
|
+
if (!result.success) {
|
|
160
|
+
logWarn(logger, "Invalid [workspaces] config; workspace registry disabled", {
|
|
161
|
+
error: result.error.message,
|
|
162
|
+
});
|
|
163
|
+
return disabledRegistry(sourcePath);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const rootAliases = new Set();
|
|
167
|
+
const allowedRoots = result.data.allowed_roots.map(root => {
|
|
168
|
+
const real = realExistingPath(expandHome(root.path));
|
|
169
|
+
const alias = validateWorkspaceAlias(root.alias ?? defaultRootAlias(real, rootAliases));
|
|
170
|
+
rootAliases.add(alias);
|
|
171
|
+
return {
|
|
172
|
+
alias,
|
|
173
|
+
path: real,
|
|
174
|
+
allowRegisterExistingGitRepos: root.allow_register_existing_git_repos,
|
|
175
|
+
allowCreateDirectories: root.allow_create_directories,
|
|
176
|
+
allowInitGitRepos: root.allow_init_git_repos,
|
|
177
|
+
maxCreateDepth: root.max_create_depth,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
const aliases = new Set();
|
|
181
|
+
const repos = result.data.repos.map(repo => {
|
|
182
|
+
const alias = validateWorkspaceAlias(repo.alias);
|
|
183
|
+
if (aliases.has(alias))
|
|
184
|
+
throw new WorkspaceRegistryError(`Duplicate workspace alias "${alias}"`);
|
|
185
|
+
aliases.add(alias);
|
|
186
|
+
const real = realExistingPath(expandHome(repo.path));
|
|
187
|
+
if (repo.kind === "git" && !isGitRepo(real)) {
|
|
188
|
+
throw new WorkspaceRegistryError(`Workspace "${alias}" is not a Git repository`);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
alias,
|
|
192
|
+
path: real,
|
|
193
|
+
providers: repo.providers,
|
|
194
|
+
allowWorktree: repo.allow_worktree,
|
|
195
|
+
allowAddDir: repo.allow_add_dir,
|
|
196
|
+
kind: repo.kind,
|
|
197
|
+
operatorEntry: repo.operator_entry,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
if (result.data.default && !repos.some(repo => repo.alias === result.data.default)) {
|
|
201
|
+
throw new WorkspaceRegistryError(`[workspaces].default references unknown alias "${result.data.default}"`);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
enabled: repos.length > 0 || allowedRoots.length > 0,
|
|
205
|
+
defaultAlias: result.data.default ?? null,
|
|
206
|
+
allowUnregisteredWorkingDir: result.data.allow_unregistered_working_dir,
|
|
207
|
+
repos,
|
|
208
|
+
allowedRoots,
|
|
209
|
+
sources: { configFile: sourcePath },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
logWarn(logger, "Invalid [workspaces] config; workspace registry disabled", {
|
|
214
|
+
error: err instanceof Error ? err.message : String(err),
|
|
215
|
+
});
|
|
216
|
+
return disabledRegistry(sourcePath);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function disabledRegistry(sourcePath) {
|
|
220
|
+
return {
|
|
221
|
+
enabled: false,
|
|
222
|
+
defaultAlias: null,
|
|
223
|
+
allowUnregisteredWorkingDir: false,
|
|
224
|
+
repos: [],
|
|
225
|
+
allowedRoots: [],
|
|
226
|
+
sources: { configFile: sourcePath },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function getWorkspace(registry, alias) {
|
|
230
|
+
validateWorkspaceAlias(alias);
|
|
231
|
+
const repo = registry.repos.find(candidate => candidate.alias === alias);
|
|
232
|
+
if (!repo)
|
|
233
|
+
throw new WorkspaceRegistryError(`Unknown workspace alias "${alias}"`);
|
|
234
|
+
return repo;
|
|
235
|
+
}
|
|
236
|
+
export function resolveWorkspaceForProvider(registry, provider, requestedAlias, sessionMetadata) {
|
|
237
|
+
const sessionAlias = typeof sessionMetadata?.workspaceAlias === "string"
|
|
238
|
+
? sessionMetadata.workspaceAlias
|
|
239
|
+
: undefined;
|
|
240
|
+
const alias = requestedAlias ?? sessionAlias ?? registry.defaultAlias;
|
|
241
|
+
if (!alias) {
|
|
242
|
+
throw new WorkspaceRegistryError("No workspace selected. Configure [workspaces].default or pass a registered workspace alias.");
|
|
243
|
+
}
|
|
244
|
+
const repo = getWorkspace(registry, alias);
|
|
245
|
+
if (!repo.providers.includes(provider)) {
|
|
246
|
+
throw new WorkspaceRegistryError(`Workspace "${alias}" does not allow provider "${provider}"`);
|
|
247
|
+
}
|
|
248
|
+
return { alias: repo.alias, root: repo.path, cwd: repo.path, repo };
|
|
249
|
+
}
|
|
250
|
+
export function validatePathInsideWorkspace(workspace, candidate, policy) {
|
|
251
|
+
if (path.isAbsolute(candidate) && policy === "workingDir") {
|
|
252
|
+
throw new WorkspaceRegistryError("Absolute workingDir is not allowed for remote workspaces");
|
|
253
|
+
}
|
|
254
|
+
if (path.isAbsolute(candidate) && policy === "addDir" && !workspace.repo.allowAddDir) {
|
|
255
|
+
throw new WorkspaceRegistryError("Absolute addDir is not allowed for this workspace");
|
|
256
|
+
}
|
|
257
|
+
const resolved = path.isAbsolute(candidate)
|
|
258
|
+
? realExistingPath(candidate)
|
|
259
|
+
: realExistingPath(path.join(workspace.root, candidate));
|
|
260
|
+
if (!isUnder(workspace.root, resolved)) {
|
|
261
|
+
throw new WorkspaceRegistryError(`${policy} must stay inside workspace "${workspace.alias}"`);
|
|
262
|
+
}
|
|
263
|
+
return resolved;
|
|
264
|
+
}
|
|
265
|
+
export function createWorkspace(input) {
|
|
266
|
+
const logger = input.logger ?? noopLogger;
|
|
267
|
+
if (input.kind !== "folder" && input.kind !== "git") {
|
|
268
|
+
throw new WorkspaceRegistryError("Workspace kind must be folder or git");
|
|
269
|
+
}
|
|
270
|
+
const configPath = input.configPath ?? defaultGatewayConfigPath();
|
|
271
|
+
const raw = readToml(configPath);
|
|
272
|
+
const registry = loadWorkspaceRegistry(logger, configPath);
|
|
273
|
+
const alias = validateWorkspaceAlias(input.alias);
|
|
274
|
+
if (registry.repos.some(repo => repo.alias === alias)) {
|
|
275
|
+
throw new WorkspaceRegistryError(`Workspace alias "${alias}" already exists`);
|
|
276
|
+
}
|
|
277
|
+
const root = registry.allowedRoots.find(candidate => candidate.alias === input.rootAlias);
|
|
278
|
+
if (!root)
|
|
279
|
+
throw new WorkspaceRegistryError(`Unknown allowed root "${input.rootAlias}"`);
|
|
280
|
+
if (!root.allowCreateDirectories) {
|
|
281
|
+
throw new WorkspaceRegistryError(`Allowed root "${input.rootAlias}" does not permit creation`);
|
|
282
|
+
}
|
|
283
|
+
if (input.kind === "git" && !root.allowInitGitRepos) {
|
|
284
|
+
throw new WorkspaceRegistryError(`Allowed root "${input.rootAlias}" does not permit git init`);
|
|
285
|
+
}
|
|
286
|
+
const segments = normalizeSlug(input.slug, root.maxCreateDepth);
|
|
287
|
+
const target = path.resolve(root.path, ...segments);
|
|
288
|
+
if (!isUnder(root.path, target)) {
|
|
289
|
+
throw new WorkspaceRegistryError("Workspace target escapes the allowed root");
|
|
290
|
+
}
|
|
291
|
+
const parent = path.dirname(target);
|
|
292
|
+
const parentReal = realExistingPath(parent);
|
|
293
|
+
if (!isUnder(root.path, parentReal)) {
|
|
294
|
+
throw new WorkspaceRegistryError("Workspace parent escapes the allowed root");
|
|
295
|
+
}
|
|
296
|
+
if (existsSync(target)) {
|
|
297
|
+
const stat = statSync(target);
|
|
298
|
+
if (!stat.isDirectory()) {
|
|
299
|
+
throw new WorkspaceRegistryError("Workspace target exists and is not a directory");
|
|
300
|
+
}
|
|
301
|
+
if (readdirSync(target).length > 0) {
|
|
302
|
+
throw new WorkspaceRegistryError("Workspace target exists and is not empty");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
306
|
+
const targetReal = realpathSync(target);
|
|
307
|
+
if (!isUnder(root.path, targetReal)) {
|
|
308
|
+
rmSync(target, { recursive: true, force: true });
|
|
309
|
+
throw new WorkspaceRegistryError("Workspace target escapes the allowed root");
|
|
310
|
+
}
|
|
311
|
+
assertNotDeniedPath(targetReal);
|
|
312
|
+
if (input.kind === "git") {
|
|
313
|
+
const init = spawnSync("git", ["init"], { cwd: targetReal, encoding: "utf8" });
|
|
314
|
+
if (init.status !== 0) {
|
|
315
|
+
throw new WorkspaceRegistryError(`git init failed: ${init.stderr || init.stdout}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const workspaces = (raw.workspaces ?? {});
|
|
319
|
+
const repos = (Array.isArray(workspaces.repos) ? workspaces.repos : []);
|
|
320
|
+
repos.push({
|
|
321
|
+
alias,
|
|
322
|
+
path: targetReal,
|
|
323
|
+
providers: [...CLI_TYPES],
|
|
324
|
+
allow_worktree: input.kind === "git",
|
|
325
|
+
allow_add_dir: false,
|
|
326
|
+
kind: input.kind,
|
|
327
|
+
operator_entry: false,
|
|
328
|
+
});
|
|
329
|
+
workspaces.repos = repos;
|
|
330
|
+
if (input.setDefault)
|
|
331
|
+
workspaces.default = alias;
|
|
332
|
+
raw.workspaces = workspaces;
|
|
333
|
+
writeToml(configPath, raw);
|
|
334
|
+
return {
|
|
335
|
+
alias,
|
|
336
|
+
path: targetReal,
|
|
337
|
+
providers: [...CLI_TYPES],
|
|
338
|
+
allowWorktree: input.kind === "git",
|
|
339
|
+
allowAddDir: false,
|
|
340
|
+
kind: input.kind,
|
|
341
|
+
operatorEntry: false,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export function registerExistingWorkspace(input) {
|
|
345
|
+
const logger = input.logger ?? noopLogger;
|
|
346
|
+
const configPath = input.configPath ?? defaultGatewayConfigPath();
|
|
347
|
+
const raw = readToml(configPath);
|
|
348
|
+
const registry = loadWorkspaceRegistry(logger, configPath);
|
|
349
|
+
const alias = validateWorkspaceAlias(input.alias);
|
|
350
|
+
if (registry.repos.some(repo => repo.alias === alias)) {
|
|
351
|
+
throw new WorkspaceRegistryError(`Workspace alias "${alias}" already exists`);
|
|
352
|
+
}
|
|
353
|
+
const real = realExistingPath(input.repoPath);
|
|
354
|
+
if (!isGitRepo(real))
|
|
355
|
+
throw new WorkspaceRegistryError("Existing workspace must be a Git repo");
|
|
356
|
+
const root = registry.allowedRoots.find(candidate => isUnder(candidate.path, real));
|
|
357
|
+
if (!root?.allowRegisterExistingGitRepos) {
|
|
358
|
+
throw new WorkspaceRegistryError("No allowed root permits registering this Git repo");
|
|
359
|
+
}
|
|
360
|
+
const workspaces = (raw.workspaces ?? {});
|
|
361
|
+
const repos = (Array.isArray(workspaces.repos) ? workspaces.repos : []);
|
|
362
|
+
repos.push({
|
|
363
|
+
alias,
|
|
364
|
+
path: real,
|
|
365
|
+
providers: [...CLI_TYPES],
|
|
366
|
+
allow_worktree: true,
|
|
367
|
+
allow_add_dir: false,
|
|
368
|
+
kind: "git",
|
|
369
|
+
operator_entry: false,
|
|
370
|
+
});
|
|
371
|
+
workspaces.repos = repos;
|
|
372
|
+
if (input.setDefault)
|
|
373
|
+
workspaces.default = alias;
|
|
374
|
+
raw.workspaces = workspaces;
|
|
375
|
+
writeToml(configPath, raw);
|
|
376
|
+
return {
|
|
377
|
+
alias,
|
|
378
|
+
path: real,
|
|
379
|
+
providers: [...CLI_TYPES],
|
|
380
|
+
allowWorktree: true,
|
|
381
|
+
allowAddDir: false,
|
|
382
|
+
kind: "git",
|
|
383
|
+
operatorEntry: false,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
export function createTempWorkspaceConfig(contents) {
|
|
387
|
+
const dir = mkdtempSync(path.join(tmpdir(), "workspace-registry-test-"));
|
|
388
|
+
const configPath = path.join(dir, "config.toml");
|
|
389
|
+
writeFileSync(configPath, contents, { mode: 0o600 });
|
|
390
|
+
return configPath;
|
|
391
|
+
}
|
|
392
|
+
export function describeWorkspace(repo) {
|
|
393
|
+
let branch = null;
|
|
394
|
+
let dirty = false;
|
|
395
|
+
if (repo.kind === "git") {
|
|
396
|
+
const branchResult = spawnSync("git", ["branch", "--show-current"], {
|
|
397
|
+
cwd: repo.path,
|
|
398
|
+
encoding: "utf8",
|
|
399
|
+
});
|
|
400
|
+
branch = branchResult.status === 0 ? branchResult.stdout.trim() || null : null;
|
|
401
|
+
const status = spawnSync("git", ["status", "--porcelain"], {
|
|
402
|
+
cwd: repo.path,
|
|
403
|
+
encoding: "utf8",
|
|
404
|
+
});
|
|
405
|
+
dirty = status.status === 0 && status.stdout.trim().length > 0;
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
alias: repo.alias,
|
|
409
|
+
path: repo.path,
|
|
410
|
+
kind: repo.kind,
|
|
411
|
+
providers: repo.providers,
|
|
412
|
+
allow_worktree: repo.allowWorktree,
|
|
413
|
+
allow_add_dir: repo.allowAddDir,
|
|
414
|
+
branch,
|
|
415
|
+
dirty,
|
|
416
|
+
};
|
|
417
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "llm-cli-gateway",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.6.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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",
|
package/setup/status.schema.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"gateway",
|
|
12
12
|
"transport",
|
|
13
13
|
"auth",
|
|
14
|
+
"workspaces",
|
|
14
15
|
"providers",
|
|
15
16
|
"endpoint_exposure",
|
|
16
17
|
"client_config",
|
|
@@ -70,7 +71,47 @@
|
|
|
70
71
|
"properties": {
|
|
71
72
|
"required": { "type": "boolean" },
|
|
72
73
|
"token_configured": { "type": "boolean" },
|
|
73
|
-
"source": { "enum": ["env", "disabled"] }
|
|
74
|
+
"source": { "enum": ["env", "disabled", "installer-auth-token-file"] },
|
|
75
|
+
"oauth": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": [
|
|
78
|
+
"enabled",
|
|
79
|
+
"registration_policy",
|
|
80
|
+
"clients_configured",
|
|
81
|
+
"shared_secret_enabled",
|
|
82
|
+
"pkce_required",
|
|
83
|
+
"issuer"
|
|
84
|
+
],
|
|
85
|
+
"properties": {
|
|
86
|
+
"enabled": { "type": "boolean" },
|
|
87
|
+
"registration_policy": {
|
|
88
|
+
"enum": ["static_clients", "shared_secret", "open_dev"]
|
|
89
|
+
},
|
|
90
|
+
"clients_configured": { "type": "integer", "minimum": 0 },
|
|
91
|
+
"shared_secret_enabled": { "type": "boolean" },
|
|
92
|
+
"pkce_required": { "type": "boolean" },
|
|
93
|
+
"issuer": { "type": ["string", "null"] }
|
|
94
|
+
},
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": false
|
|
99
|
+
},
|
|
100
|
+
"workspaces": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"required": [
|
|
103
|
+
"enabled",
|
|
104
|
+
"default",
|
|
105
|
+
"repo_count",
|
|
106
|
+
"allowed_root_count",
|
|
107
|
+
"gateway_app_dir_is_workspace"
|
|
108
|
+
],
|
|
109
|
+
"properties": {
|
|
110
|
+
"enabled": { "type": "boolean" },
|
|
111
|
+
"default": { "type": ["string", "null"] },
|
|
112
|
+
"repo_count": { "type": "integer", "minimum": 0 },
|
|
113
|
+
"allowed_root_count": { "type": "integer", "minimum": 0 },
|
|
114
|
+
"gateway_app_dir_is_workspace": { "type": "boolean" }
|
|
74
115
|
},
|
|
75
116
|
"additionalProperties": false
|
|
76
117
|
},
|