@tronsfey/ucli 0.4.1 → 0.4.3
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/dist/{chunk-NNFC34A5.js → chunk-FJU3QOHW.js} +3 -1
- package/dist/{client-O6QINOP2.js → client-3I7XBDJU.js} +4 -2
- package/dist/{client-O6QINOP2.js.map → client-3I7XBDJU.js.map} +1 -1
- package/dist/index.js +293 -44
- package/dist/index.js.map +1 -1
- package/dist/{runner-VTUGJUH7.js → runner-GVYIJNHN.js} +4 -2
- package/dist/{runner-VTUGJUH7.js.map → runner-GVYIJNHN.js.map} +1 -1
- package/package.json +16 -6
- package/skill.md +17 -0
- /package/dist/{chunk-NNFC34A5.js.map → chunk-FJU3QOHW.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
2
|
+
import { createRequire as __createRequire } from "module";
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
import "./chunk-FJU3QOHW.js";
|
|
3
5
|
|
|
4
6
|
// src/index.ts
|
|
5
7
|
import { Command } from "commander";
|
|
@@ -8,7 +10,61 @@ import { createRequire as createRequire2 } from "module";
|
|
|
8
10
|
// src/config.ts
|
|
9
11
|
import Conf from "conf";
|
|
10
12
|
import { homedir } from "os";
|
|
11
|
-
import { join } from "path";
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
|
+
import { chmodSync, mkdirSync } from "fs";
|
|
15
|
+
|
|
16
|
+
// src/lib/config-encryption.ts
|
|
17
|
+
import {
|
|
18
|
+
createCipheriv,
|
|
19
|
+
createDecipheriv,
|
|
20
|
+
randomBytes,
|
|
21
|
+
pbkdf2Sync
|
|
22
|
+
} from "crypto";
|
|
23
|
+
import { hostname, userInfo } from "os";
|
|
24
|
+
var ENC_PREFIX = "enc:v1:";
|
|
25
|
+
var ALGORITHM = "aes-256-gcm";
|
|
26
|
+
var SALT_LEN = 32;
|
|
27
|
+
var IV_LEN = 12;
|
|
28
|
+
var TAG_LEN = 16;
|
|
29
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
30
|
+
function deriveKey(salt) {
|
|
31
|
+
let user = "default";
|
|
32
|
+
try {
|
|
33
|
+
user = userInfo().username;
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
const material = `ucli:${user}@${hostname()}`;
|
|
37
|
+
return pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha512");
|
|
38
|
+
}
|
|
39
|
+
function encryptValue(plaintext) {
|
|
40
|
+
const salt = randomBytes(SALT_LEN);
|
|
41
|
+
const key = deriveKey(salt);
|
|
42
|
+
const iv = randomBytes(IV_LEN);
|
|
43
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
44
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
45
|
+
const tag = cipher.getAuthTag();
|
|
46
|
+
const packed = Buffer.concat([salt, iv, tag, encrypted]);
|
|
47
|
+
return ENC_PREFIX + packed.toString("base64");
|
|
48
|
+
}
|
|
49
|
+
function decryptValue(stored) {
|
|
50
|
+
if (!stored.startsWith(ENC_PREFIX)) {
|
|
51
|
+
return stored;
|
|
52
|
+
}
|
|
53
|
+
const packed = Buffer.from(stored.slice(ENC_PREFIX.length), "base64");
|
|
54
|
+
const salt = packed.subarray(0, SALT_LEN);
|
|
55
|
+
const iv = packed.subarray(SALT_LEN, SALT_LEN + IV_LEN);
|
|
56
|
+
const tag = packed.subarray(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN);
|
|
57
|
+
const encrypted = packed.subarray(SALT_LEN + IV_LEN + TAG_LEN);
|
|
58
|
+
const key = deriveKey(salt);
|
|
59
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
60
|
+
decipher.setAuthTag(tag);
|
|
61
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
|
62
|
+
}
|
|
63
|
+
function isEncrypted(value) {
|
|
64
|
+
return value.startsWith(ENC_PREFIX);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/config.ts
|
|
12
68
|
var conf = new Conf({
|
|
13
69
|
projectName: "ucli",
|
|
14
70
|
schema: {
|
|
@@ -17,18 +73,35 @@ var conf = new Conf({
|
|
|
17
73
|
}
|
|
18
74
|
});
|
|
19
75
|
var cacheDir = join(homedir(), ".cache", "ucli");
|
|
76
|
+
function hardenConfigPermissions() {
|
|
77
|
+
try {
|
|
78
|
+
const configPath = conf.path;
|
|
79
|
+
const configDir = dirname(configPath);
|
|
80
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
81
|
+
chmodSync(configDir, 448);
|
|
82
|
+
chmodSync(configPath, 384);
|
|
83
|
+
} catch {
|
|
84
|
+
console.warn("Warning: Could not enforce restrictive file permissions on config. Token is encrypted but file permissions may be permissive.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
20
87
|
function getConfig() {
|
|
21
88
|
const serverUrl = conf.get("serverUrl");
|
|
22
|
-
const
|
|
23
|
-
if (!serverUrl || !
|
|
89
|
+
const rawToken = conf.get("token");
|
|
90
|
+
if (!serverUrl || !rawToken) {
|
|
24
91
|
console.error("ucli is not configured. Run: ucli configure --server <url> --token <jwt>");
|
|
25
92
|
process.exit(1);
|
|
26
93
|
}
|
|
94
|
+
const token = decryptValue(rawToken);
|
|
95
|
+
if (!isEncrypted(rawToken)) {
|
|
96
|
+
conf.set("token", encryptValue(token));
|
|
97
|
+
hardenConfigPermissions();
|
|
98
|
+
}
|
|
27
99
|
return { serverUrl, token };
|
|
28
100
|
}
|
|
29
101
|
function saveConfig(cfg) {
|
|
30
102
|
conf.set("serverUrl", cfg.serverUrl);
|
|
31
|
-
conf.set("token", cfg.token);
|
|
103
|
+
conf.set("token", encryptValue(cfg.token));
|
|
104
|
+
hardenConfigPermissions();
|
|
32
105
|
}
|
|
33
106
|
function isConfigured() {
|
|
34
107
|
return Boolean(conf.get("serverUrl") && conf.get("token"));
|
|
@@ -103,11 +176,17 @@ function registerConfigure(program2) {
|
|
|
103
176
|
}
|
|
104
177
|
|
|
105
178
|
// src/lib/cache.ts
|
|
106
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
179
|
+
import { readFile, writeFile, mkdir, unlink, chmod } from "fs/promises";
|
|
107
180
|
import { join as join2 } from "path";
|
|
108
181
|
var LIST_CACHE_FILE = join2(cacheDir, "oas-list.json");
|
|
109
182
|
async function ensureCacheDir() {
|
|
110
|
-
await mkdir(cacheDir, { recursive: true });
|
|
183
|
+
await mkdir(cacheDir, { recursive: true, mode: 448 });
|
|
184
|
+
}
|
|
185
|
+
function redactEntries(entries) {
|
|
186
|
+
return entries.map((e) => ({
|
|
187
|
+
...e,
|
|
188
|
+
authConfig: { type: e.authConfig["type"] ?? e.authType }
|
|
189
|
+
}));
|
|
111
190
|
}
|
|
112
191
|
async function readOASListCache() {
|
|
113
192
|
try {
|
|
@@ -122,25 +201,45 @@ async function readOASListCache() {
|
|
|
122
201
|
}
|
|
123
202
|
async function writeOASListCache(entries, ttlSec) {
|
|
124
203
|
await ensureCacheDir();
|
|
125
|
-
const cached = { entries, fetchedAt: Date.now(), ttlSec };
|
|
126
|
-
await writeFile(LIST_CACHE_FILE, JSON.stringify(cached, null, 2), "utf8");
|
|
204
|
+
const cached = { entries: redactEntries(entries), fetchedAt: Date.now(), ttlSec };
|
|
205
|
+
await writeFile(LIST_CACHE_FILE, JSON.stringify(cached, null, 2), { encoding: "utf8", mode: 384 });
|
|
206
|
+
await chmod(LIST_CACHE_FILE, 384);
|
|
127
207
|
}
|
|
128
208
|
async function clearOASListCache() {
|
|
129
209
|
try {
|
|
130
|
-
await
|
|
210
|
+
await unlink(LIST_CACHE_FILE);
|
|
131
211
|
} catch {
|
|
132
212
|
}
|
|
133
213
|
}
|
|
214
|
+
async function clearOASCache(name) {
|
|
215
|
+
try {
|
|
216
|
+
const raw = await readFile(LIST_CACHE_FILE, "utf8");
|
|
217
|
+
const cached = JSON.parse(raw);
|
|
218
|
+
const entries = Array.isArray(cached.entries) ? cached.entries.filter((e) => e.name !== name) : [];
|
|
219
|
+
if (entries.length === 0) {
|
|
220
|
+
await clearOASListCache();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const next = {
|
|
224
|
+
entries,
|
|
225
|
+
fetchedAt: cached.fetchedAt ?? Date.now(),
|
|
226
|
+
ttlSec: cached.ttlSec ?? 0
|
|
227
|
+
};
|
|
228
|
+
await writeFile(LIST_CACHE_FILE, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 384 });
|
|
229
|
+
} catch {
|
|
230
|
+
await clearOASListCache();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
134
233
|
|
|
135
234
|
// src/lib/oas-runner.ts
|
|
136
235
|
import { spawn } from "child_process";
|
|
137
236
|
import { createRequire } from "module";
|
|
138
|
-
import { join as join3, dirname } from "path";
|
|
237
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
139
238
|
var require2 = createRequire(import.meta.url);
|
|
140
239
|
function resolveOpenapi2CliBin() {
|
|
141
240
|
try {
|
|
142
241
|
const pkgPath = require2.resolve("@tronsfey/openapi2cli/package.json");
|
|
143
|
-
const pkgDir =
|
|
242
|
+
const pkgDir = dirname2(pkgPath);
|
|
144
243
|
const pkg2 = require2("@tronsfey/openapi2cli/package.json");
|
|
145
244
|
const binEntry = pkg2.bin?.["openapi2cli"] ?? "bin/openapi2cli.js";
|
|
146
245
|
return join3(pkgDir, binEntry);
|
|
@@ -170,6 +269,58 @@ function buildAuthEnv(entry) {
|
|
|
170
269
|
return {};
|
|
171
270
|
}
|
|
172
271
|
}
|
|
272
|
+
var SAFE_ENV_KEYS = [
|
|
273
|
+
// System essentials
|
|
274
|
+
"PATH",
|
|
275
|
+
"HOME",
|
|
276
|
+
"USER",
|
|
277
|
+
"LOGNAME",
|
|
278
|
+
"SHELL",
|
|
279
|
+
"TMPDIR",
|
|
280
|
+
"TMP",
|
|
281
|
+
"TEMP",
|
|
282
|
+
// Terminal / display
|
|
283
|
+
"TERM",
|
|
284
|
+
"COLORTERM",
|
|
285
|
+
"NO_COLOR",
|
|
286
|
+
"FORCE_COLOR",
|
|
287
|
+
"LANG",
|
|
288
|
+
"LC_ALL",
|
|
289
|
+
"LC_CTYPE",
|
|
290
|
+
"LC_MESSAGES",
|
|
291
|
+
"LC_COLLATE",
|
|
292
|
+
// Node.js
|
|
293
|
+
"NODE_ENV",
|
|
294
|
+
"NODE_PATH",
|
|
295
|
+
"NODE_OPTIONS",
|
|
296
|
+
"NODE_EXTRA_CA_CERTS",
|
|
297
|
+
// Network proxy (required for tools behind corporate proxies)
|
|
298
|
+
"HTTP_PROXY",
|
|
299
|
+
"HTTPS_PROXY",
|
|
300
|
+
"NO_PROXY",
|
|
301
|
+
"http_proxy",
|
|
302
|
+
"https_proxy",
|
|
303
|
+
"no_proxy",
|
|
304
|
+
// OS-specific
|
|
305
|
+
"SYSTEMROOT",
|
|
306
|
+
"COMSPEC",
|
|
307
|
+
"APPDATA",
|
|
308
|
+
"LOCALAPPDATA",
|
|
309
|
+
"PROGRAMFILES",
|
|
310
|
+
"XDG_RUNTIME_DIR",
|
|
311
|
+
"XDG_CONFIG_HOME",
|
|
312
|
+
"XDG_CACHE_HOME",
|
|
313
|
+
"XDG_DATA_HOME"
|
|
314
|
+
];
|
|
315
|
+
function buildSafeEnv(authEnv) {
|
|
316
|
+
const safe = {};
|
|
317
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
318
|
+
if (process.env[key] !== void 0) {
|
|
319
|
+
safe[key] = process.env[key];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { ...safe, ...authEnv };
|
|
323
|
+
}
|
|
173
324
|
async function runOperation(opts) {
|
|
174
325
|
const bin = resolveOpenapi2CliBin();
|
|
175
326
|
const { entry, operationArgs, format, query } = opts;
|
|
@@ -185,17 +336,13 @@ async function runOperation(opts) {
|
|
|
185
336
|
...operationArgs
|
|
186
337
|
];
|
|
187
338
|
const authEnv = buildAuthEnv(entry);
|
|
188
|
-
await new Promise((
|
|
339
|
+
await new Promise((resolve2, reject) => {
|
|
189
340
|
const child = spawn(process.execPath, [bin, ...args], {
|
|
190
341
|
stdio: "inherit",
|
|
191
|
-
env:
|
|
192
|
-
...process.env,
|
|
193
|
-
...authEnv
|
|
194
|
-
// inject auth — not visible to parent shell
|
|
195
|
-
}
|
|
342
|
+
env: buildSafeEnv(authEnv)
|
|
196
343
|
});
|
|
197
344
|
child.on("close", (code) => {
|
|
198
|
-
if (code === 0)
|
|
345
|
+
if (code === 0) resolve2();
|
|
199
346
|
else reject(new Error(`openapi2cli exited with code ${code}`));
|
|
200
347
|
});
|
|
201
348
|
child.on("error", reject);
|
|
@@ -211,7 +358,7 @@ async function getServiceHelp(entry) {
|
|
|
211
358
|
String(entry.cacheTtl),
|
|
212
359
|
"--help"
|
|
213
360
|
];
|
|
214
|
-
return new Promise((
|
|
361
|
+
return new Promise((resolve2, reject) => {
|
|
215
362
|
let output = "";
|
|
216
363
|
const child = spawn(process.execPath, [bin, ...args], {
|
|
217
364
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -222,7 +369,7 @@ async function getServiceHelp(entry) {
|
|
|
222
369
|
child.stderr?.on("data", (d) => {
|
|
223
370
|
output += d.toString();
|
|
224
371
|
});
|
|
225
|
-
child.on("close", () =>
|
|
372
|
+
child.on("close", () => resolve2(output));
|
|
226
373
|
child.on("error", reject);
|
|
227
374
|
});
|
|
228
375
|
}
|
|
@@ -230,10 +377,14 @@ async function getServiceHelp(entry) {
|
|
|
230
377
|
// src/commands/services.ts
|
|
231
378
|
function registerServices(program2) {
|
|
232
379
|
const services = program2.command("services").description("Manage and inspect available OAS services");
|
|
233
|
-
services.command("list").description("List all OAS services available in the current group").option("--no-cache", "Bypass local cache and fetch fresh from server").action(async (opts) => {
|
|
380
|
+
services.command("list").description("List all OAS services available in the current group").option("--refresh", "Bypass local cache and fetch fresh from server").option("--format <fmt>", "Output format: table | json | yaml", "table").option("--no-cache", "Bypass local cache and fetch fresh from server").action(async (opts) => {
|
|
234
381
|
const cfg = getConfig();
|
|
235
382
|
const client = new ServerClient(cfg);
|
|
236
|
-
|
|
383
|
+
if (!opts.cache) {
|
|
384
|
+
process.emitWarning("The --no-cache flag is deprecated. Please use --refresh instead.");
|
|
385
|
+
}
|
|
386
|
+
const useCache = opts.cache && !opts.refresh;
|
|
387
|
+
let entries = useCache ? await readOASListCache() : null;
|
|
237
388
|
if (!entries) {
|
|
238
389
|
entries = await client.listOAS();
|
|
239
390
|
if (entries.length > 0) {
|
|
@@ -241,10 +392,19 @@ function registerServices(program2) {
|
|
|
241
392
|
await writeOASListCache(entries, maxTtl);
|
|
242
393
|
}
|
|
243
394
|
}
|
|
395
|
+
const format = (opts.format ?? "table").toLowerCase();
|
|
244
396
|
if (entries.length === 0) {
|
|
245
397
|
console.log("No services registered in this group.");
|
|
246
398
|
return;
|
|
247
399
|
}
|
|
400
|
+
if (format === "json") {
|
|
401
|
+
const safe = entries.map(({ authConfig, ...rest }) => ({
|
|
402
|
+
...rest,
|
|
403
|
+
authConfig: { type: authConfig["type"] ?? rest.authType }
|
|
404
|
+
}));
|
|
405
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
248
408
|
const nameWidth = Math.max(10, ...entries.map((e) => e.name.length));
|
|
249
409
|
console.log(`
|
|
250
410
|
${"SERVICE".padEnd(nameWidth)} AUTH DESCRIPTION`);
|
|
@@ -256,7 +416,7 @@ ${"SERVICE".padEnd(nameWidth)} AUTH DESCRIPTION`);
|
|
|
256
416
|
}
|
|
257
417
|
console.log();
|
|
258
418
|
});
|
|
259
|
-
services.command("info <name>").description("Show detailed information and available operations for a service").action(async (name) => {
|
|
419
|
+
services.command("info <name>").description("Show detailed information and available operations for a service").option("--format <fmt>", "Output format: table | json | yaml", "table").action(async (name, opts) => {
|
|
260
420
|
const cfg = getConfig();
|
|
261
421
|
const client = new ServerClient(cfg);
|
|
262
422
|
let entry;
|
|
@@ -267,6 +427,18 @@ ${"SERVICE".padEnd(nameWidth)} AUTH DESCRIPTION`);
|
|
|
267
427
|
console.error("Run `ucli services list` to see available services.");
|
|
268
428
|
process.exit(1);
|
|
269
429
|
}
|
|
430
|
+
const help = await getServiceHelp(entry);
|
|
431
|
+
const format = (opts.format ?? "table").toLowerCase();
|
|
432
|
+
if (format === "json") {
|
|
433
|
+
const { authConfig, ...rest } = entry;
|
|
434
|
+
const safe = {
|
|
435
|
+
...rest,
|
|
436
|
+
authConfig: { type: authConfig["type"] ?? rest.authType },
|
|
437
|
+
operationsHelp: help
|
|
438
|
+
};
|
|
439
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
270
442
|
console.log(`
|
|
271
443
|
Service: ${entry.name}`);
|
|
272
444
|
console.log(`Description: ${entry.description || "(none)"}`);
|
|
@@ -276,14 +448,22 @@ Service: ${entry.name}`);
|
|
|
276
448
|
console.log(`Cache TTL: ${entry.cacheTtl}s`);
|
|
277
449
|
console.log("\nAvailable operations:");
|
|
278
450
|
console.log("\u2500".repeat(60));
|
|
279
|
-
const help = await getServiceHelp(entry);
|
|
280
451
|
console.log(help);
|
|
281
452
|
});
|
|
282
453
|
}
|
|
283
454
|
|
|
284
455
|
// src/commands/run.ts
|
|
285
456
|
function registerRun(program2) {
|
|
286
|
-
program2.command("run
|
|
457
|
+
program2.command("run [service] [args...]").description("Execute an operation on a service").option("--service <name>", "Service name (from `services list`)").option("--operation <id>", "OperationId to execute").option("--params <json>", "JSON string of operation parameters").option("--format <fmt>", "Output format: json | table | yaml", "json").option("--query <jmespath>", "Filter response with JMESPath expression").option("--data <json>", "Request body (JSON string or @filename)").allowUnknownOption(true).action(async (serviceArg, args, opts) => {
|
|
458
|
+
if (serviceArg && opts.service && serviceArg !== opts.service) {
|
|
459
|
+
console.error(`Conflicting service values: positional "${serviceArg}" and --service "${opts.service}". Use either the positional argument or --service flag, not both.`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
const service = opts.service ?? serviceArg;
|
|
463
|
+
if (!service) {
|
|
464
|
+
console.error("Missing service name. Use positional <service> or --service <name>.");
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
287
467
|
const cfg = getConfig();
|
|
288
468
|
const client = new ServerClient(cfg);
|
|
289
469
|
let entry;
|
|
@@ -295,11 +475,29 @@ function registerRun(program2) {
|
|
|
295
475
|
process.exit(1);
|
|
296
476
|
}
|
|
297
477
|
const extraArgs = opts.args ?? [];
|
|
298
|
-
const operationArgs = [
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
478
|
+
const operationArgs = [...args, ...extraArgs];
|
|
479
|
+
if (opts.operation) {
|
|
480
|
+
operationArgs.unshift(opts.operation);
|
|
481
|
+
}
|
|
482
|
+
if (opts.params) {
|
|
483
|
+
let parsed;
|
|
484
|
+
try {
|
|
485
|
+
parsed = JSON.parse(opts.params);
|
|
486
|
+
} catch {
|
|
487
|
+
console.error(`Invalid --params JSON. Example: --params '{"petId": 1}'`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
491
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
492
|
+
if (v === void 0 || v === null) continue;
|
|
493
|
+
const strVal = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
494
|
+
operationArgs.push(`--${k}`, strVal);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (opts.data) {
|
|
499
|
+
operationArgs.push("--data", opts.data);
|
|
500
|
+
}
|
|
303
501
|
const format = opts.format;
|
|
304
502
|
const query = opts.query;
|
|
305
503
|
try {
|
|
@@ -318,11 +516,15 @@ function registerRun(program2) {
|
|
|
318
516
|
|
|
319
517
|
// src/commands/refresh.ts
|
|
320
518
|
function registerRefresh(program2) {
|
|
321
|
-
program2.command("refresh").description("Force-refresh the local OAS cache from the server").action(async () => {
|
|
519
|
+
program2.command("refresh").description("Force-refresh the local OAS cache from the server").option("--service <name>", "Refresh only a specific service").action(async (opts) => {
|
|
322
520
|
const cfg = getConfig();
|
|
323
521
|
const client = new ServerClient(cfg);
|
|
324
522
|
console.log("Refreshing OAS list from server...");
|
|
325
|
-
|
|
523
|
+
if (opts.service) {
|
|
524
|
+
await clearOASCache(opts.service);
|
|
525
|
+
} else {
|
|
526
|
+
await clearOASListCache();
|
|
527
|
+
}
|
|
326
528
|
const entries = await client.listOAS();
|
|
327
529
|
if (entries.length > 0) {
|
|
328
530
|
const maxTtl = Math.min(...entries.map((e) => e.cacheTtl));
|
|
@@ -427,10 +629,24 @@ ERRORS
|
|
|
427
629
|
}
|
|
428
630
|
|
|
429
631
|
// src/lib/mcp-runner.ts
|
|
632
|
+
function resolve(mod, name) {
|
|
633
|
+
if (typeof mod[name] === "function") return mod[name];
|
|
634
|
+
if (mod.default && typeof mod.default[name] === "function") return mod.default[name];
|
|
635
|
+
throw new Error(`Cannot resolve export "${name}" from module`);
|
|
636
|
+
}
|
|
430
637
|
async function getMcp2cli() {
|
|
431
|
-
const clientMod = await import("./client-
|
|
432
|
-
const runnerMod = await import("./runner-
|
|
433
|
-
return {
|
|
638
|
+
const clientMod = await import("./client-3I7XBDJU.js");
|
|
639
|
+
const runnerMod = await import("./runner-GVYIJNHN.js");
|
|
640
|
+
return {
|
|
641
|
+
createMcpClient: resolve(clientMod, "createMcpClient"),
|
|
642
|
+
getTools: resolve(runnerMod, "getTools"),
|
|
643
|
+
runTool: resolve(runnerMod, "runTool")
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function closeClient(client) {
|
|
647
|
+
if (typeof client.close === "function") {
|
|
648
|
+
await client.close();
|
|
649
|
+
}
|
|
434
650
|
}
|
|
435
651
|
function buildMcpConfig(entry) {
|
|
436
652
|
const base = { type: entry.transport };
|
|
@@ -451,23 +667,42 @@ async function listMcpTools(entry) {
|
|
|
451
667
|
const { createMcpClient, getTools } = await getMcp2cli();
|
|
452
668
|
const config = buildMcpConfig(entry);
|
|
453
669
|
const client = await createMcpClient(config);
|
|
454
|
-
|
|
455
|
-
|
|
670
|
+
try {
|
|
671
|
+
const tools = await getTools(client, config, { noCache: true });
|
|
672
|
+
return tools;
|
|
673
|
+
} finally {
|
|
674
|
+
await closeClient(client);
|
|
675
|
+
}
|
|
456
676
|
}
|
|
457
677
|
async function runMcpTool(entry, toolName, rawArgs) {
|
|
458
678
|
const { createMcpClient, getTools, runTool } = await getMcp2cli();
|
|
459
679
|
const config = buildMcpConfig(entry);
|
|
460
680
|
const client = await createMcpClient(config);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
681
|
+
try {
|
|
682
|
+
const tools = await getTools(client, config, { noCache: false, cacheTtl: 3600 });
|
|
683
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
684
|
+
if (!tool) throw new Error(`Tool "${toolName}" not found on MCP server "${entry.name}"`);
|
|
685
|
+
const normalizedArgs = [];
|
|
686
|
+
for (const arg of rawArgs) {
|
|
687
|
+
if (arg.includes("=") && !arg.startsWith("--")) {
|
|
688
|
+
const idx = arg.indexOf("=");
|
|
689
|
+
const key = arg.slice(0, idx);
|
|
690
|
+
const value = arg.slice(idx + 1);
|
|
691
|
+
normalizedArgs.push(`--${key}`, value);
|
|
692
|
+
} else {
|
|
693
|
+
normalizedArgs.push(arg);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
await runTool(client, tool, normalizedArgs, {});
|
|
697
|
+
} finally {
|
|
698
|
+
await closeClient(client);
|
|
699
|
+
}
|
|
465
700
|
}
|
|
466
701
|
|
|
467
702
|
// src/commands/mcp.ts
|
|
468
703
|
function registerMcp(program2) {
|
|
469
704
|
const mcp = program2.command("mcp").description("Interact with MCP servers registered in your group");
|
|
470
|
-
mcp.command("list").description("List all MCP servers available in the current group").action(async () => {
|
|
705
|
+
mcp.command("list").description("List all MCP servers available in the current group").option("--format <fmt>", "Output format: table | json | yaml", "table").action(async (opts) => {
|
|
471
706
|
const cfg = getConfig();
|
|
472
707
|
const client = new ServerClient(cfg);
|
|
473
708
|
const entries = await client.listMCP();
|
|
@@ -475,6 +710,15 @@ function registerMcp(program2) {
|
|
|
475
710
|
console.log("No MCP servers registered in this group.");
|
|
476
711
|
return;
|
|
477
712
|
}
|
|
713
|
+
const format = (opts.format ?? "table").toLowerCase();
|
|
714
|
+
if (format === "json") {
|
|
715
|
+
const safe = entries.map(({ authConfig, ...rest }) => ({
|
|
716
|
+
...rest,
|
|
717
|
+
authConfig: { type: authConfig.type }
|
|
718
|
+
}));
|
|
719
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
478
722
|
const nameWidth = Math.max(10, ...entries.map((e) => e.name.length));
|
|
479
723
|
console.log(`
|
|
480
724
|
${"SERVER".padEnd(nameWidth)} TRANSPORT DESCRIPTION`);
|
|
@@ -485,7 +729,7 @@ ${"SERVER".padEnd(nameWidth)} TRANSPORT DESCRIPTION`);
|
|
|
485
729
|
}
|
|
486
730
|
console.log();
|
|
487
731
|
});
|
|
488
|
-
mcp.command("tools <server>").description("List tools available on a MCP server").action(async (serverName) => {
|
|
732
|
+
mcp.command("tools <server>").description("List tools available on a MCP server").option("--format <fmt>", "Output format: table | json", "table").action(async (serverName, opts) => {
|
|
489
733
|
const cfg = getConfig();
|
|
490
734
|
const client = new ServerClient(cfg);
|
|
491
735
|
let entry;
|
|
@@ -507,6 +751,11 @@ ${"SERVER".padEnd(nameWidth)} TRANSPORT DESCRIPTION`);
|
|
|
507
751
|
console.log(`No tools found on MCP server "${serverName}".`);
|
|
508
752
|
return;
|
|
509
753
|
}
|
|
754
|
+
const format = (opts.format ?? "table").toLowerCase();
|
|
755
|
+
if (format === "json") {
|
|
756
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
510
759
|
console.log(`
|
|
511
760
|
Tools on "${serverName}":`);
|
|
512
761
|
console.log("\u2500".repeat(60));
|