@openbkn/bkn-sdk 0.1.1-alpha.0 → 0.1.1-alpha.10

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/cli.js CHANGED
@@ -1,34 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_BUSINESS_DOMAIN,
3
4
  DEFAULT_LIST_LIMIT,
4
5
  DEFAULT_QUERY_LIMIT,
6
+ HttpError,
5
7
  InputError,
6
8
  activePlatform,
9
+ attachNoAuth,
7
10
  attachToken,
8
- changePassword,
9
11
  createClient,
12
+ credentialDeviceLogin,
10
13
  currentToken,
14
+ currentTokenFresh,
15
+ decodeJwt,
11
16
  deletePlatform,
17
+ deviceLogin,
12
18
  exportCreds,
19
+ fetchAuthStatus,
13
20
  formatError,
21
+ getUserSafe,
22
+ isHeadless,
14
23
  listPlatforms,
15
24
  logout,
25
+ openBrowser,
16
26
  parseEmbeddingFields,
17
27
  parsePkMap,
18
28
  rawCall,
19
29
  readPlatformConfig,
20
- renderOrgTree,
21
30
  renderReportMarkdown,
31
+ request,
22
32
  resolveContext,
23
33
  setActivePlatform,
24
34
  status,
25
35
  switchUser,
26
36
  toExitCode,
27
37
  use,
28
- usersOf,
29
38
  whoami,
30
39
  writePlatformConfig
31
- } from "./chunk-DXA44XWY.js";
40
+ } from "./chunk-GNL6Z5VF.js";
32
41
 
33
42
  // src/cli.ts
34
43
  import { Command as Command16 } from "commander";
@@ -36,12 +45,12 @@ import { Command as Command16 } from "commander";
36
45
  // package.json
37
46
  var package_default = {
38
47
  name: "@openbkn/bkn-sdk",
39
- version: "0.1.1-alpha.0",
48
+ version: "0.1.1-alpha.10",
40
49
  description: "Unified TypeScript SDK + CLI for the BKN (Business Knowledge Network) platform.",
41
50
  type: "module",
42
51
  license: "Apache-2.0",
43
52
  engines: {
44
- node: ">=22"
53
+ node: ">=18"
45
54
  },
46
55
  bin: {
47
56
  openbkn: "./dist/cli.js"
@@ -76,8 +85,8 @@ var package_default = {
76
85
  },
77
86
  dependencies: {
78
87
  "@clack/prompts": "^0.9.1",
88
+ "@openbkn/bkn-sdk": "^0.1.1-alpha.3",
79
89
  chalk: "^5.4.1",
80
- "cli-table3": "^0.6.5",
81
90
  commander: "^13.1.0",
82
91
  "csv-parse": "^6.2.1",
83
92
  "js-yaml": "^4.2.0",
@@ -149,8 +158,20 @@ function installGroupedHelp(root) {
149
158
  apply(root);
150
159
  }
151
160
 
161
+ // src/utils/org-tree.ts
162
+ function renderOrgTree(nodes, prefix = "") {
163
+ const lines = [];
164
+ nodes.forEach((node, i) => {
165
+ const last = i === nodes.length - 1;
166
+ lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${node.name} (id: ${node.id})`);
167
+ if (node.children.length) {
168
+ lines.push(renderOrgTree(node.children, `${prefix}${last ? " " : "\u2502 "}`));
169
+ }
170
+ });
171
+ return lines.join("\n");
172
+ }
173
+
152
174
  // src/utils/output.ts
153
- import Table from "cli-table3";
154
175
  function printJson(value, opts = {}) {
155
176
  if (opts.json || opts.compact) {
156
177
  process.stdout.write(`${JSON.stringify(value, null, opts.compact ? 0 : 2)}
@@ -159,16 +180,43 @@ function printJson(value, opts = {}) {
159
180
  }
160
181
  const rows = toRows(value);
161
182
  if (rows) {
162
- const columns = columnsOf(rows);
183
+ const fullColumns = columnsOf(rows).filter((c) => rows.some((r) => stringifyCell(r[c]) !== ""));
184
+ const columns = opts.full ? fullColumns : selectColumns(rows);
163
185
  if (columns.length > 0) {
164
186
  printTable(rows, columns);
187
+ const hidden = fullColumns.length - columns.length;
188
+ if (hidden > 0 && !opts.full) {
189
+ process.stdout.write(`\u2026 ${hidden} more column(s); use --full or --json for everything
190
+ `);
191
+ }
165
192
  return;
166
193
  }
167
194
  }
195
+ if (isEmptyEnvelope(value)) {
196
+ process.stdout.write("(no results)\n");
197
+ return;
198
+ }
168
199
  process.stdout.write(`${JSON.stringify(value, null, 2)}
169
200
  `);
170
201
  }
171
- var ROW_ENVELOPES = ["entries", "data", "cases", "reports", "results", "list", "recurringRules"];
202
+ function isEmptyEnvelope(value) {
203
+ if (!value || typeof value !== "object") return false;
204
+ const o = value;
205
+ return ROW_ENVELOPES.some((k) => Array.isArray(o[k]) && o[k].length === 0);
206
+ }
207
+ var ROW_ENVELOPES = [
208
+ "entries",
209
+ "data",
210
+ "cases",
211
+ "reports",
212
+ "results",
213
+ "list",
214
+ "recurringRules",
215
+ "users",
216
+ "roles",
217
+ "departments",
218
+ "members"
219
+ ];
172
220
  function toRows(value) {
173
221
  const isRowArray = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
174
222
  if (isRowArray(value)) return value;
@@ -187,25 +235,90 @@ function columnsOf(rows) {
187
235
  }
188
236
  return seen;
189
237
  }
238
+ var MAX_COLS = 8;
239
+ var NOISE_COLS = /* @__PURE__ */ new Set([
240
+ "creator",
241
+ "updater",
242
+ "create_by",
243
+ "update_by",
244
+ "create_user",
245
+ "update_user",
246
+ "operations",
247
+ "status_message",
248
+ "last_check_time",
249
+ "last_discover_status",
250
+ "health_check_result",
251
+ "health_check_enabled"
252
+ ]);
253
+ var isNoiseCol = (c) => NOISE_COLS.has(c) || /_time$/.test(c);
254
+ var isKeyCol = (c) => /^(id|name|key|title|label)$/i.test(c) || /_(id|name|key)$/i.test(c) || /^(status|state|type|category|mode|enabled|version|branch)$/i.test(c);
255
+ function selectColumns(rows) {
256
+ const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
257
+ const kept = columnsOf(rows).filter((c) => {
258
+ if (isNoiseCol(c)) return false;
259
+ const vals = rows.map((r) => r[c]);
260
+ if (!vals.some((v) => stringifyCell(v) !== "")) return false;
261
+ if (vals.every((v) => v === null || v === void 0 || isObj(v))) return false;
262
+ return true;
263
+ });
264
+ const isLongText = (c) => rows.every((r) => {
265
+ const s = stringifyCell(r[c]);
266
+ return s === "" || s.length >= CELL_MAX - 1;
267
+ });
268
+ const rank = (c) => isKeyCol(c) ? 0 : isLongText(c) ? 2 : 1;
269
+ const ordered = kept.map((c, i) => ({ c, i, r: rank(c) })).sort((a, b) => a.r - b.r || a.i - b.i).map((x) => x.c);
270
+ return ordered.slice(0, MAX_COLS);
271
+ }
190
272
  function printTable(rows, columns, opts = {}) {
191
273
  if (opts.json || opts.compact) {
192
274
  printJson(rows, opts);
193
275
  return;
194
276
  }
195
- const table = new Table({ head: columns });
196
- for (const row of rows) {
197
- table.push(columns.map((c) => stringifyCell(row[c])));
198
- }
199
- process.stdout.write(`${table.toString()}
277
+ const cells = rows.map((row) => columns.map((c) => stringifyCell(row[c])));
278
+ const widths = columns.map(
279
+ (col, i) => Math.max(displayWidth(col), ...cells.map((r) => displayWidth(r[i] ?? "")))
280
+ );
281
+ const fmt = (parts) => parts.map((p, i) => i === parts.length - 1 ? p : pad(p, widths[i] ?? 0)).join(" ").trimEnd();
282
+ const lines = [fmt(columns), ...cells.map(fmt)];
283
+ process.stdout.write(`${lines.join("\n")}
200
284
  `);
201
285
  }
202
286
  var CELL_MAX = 48;
287
+ function displayWidth(s) {
288
+ let w = 0;
289
+ for (const ch of s) w += /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/.test(ch) ? 2 : 1;
290
+ return w;
291
+ }
292
+ function pad(s, width) {
293
+ const gap = width - displayWidth(s);
294
+ return gap > 0 ? s + " ".repeat(gap) : s;
295
+ }
203
296
  function stringifyCell(v) {
204
297
  if (v === null || v === void 0) return "";
205
- const s = (typeof v === "object" ? JSON.stringify(v) : String(v)).replace(/\s+/g, " ").trim();
298
+ const raw = Array.isArray(v) && v.every((x) => x === null || typeof x !== "object") ? v.join(",") : typeof v === "object" ? JSON.stringify(v) : String(v);
299
+ const s = raw.replace(/\s+/g, " ").trim();
206
300
  return s.length > CELL_MAX ? `${s.slice(0, CELL_MAX - 1)}\u2026` : s;
207
301
  }
208
302
 
303
+ // src/utils/prompt.ts
304
+ import { createInterface } from "readline";
305
+ function promptLine(query, hidden = false) {
306
+ return new Promise((resolve2) => {
307
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
308
+ if (hidden) {
309
+ const mutable = rl;
310
+ mutable._writeToOutput = (s) => {
311
+ if (s.startsWith(query)) process.stdout.write(query);
312
+ };
313
+ }
314
+ rl.question(query, (answer) => {
315
+ rl.close();
316
+ if (hidden) process.stdout.write("\n");
317
+ resolve2(answer.trim());
318
+ });
319
+ });
320
+ }
321
+
209
322
  // src/commands/_shared.ts
210
323
  import { readFileSync } from "fs";
211
324
  function clientFrom(cmd) {
@@ -220,7 +333,7 @@ function clientFrom(cmd) {
220
333
  }
221
334
  function outputOptions(cmd) {
222
335
  const o = cmd.optsWithGlobals();
223
- return { json: Boolean(o.json), compact: Boolean(o.compact) };
336
+ return { json: Boolean(o.json), compact: Boolean(o.compact), full: Boolean(o.full) };
224
337
  }
225
338
  function csv(value) {
226
339
  if (!value) return void 0;
@@ -237,350 +350,212 @@ function readBody(opts) {
237
350
  }
238
351
 
239
352
  // src/commands/auth.ts
240
- import { readFileSync as readFileSync2 } from "fs";
241
353
  import { Command } from "commander";
242
354
 
243
- // src/auth/oauth.ts
244
- import { spawn } from "child_process";
245
- import { createHash, constants as cryptoConstants, publicEncrypt, randomBytes } from "crypto";
246
- import { createServer } from "http";
247
- var DEFAULT_REDIRECT_PORT = 9010;
248
- var DEFAULT_SCOPE = "openid offline all";
249
- var STUDIOWEB_LOGIN_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
250
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyOstgbYuubBi2PUqeVj
251
- GKlkwVUY6w1Y8d4k116dI2SkZI8fxcjHALv77kItO4jYLVplk9gO4HAtsisnNE2o
252
- wlYIqdmyEPMwupaeFFFcg751oiTXJiYbtX7ABzU5KQYPjRSEjMq6i5qu/mL67XTk
253
- hvKwrC83zme66qaKApmKupDODPb0RRkutK/zHfd1zL7sciBQ6psnNadh8pE24w8O
254
- 2XVy1v2bgSNkGHABgncR7seyIg81JQ3c/Axxd6GsTztjLnlvGAlmT1TphE84mi99
255
- fUaGD2A1u1qdIuNc+XuisFeNcUW6fct0+x97eS2eEGRr/7qxWmO/P20sFVzXc2bF
256
- 1QIDAQAB
257
- -----END PUBLIC KEY-----`;
258
- function normalizeBaseUrl(value) {
259
- return value.replace(/\/+$/, "");
260
- }
261
- function generatePkce() {
262
- const verifier = randomBytes(48).toString("base64url");
263
- return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
264
- }
265
- function buildAuthorizeUrl(base, clientId, redirectUri, state, codeChallenge, scope = DEFAULT_SCOPE) {
266
- const params = new URLSearchParams({
267
- response_type: "code",
268
- client_id: clientId,
269
- redirect_uri: redirectUri,
270
- scope,
271
- state,
272
- "x-forwarded-prefix": "",
273
- lang: "zh-cn",
274
- product: "adp",
275
- code_challenge: codeChallenge,
276
- code_challenge_method: "S256"
277
- });
278
- return `${base}/oauth2/auth?${params.toString()}`;
279
- }
280
- function mapToken(data) {
281
- return {
282
- accessToken: data.access_token,
283
- refreshToken: data.refresh_token,
284
- idToken: data.id_token
285
- };
286
- }
287
- async function registerClient(base, redirectUri, scope = DEFAULT_SCOPE) {
288
- const res = await fetch(`${base}/oauth2/clients`, {
289
- method: "POST",
290
- headers: { "Content-Type": "application/json", Accept: "application/json" },
291
- body: JSON.stringify({
292
- client_name: "openbkn-cli",
293
- grant_types: ["authorization_code", "implicit", "refresh_token"],
294
- response_types: ["token id_token", "code", "token"],
295
- scope,
296
- redirect_uris: [redirectUri],
297
- post_logout_redirect_uris: [redirectUri.replace("/callback", "/successful-logout")],
298
- metadata: { device: { name: "openbkn-cli", client_type: "web", description: "openbkn CLI" } }
299
- })
300
- });
301
- if (!res.ok) {
302
- throw new Error(
303
- `Client registration failed (${res.status}): ${await res.text() || res.statusText}`
304
- );
305
- }
306
- const data = await res.json();
307
- return { clientId: data.client_id, clientSecret: data.client_secret };
308
- }
309
- async function exchangeCode(base, code, redirectUri, client, codeVerifier) {
310
- const params = {
311
- grant_type: "authorization_code",
312
- code,
313
- redirect_uri: redirectUri,
314
- code_verifier: codeVerifier
315
- };
316
- const headers = {
317
- "Content-Type": "application/x-www-form-urlencoded",
318
- Accept: "application/json"
319
- };
320
- if (client.clientSecret) {
321
- headers.Authorization = `Basic ${Buffer.from(`${client.clientId}:${client.clientSecret}`).toString("base64")}`;
322
- } else {
323
- params.client_id = client.clientId;
324
- }
325
- const res = await fetch(`${base}/oauth2/token`, {
355
+ // src/api/eacp-crypto.ts
356
+ import {
357
+ constants,
358
+ createPrivateKey,
359
+ createPublicKey,
360
+ publicEncrypt
361
+ } from "crypto";
362
+
363
+ // src/api/admin.ts
364
+ async function changePasswordSafe(ctx, account, oldPassword, newPassword) {
365
+ await request(ctx, "/api/safe/v1/auth/change-password", {
326
366
  method: "POST",
327
- headers,
328
- body: new URLSearchParams(params).toString()
367
+ body: { account, old_password: oldPassword, new_password: newPassword }
329
368
  });
330
- if (!res.ok) {
331
- throw new Error(
332
- `Token exchange failed (${res.status}): ${await res.text() || res.statusText}`
333
- );
334
- }
335
- return mapToken(await res.json());
369
+ return { ok: true };
336
370
  }
337
- function startCallbackServer(port) {
338
- return new Promise((resolve2, reject) => {
339
- const server = createServer((req2, res) => {
340
- const u = new URL(req2.url ?? "/", `http://127.0.0.1:${port}`);
341
- if (u.pathname !== "/callback") {
342
- res.writeHead(404);
343
- res.end();
344
- return;
345
- }
346
- const code = u.searchParams.get("code");
347
- const error = u.searchParams.get("error");
348
- if (error) {
349
- res.writeHead(400, { "content-type": "text/html" });
350
- res.end(`<h1>Login failed</h1><p>${error}</p>`);
351
- server.close(() => reject(new Error(`OAuth error: ${error}`)));
352
- return;
353
- }
354
- if (!code) {
355
- res.writeHead(400);
356
- res.end("missing code");
357
- return;
358
- }
359
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
360
- res.end("<h1>Login successful</h1><p>You can close this window.</p>");
361
- resolve2({
362
- code,
363
- state: u.searchParams.get("state") ?? void 0,
364
- close: () => server.close()
365
- });
366
- });
367
- server.on("error", reject);
368
- server.listen(port, "127.0.0.1");
369
- });
370
- }
371
- function openBrowser(url) {
372
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
371
+
372
+ // src/commands/auth.ts
373
+ async function resolveAccount(baseUrl, accessToken, insecure, idToken) {
374
+ const sub = decodeJwt(idToken ?? accessToken)?.sub;
375
+ if (!sub) return void 0;
373
376
  try {
374
- spawn(cmd, [url], {
375
- stdio: "ignore",
376
- detached: true,
377
- shell: process.platform === "win32"
378
- }).unref();
377
+ const u = await getUserSafe(
378
+ { baseUrl, token: accessToken, businessDomain: DEFAULT_BUSINESS_DOMAIN, insecure },
379
+ sub
380
+ );
381
+ return u.account;
379
382
  } catch {
383
+ return void 0;
380
384
  }
381
385
  }
382
- async function browserLogin(baseUrl, opts = {}) {
383
- const base = normalizeBaseUrl(baseUrl);
384
- const port = opts.port ?? DEFAULT_REDIRECT_PORT;
385
- const redirectUri = `http://127.0.0.1:${port}/callback`;
386
- const scope = opts.scope ?? DEFAULT_SCOPE;
387
- const client = opts.clientId ? { clientId: opts.clientId } : await registerClient(base, redirectUri, scope);
388
- const { verifier, challenge } = generatePkce();
389
- const state = randomBytes(12).toString("hex");
390
- const authUrl = buildAuthorizeUrl(base, client.clientId, redirectUri, state, challenge, scope);
391
- const waiter = startCallbackServer(port);
392
- if (opts.noBrowser) {
393
- process.stderr.write(`Open this URL to log in:
394
- ${authUrl}
395
- `);
396
- } else {
397
- process.stderr.write(`Opening browser for login\u2026
398
- If it doesn't open, visit:
399
- ${authUrl}
400
- `);
401
- openBrowser(authUrl);
386
+ function renderSessions(items) {
387
+ const byPlatform = /* @__PURE__ */ new Map();
388
+ for (const it of items) {
389
+ const arr = byPlatform.get(it.baseUrl) ?? [];
390
+ arr.push(it);
391
+ byPlatform.set(it.baseUrl, arr);
402
392
  }
403
- const { code, state: returned, close } = await waiter;
404
- close();
405
- if (returned && returned !== state) throw new Error("OAuth state mismatch \u2014 possible CSRF.");
406
- return exchangeCode(base, code, redirectUri, client, verifier);
407
- }
408
- function mergeCookies(existing, res) {
409
- const setCookies = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : res.headers.get("set-cookie") ? [res.headers.get("set-cookie")] : [];
410
- const map = /* @__PURE__ */ new Map();
411
- const add = (pair) => {
412
- const eq = pair.indexOf("=");
413
- if (eq > 0) map.set(pair.slice(0, eq), pair.slice(eq + 1));
414
- };
415
- for (const p of existing.split(";").map((s) => s.trim()).filter(Boolean))
416
- add(p);
417
- for (const sc of setCookies) add(sc.split(";")[0]?.trim() ?? "");
418
- return [...map.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
419
- }
420
- function parseSigninProps(html) {
421
- const m = html.match(/<script[^>]*\bid=["']__NEXT_DATA__["'][^>]*>([\s\S]*?)<\/script>/i);
422
- if (!m?.[1]) throw new Error("Could not find __NEXT_DATA__ on /oauth2/signin.");
423
- const data = JSON.parse(m[1]);
424
- const pp = data.props?.pageProps;
425
- const csrftoken = pp?.csrftoken ?? pp?._csrf;
426
- if (typeof csrftoken !== "string") throw new Error("Sign-in page did not expose csrftoken.");
427
- return {
428
- csrftoken,
429
- challenge: typeof pp?.challenge === "string" ? pp.challenge : void 0,
430
- remember: pp?.remember === true || pp?.remember === "true"
431
- };
432
- }
433
- async function followToCallback(startUrl, jar0, state, redirectUri) {
434
- let url = startUrl;
435
- let jar = jar0;
436
- const cb = new URL(redirectUri);
437
- for (let hop = 0; hop < 40; hop++) {
438
- const resp = await fetch(url, {
439
- headers: { Cookie: jar, Accept: "text/html,*/*;q=0.8" },
440
- redirect: "manual"
441
- });
442
- jar = mergeCookies(jar, resp);
443
- if (![302, 303, 307, 308].includes(resp.status)) {
444
- throw new Error(`Unexpected OAuth response (HTTP ${resp.status}).`);
445
- }
446
- const loc = resp.headers.get("location");
447
- if (!loc) throw new Error(`OAuth redirect missing Location (HTTP ${resp.status}).`);
448
- const next = new URL(loc, url);
449
- if (next.origin === cb.origin && next.pathname === cb.pathname) {
450
- const err = next.searchParams.get("error");
451
- if (err) throw new Error(`Authorization failed: ${err}`);
452
- const code = next.searchParams.get("code");
453
- if (next.searchParams.get("state") !== state) throw new Error("OAuth state mismatch.");
454
- if (!code) throw new Error("Callback missing authorization code.");
455
- return code;
456
- }
457
- url = next.href;
393
+ const lines = [];
394
+ for (const [platform, users] of byPlatform) {
395
+ lines.push(platform);
396
+ for (const u of users) lines.push(` ${u.active ? "*" : " "} ${u.username ?? u.userId}`);
458
397
  }
459
- throw new Error("Too many OAuth redirects.");
398
+ return lines.join("\n") || "(no saved sessions)";
460
399
  }
461
- async function passwordLogin(baseUrl, username, password, opts = {}) {
462
- const base = normalizeBaseUrl(baseUrl);
463
- const port = opts.port ?? DEFAULT_REDIRECT_PORT;
464
- const redirectUri = `http://127.0.0.1:${port}/callback`;
465
- const scope = opts.scope ?? DEFAULT_SCOPE;
466
- const client = opts.clientId ? { clientId: opts.clientId } : await registerClient(base, redirectUri, scope);
467
- const { verifier, challenge } = generatePkce();
468
- const state = randomBytes(12).toString("hex");
469
- let jar = "";
470
- const authResp = await fetch(
471
- buildAuthorizeUrl(base, client.clientId, redirectUri, state, challenge, scope),
472
- { redirect: "manual" }
473
- );
474
- jar = mergeCookies(jar, authResp);
475
- const authLoc = authResp.headers.get("location");
476
- if (!authLoc) throw new Error(`/oauth2/auth did not redirect (HTTP ${authResp.status}).`);
477
- const signinUrl = new URL(authLoc, base);
478
- if (!signinUrl.pathname.includes("signin")) {
479
- throw new Error(`Expected a sign-in redirect, got: ${authLoc}`);
480
- }
481
- const pageResp = await fetch(signinUrl.href, {
482
- headers: { Cookie: jar, Accept: "text/html,*/*;q=0.8" },
483
- redirect: "manual"
484
- });
485
- jar = mergeCookies(jar, pageResp);
486
- const props = parseSigninProps(await pageResp.text());
487
- const loginChallenge = signinUrl.searchParams.get("login_challenge")?.trim() || props.challenge?.trim();
488
- if (!loginChallenge) throw new Error("Could not resolve the login challenge.");
489
- const cipher = publicEncrypt(
490
- {
491
- key: opts.signinPublicKeyPem ?? STUDIOWEB_LOGIN_PUBLIC_KEY_PEM,
492
- padding: cryptoConstants.RSA_PKCS1_PADDING
493
- },
494
- Buffer.from(password, "utf8")
495
- ).toString("base64");
496
- const postResp = await fetch(`${base}/oauth2/signin`, {
497
- method: "POST",
498
- headers: {
499
- Cookie: jar,
500
- "Content-Type": "application/json",
501
- Accept: "application/json, text/plain, */*",
502
- Origin: new URL(base).origin,
503
- Referer: signinUrl.href
504
- },
505
- body: JSON.stringify({
506
- _csrf: props.csrftoken,
507
- challenge: loginChallenge,
508
- account: username,
509
- password: cipher,
510
- vcode: { id: "", content: "" },
511
- dualfactorauthinfo: { validcode: { vcode: "" }, OTP: { OTP: "" } },
512
- remember: props.remember ?? false,
513
- device: { name: "", description: "", client_type: "console_web", udids: [] }
514
- }),
515
- redirect: "manual"
516
- });
517
- jar = mergeCookies(jar, postResp);
518
- let code;
519
- if ([302, 303, 307].includes(postResp.status)) {
520
- const loc = postResp.headers.get("location");
521
- if (!loc) throw new Error("Sign-in response missing Location.");
522
- code = await followToCallback(new URL(loc, base).href, jar, state, redirectUri);
523
- } else if (postResp.status === 200) {
524
- const text = await postResp.text();
525
- let json = null;
526
- try {
527
- json = JSON.parse(text);
528
- } catch {
529
- }
530
- const redir = json && typeof json.redirect === "string" ? json.redirect : "";
531
- if (!redir) {
532
- const msg = json && typeof json.message === "string" ? json.message : text.slice(0, 300);
533
- throw new InputError(`Sign-in failed: ${msg}`);
534
- }
535
- code = await followToCallback(new URL(redir, base).href, jar, state, redirectUri);
536
- } else {
537
- throw new InputError(
538
- `Sign-in failed (HTTP ${postResp.status}): ${(await postResp.text()).slice(0, 300)}`
539
- );
540
- }
541
- return exchangeCode(base, code, redirectUri, client, verifier);
542
- }
543
-
544
- // src/commands/auth.ts
545
400
  function registerAuthLeaves(cmd) {
546
- cmd.command("login <url>").description("Log in to a platform (attach a token, or browser/password OAuth)").option("-u, --username <name>", "username for password signin").option("-p, --password <pwd>", "password for password signin").option("--token <token>", "provide a token directly (CI / headless)").option("--client-id <id>", "use a fixed OAuth2 client id (skip dynamic registration)").option("--client-secret <secret>", "OAuth2 client secret (omit for public/PKCE)").option("--port <n>", "local callback port", (v) => Number.parseInt(v, 10)).option(
547
- "--signin-public-key-file <path>",
548
- "override the RSA public key (PEM) for password signin"
549
- ).option("--product <name>", "OAuth product query (default 'adp')").option("--no-browser", "headless: print the authorize URL instead of opening a browser").action(async (url, opts, cmd2) => {
401
+ cmd.command("login <url>").description("Log in to a platform (attach a token, or browser/password OAuth)").option("-u, --username <name>", "username for password signin").option("-p, --password <pwd>", "password for password signin").option("--token <token>", "provide a token directly (CI / headless)").option("--client-id <id>", "use a fixed OAuth2 client id (skip dynamic registration)").option("--client-secret <secret>", "OAuth2 client secret (omit for public/PKCE)").option(
402
+ "--port <n>",
403
+ "loopback redirect port for the auth_code flow",
404
+ (v) => Number.parseInt(v, 10)
405
+ ).option("--device", "headless device-code login (RFC 8628) \u2014 no callback server, no password").option("--audience <aud>", "device-code token audience", "bkn-safe").option(
406
+ "--timeout <s>",
407
+ "device-login wait before timing out",
408
+ (v) => Number.parseInt(v, 10),
409
+ 120
410
+ ).option("--no-browser", "(legacy) print the URL instead of opening a browser").option("--product <name>", "(legacy) ISF OAuth product query").option("--signin-public-key-file <path>", "(legacy) RSA public key for ISF /oauth2/signin").option("--no-auth", "register the platform with no authentication (no bkn-safe)").action(async (url, opts, cmd2) => {
550
411
  const g = cmd2.optsWithGlobals();
551
412
  if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
413
+ const out = outputOptions(cmd2);
414
+ const report = (r) => {
415
+ if (out.json || out.compact) {
416
+ printJson({ loggedIn: true, ...r }, out);
417
+ } else if (r.noAuth) {
418
+ process.stdout.write(`Registered ${r.baseUrl ?? url} (no authentication)
419
+ `);
420
+ } else {
421
+ process.stdout.write(`Logged in to ${r.baseUrl ?? url} as ${r.username ?? r.userId}
422
+ `);
423
+ }
424
+ };
552
425
  const token = opts.token ?? g.token;
553
426
  if (token) {
554
- const r2 = attachToken(url, token, { insecure: g.insecure });
555
- printJson({ loggedIn: true, ...r2 }, outputOptions(cmd2));
427
+ report(attachToken(url, token, { insecure: g.insecure }));
556
428
  return;
557
429
  }
558
- const signinKey = opts.signinPublicKeyFile ? readFileSync2(opts.signinPublicKeyFile, "utf8") : void 0;
559
- const tokens = opts.username ? await passwordLogin(url, opts.username, opts.password ?? "", {
560
- clientId: opts.clientId,
561
- port: opts.port,
562
- signinPublicKeyPem: signinKey
563
- }) : await browserLogin(url, {
564
- clientId: opts.clientId,
565
- port: opts.port,
566
- noBrowser: opts.browser === false
567
- });
568
- const r = attachToken(url, tokens.accessToken, {
569
- refreshToken: tokens.refreshToken,
570
- idToken: tokens.idToken,
571
- insecure: g.insecure
572
- });
573
- printJson({ loggedIn: true, ...r }, outputOptions(cmd2));
430
+ if (opts.auth === false) {
431
+ report(attachNoAuth(url, { insecure: g.insecure }));
432
+ return;
433
+ }
434
+ const authStatus = await fetchAuthStatus(url);
435
+ if (authStatus && !authStatus.enabled) {
436
+ process.stderr.write(
437
+ `Platform auth is disabled (stack: ${authStatus.stack ?? "none"}) \u2014 registering without auth.
438
+ `
439
+ );
440
+ report(attachNoAuth(url, { insecure: g.insecure }));
441
+ return;
442
+ }
443
+ let tokens;
444
+ let account;
445
+ try {
446
+ if (opts.username || opts.password) {
447
+ const username = opts.username ?? await promptLine("Username: ");
448
+ account = username;
449
+ const password = opts.password ?? await promptLine("Password: ", true);
450
+ tokens = await credentialDeviceLogin(url, username, password, {
451
+ clientId: opts.clientId,
452
+ audience: opts.audience,
453
+ timeoutMs: opts.timeout * 1e3
454
+ });
455
+ } else {
456
+ const headless = isHeadless();
457
+ const openInBrowser = !opts.device && opts.browser !== false && !headless;
458
+ tokens = await deviceLogin(url, {
459
+ clientId: opts.clientId,
460
+ audience: opts.audience,
461
+ timeoutMs: opts.timeout * 1e3,
462
+ onPrompt: ({ userCode, verificationUri, verificationUriComplete }) => {
463
+ const target = verificationUriComplete ?? verificationUri;
464
+ process.stderr.write(
465
+ `
466
+ Open this URL to sign in and authorize:
467
+ ${target}
468
+ User code: ${userCode}
469
+ `
470
+ );
471
+ if (openInBrowser) openBrowser(target);
472
+ else if (headless && !opts.device && opts.browser !== false)
473
+ process.stderr.write("(headless \u2014 approve on any machine with a browser)\n");
474
+ process.stderr.write("Waiting for authorization\u2026\n");
475
+ }
476
+ });
477
+ }
478
+ } catch (e) {
479
+ if (e instanceof Error && /Device auth failed \(404\)/.test(e.message)) {
480
+ process.stderr.write("No auth endpoint found \u2014 registering platform without auth.\n");
481
+ report(attachNoAuth(url, { insecure: g.insecure }));
482
+ return;
483
+ }
484
+ throw e;
485
+ }
486
+ if (!account) {
487
+ account = await resolveAccount(
488
+ url,
489
+ tokens.accessToken,
490
+ Boolean(g.insecure),
491
+ tokens.idToken
492
+ );
493
+ }
494
+ report(
495
+ attachToken(url, tokens.accessToken, {
496
+ refreshToken: tokens.refreshToken,
497
+ idToken: tokens.idToken,
498
+ insecure: g.insecure,
499
+ username: account
500
+ })
501
+ );
574
502
  });
575
503
  cmd.command("status").description("Show base URL and whether a token is configured").action((_opts, cmd2) => printJson(status(), outputOptions(cmd2)));
576
- cmd.command("token").description("Print the current access token (keep secret)").action(() => {
577
- process.stdout.write(`${currentToken()}
504
+ cmd.command("token").description("Print the current access token, refreshing it if expired (keep secret)").option("--no-refresh", "print the stored token as-is, without refreshing").action(async (opts, cmd2) => {
505
+ const g = cmd2.optsWithGlobals();
506
+ if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
507
+ const token = opts.refresh === false ? currentToken() : await currentTokenFresh();
508
+ process.stdout.write(`${token}
509
+ `);
510
+ });
511
+ cmd.command("whoami [url]").description("Show current user identity (from the token)").option("--no-lookup", "skip the backend identity fallback (eacp/user/get)").action(async (_url, opts, cmd2) => {
512
+ const g = cmd2.optsWithGlobals();
513
+ if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
514
+ const me = whoami();
515
+ if (opts.lookup !== false && me.baseUrl && me.sub) {
516
+ try {
517
+ const u = await getUserSafe(
518
+ {
519
+ baseUrl: me.baseUrl,
520
+ token: currentToken(),
521
+ businessDomain: DEFAULT_BUSINESS_DOMAIN,
522
+ insecure: Boolean(g.insecure)
523
+ },
524
+ me.sub
525
+ );
526
+ if (u.account) me.username = u.account;
527
+ if (u.name) me.name = u.name;
528
+ } catch {
529
+ }
530
+ }
531
+ const out = outputOptions(cmd2);
532
+ if (out.json || out.compact || out.full) {
533
+ printJson(me, out);
534
+ return;
535
+ }
536
+ const expMs = typeof me.exp === "number" ? me.exp * 1e3 : void 0;
537
+ const expired = expMs !== void 0 && expMs < Date.now();
538
+ const rows = [
539
+ ["User", String(me.username ?? me.sub ?? "(unknown)")],
540
+ ...me.name && me.name !== me.username ? [["Name", String(me.name)]] : [],
541
+ ["ID", String(me.userId ?? me.sub ?? "-")],
542
+ ["Platform", String(me.baseUrl ?? "-")],
543
+ ...expMs !== void 0 ? [["Expires", `${new Date(expMs).toISOString()}${expired ? " (expired)" : ""}`]] : []
544
+ ];
545
+ const pad2 = Math.max(...rows.map(([k]) => k.length));
546
+ process.stdout.write(
547
+ `${rows.map(([k, v]) => `${k.padEnd(pad2)} ${v}`).join("\n")}
548
+ \u2026 use --full or --json for all claims
549
+ `
550
+ );
551
+ });
552
+ cmd.command("list").alias("ls").description("List saved sessions (platform \u2192 users; * = active)").action((_opts, cmd2) => {
553
+ const items = listPlatforms();
554
+ const out = outputOptions(cmd2);
555
+ if (out.json || out.compact) printJson(items, out);
556
+ else process.stdout.write(`${renderSessions(items)}
578
557
  `);
579
558
  });
580
- cmd.command("whoami [url]").description("Show current user identity (from the token)").option("--no-lookup", "skip the backend identity fallback (eacp/user/get)").action(
581
- (_url, _opts, cmd2) => printJson(whoami(), outputOptions(cmd2))
582
- );
583
- cmd.command("list").alias("ls").description("List platforms with a saved session").action((_opts, cmd2) => printJson(listPlatforms(), outputOptions(cmd2)));
584
559
  cmd.command("use <url>").description("Switch the active platform").action((url, _opts, cmd2) => {
585
560
  use(url);
586
561
  printJson(status(), outputOptions(cmd2));
@@ -589,28 +564,56 @@ function registerAuthLeaves(cmd) {
589
564
  cmd.command("delete <url>").description("Delete saved credentials for a platform").action(
590
565
  (url, _opts, cmd2) => printJson({ deleted: deletePlatform(url) }, outputOptions(cmd2))
591
566
  );
592
- cmd.command("switch <url> <user-id>").description("Switch the active user for a platform").action((url, userId, _opts, cmd2) => {
593
- printJson(switchUser(url, userId), outputOptions(cmd2));
567
+ cmd.command("switch <url> <user>").description("Switch the active user for a platform (by username or user id)").action((url, user, _opts, cmd2) => {
568
+ const r = switchUser(url, user);
569
+ const out = outputOptions(cmd2);
570
+ if (out.json || out.compact) printJson(r, out);
571
+ else process.stdout.write(`Switched to ${r.username ?? r.userId} on ${r.baseUrl}
572
+ `);
594
573
  });
595
- cmd.command("users <url>").description("List saved user profiles for a platform").action((url, _opts, cmd2) => {
596
- printJson(usersOf(url), outputOptions(cmd2));
574
+ cmd.command("users <url>").description("List saved users for a platform (* = active)").action((url, _opts, cmd2) => {
575
+ const norm = url.replace(/\/+$/, "");
576
+ const items = listPlatforms().filter((i) => i.baseUrl === norm);
577
+ const out = outputOptions(cmd2);
578
+ if (out.json || out.compact) printJson(items, out);
579
+ else process.stdout.write(`${renderSessions(items)}
580
+ `);
597
581
  });
598
582
  cmd.command("export").description("Export the active session's tokens (for a headless host)").action((_opts, cmd2) => {
599
583
  printJson(exportCreds(), outputOptions(cmd2));
600
584
  });
601
- cmd.command("change-password [url]").description("Change your account password (EACP, RSA-encrypted in transit)").requiredOption("-a, --account <name>", "account / login name").requiredOption("--old-password <pwd>", "current password").requiredOption("--new-password <pwd>", "new password").option("--public-key-file <path>", "override the RSA public key (PEM) for password encryption").action(async (_url, opts, cmd2) => {
585
+ cmd.command("change-password [url]").description("Change your account password (bkn-safe self-service; no browser)").option("-a, --account <name>", "account / login name (the login column, e.g. admin)").option("--old-password <pwd>", "current password").option("--new-password <pwd>", "new password").option("--public-key-file <path>", "(legacy) RSA public key for ISF password encryption").action(async (url, opts, cmd2) => {
602
586
  const g = cmd2.optsWithGlobals();
587
+ if (g.insecure) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
603
588
  const ctx = resolveContext({
604
- baseUrl: g.baseUrl,
589
+ baseUrl: url ?? g.baseUrl,
605
590
  token: g.token,
606
591
  user: g.user,
607
592
  businessDomain: g.bizDomain,
608
593
  insecure: g.insecure
609
594
  });
610
- printJson(
611
- await changePassword(ctx, opts.account, opts.oldPassword, opts.newPassword),
612
- outputOptions(cmd2)
613
- );
595
+ const account = opts.account ?? await promptLine("Account: ");
596
+ const oldPassword = opts.oldPassword ?? await promptLine("Current password: ", true);
597
+ let newPassword = opts.newPassword;
598
+ if (!newPassword) {
599
+ newPassword = await promptLine("New password: ", true);
600
+ const confirm = await promptLine("Confirm new password: ", true);
601
+ if (newPassword !== confirm) throw new Error("New passwords do not match.");
602
+ }
603
+ try {
604
+ printJson(
605
+ await changePasswordSafe(ctx, account, oldPassword, newPassword),
606
+ outputOptions(cmd2)
607
+ );
608
+ } catch (e) {
609
+ if (e instanceof HttpError && e.status === 401) {
610
+ throw new InputError("Wrong account or current password.");
611
+ }
612
+ if (e instanceof HttpError && e.status === 400) {
613
+ throw new InputError("New password must differ from the current one.");
614
+ }
615
+ throw e;
616
+ }
614
617
  });
615
618
  }
616
619
  function authCommand() {
@@ -621,6 +624,7 @@ function authCommand() {
621
624
 
622
625
  // src/commands/admin.ts
623
626
  var int = (v) => Number.parseInt(v, 10);
627
+ var DEFAULT_RESET_PASSWORD = "openbkn";
624
628
  function adminCommand() {
625
629
  const admin = new Command2("admin").description(
626
630
  "Operator CLI (kweaver-admin): org, user, role, models, audit"
@@ -753,12 +757,25 @@ function adminCommand() {
753
757
  user.command("delete <id>").description("Delete a user").action(async (id, _opts, cmd) => {
754
758
  printJson(await clientFrom(cmd).admin.userDelete(id), outputOptions(cmd));
755
759
  });
756
- user.command("reset-password [id]").description("Reset a user's password (RSA-encrypted in transit)").option("--id <userId>", "explicit user UUID (alt to the positional id)").option("--user <account>", "resolve the user by account / login name").option("--password <s>", "the new password").option("--new-password <s>", "the new password (alias of --password)").option("--prompt-password", "prompt for the new password interactively").option("-y, --yes", "skip confirmation").action(async (id, opts, cmd) => {
760
+ user.command("reset-password [id]").description("Reset a user's password (defaults to the platform initial password)").option("--id <userId>", "explicit user UUID (alt to the positional id)").option("--user <account>", "resolve the user by account / login name").option("--password <s>", "the new password (default: platform initial 'openbkn')").option("--new-password <s>", "the new password (alias of --password)").option("--prompt-password", "type the new password interactively (input hidden)").option("-y, --yes", "skip confirmation").action(async (id, opts, cmd) => {
757
761
  const userId = id ?? opts.id ?? opts.user;
758
- const pwd = opts.password ?? opts.newPassword;
759
762
  if (!userId) throw new Error("Provide a user id (positional or --id).");
760
- if (!pwd) throw new Error("Provide --password / --new-password.");
761
- printJson(await clientFrom(cmd).admin.userResetPassword(userId, pwd), outputOptions(cmd));
763
+ let explicit = opts.password ?? opts.newPassword;
764
+ if (!explicit && opts.promptPassword) {
765
+ explicit = await promptLine("New password: ", true);
766
+ if (!explicit) throw new InputError("No password entered.");
767
+ }
768
+ const pwd = explicit ?? DEFAULT_RESET_PASSWORD;
769
+ const r = await clientFrom(cmd).admin.userResetPassword(userId, pwd);
770
+ const out = outputOptions(cmd);
771
+ if (out.json || out.compact) printJson(r, out);
772
+ else if (explicit) process.stdout.write(`Password reset for ${userId}.
773
+ `);
774
+ else
775
+ process.stdout.write(
776
+ `Password reset for ${userId} to the initial password '${DEFAULT_RESET_PASSWORD}' (must change on next login).
777
+ `
778
+ );
762
779
  });
763
780
  const role = admin.command("role").description("Role management");
764
781
  role.command("list").description("List roles").option("--keyword <s>", "filter by keyword").option("--limit <n>", "page size", int, 100).option("--offset <n>", "page offset", int, 0).option("--source <s>", "role source filter (business | user)").action(async (opts, cmd) => {
@@ -791,6 +808,41 @@ function adminCommand() {
791
808
  outputOptions(cmd)
792
809
  );
793
810
  });
811
+ role.command("create").description("Create a custom role (bkn-safe; built-in roles are read-only)").requiredOption("--name <name>", "role name").option("--description <text>", "role description").action(async (opts, cmd) => {
812
+ printJson(
813
+ await clientFrom(cmd).admin.roleCreate(opts.name, opts.description),
814
+ outputOptions(cmd)
815
+ );
816
+ });
817
+ role.command("update <role>").description("Update a custom role's name/description (403 on built-in)").option("--name <name>", "new name").option("--description <text>", "new description").action(async (roleId, opts, cmd) => {
818
+ printJson(
819
+ await clientFrom(cmd).admin.roleUpdate(roleId, {
820
+ name: opts.name,
821
+ description: opts.description
822
+ }),
823
+ outputOptions(cmd)
824
+ );
825
+ });
826
+ role.command("delete <role>").description("Delete a custom role (403 on built-in)").option("-y, --yes", "skip confirmation").action(async (roleId, _opts, cmd) => {
827
+ printJson(await clientFrom(cmd).admin.roleDelete(roleId), outputOptions(cmd));
828
+ });
829
+ for (const [verb, grant] of [
830
+ ["grant-perm", true],
831
+ ["revoke-perm", false]
832
+ ]) {
833
+ role.command(`${verb} <role>`).description(`${grant ? "Grant" : "Revoke"} a permission on a custom role (403 on built-in)`).requiredOption("--resource-type <t>", "resource type (e.g. catalog)").option("--resource-id <id>", "resource id ('*' = whole type)", "*").requiredOption("--operations <list>", "comma-separated operations").action(async (roleId, opts, cmd) => {
834
+ printJson(
835
+ await clientFrom(cmd).admin.rolePermission(
836
+ roleId,
837
+ grant,
838
+ opts.resourceType,
839
+ opts.resourceId,
840
+ csv(opts.operations) ?? []
841
+ ),
842
+ outputOptions(cmd)
843
+ );
844
+ });
845
+ }
794
846
  const modelBody = (opts) => {
795
847
  if (opts.body || opts.bodyFile) return readBody(opts);
796
848
  const mc = {};
@@ -1026,7 +1078,7 @@ function agentCommand() {
1026
1078
  import { Command as Command4 } from "commander";
1027
1079
 
1028
1080
  // src/utils/bkn-validate.ts
1029
- import { existsSync, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
1081
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1030
1082
  import { join, resolve } from "path";
1031
1083
  var BKN_OBJECT_NAME_MAX_LENGTH = 40;
1032
1084
  function parseFrontmatter(text) {
@@ -1079,7 +1131,7 @@ function validateBknDirectory(dirPath) {
1079
1131
  if (!existsSync(networkPath)) {
1080
1132
  errors.push("Missing network.bkn at the BKN root.");
1081
1133
  } else {
1082
- const fm = parseFrontmatter(readFileSync3(networkPath, "utf8"));
1134
+ const fm = parseFrontmatter(readFileSync2(networkPath, "utf8"));
1083
1135
  if (!fm) errors.push("network.bkn has no frontmatter block.");
1084
1136
  else {
1085
1137
  if (fm.type !== "knowledge_network")
@@ -1091,7 +1143,7 @@ function validateBknDirectory(dirPath) {
1091
1143
  const otIds = /* @__PURE__ */ new Set();
1092
1144
  const otFiles = bknFiles(join(dir, "object_types"));
1093
1145
  for (const file of otFiles) {
1094
- const fm = parseFrontmatter(readFileSync3(file, "utf8"));
1146
+ const fm = parseFrontmatter(readFileSync2(file, "utf8"));
1095
1147
  const rel = file.slice(dir.length + 1);
1096
1148
  if (!fm || fm.type !== "object_type") {
1097
1149
  errors.push(`${rel}: not a valid object_type (missing/wrong frontmatter type).`);
@@ -1111,7 +1163,7 @@ function validateBknDirectory(dirPath) {
1111
1163
  }
1112
1164
  const rtFiles = bknFiles(join(dir, "relation_types"));
1113
1165
  for (const file of rtFiles) {
1114
- const text = readFileSync3(file, "utf8");
1166
+ const text = readFileSync2(file, "utf8");
1115
1167
  const fm = parseFrontmatter(text);
1116
1168
  const rel = file.slice(dir.length + 1);
1117
1169
  if (!fm || fm.type !== "relation_type") {
@@ -1541,6 +1593,49 @@ function configCommand() {
1541
1593
  // src/commands/context.ts
1542
1594
  import { Command as Command7 } from "commander";
1543
1595
  var int4 = (v) => Number.parseInt(v, 10);
1596
+ var collectArg = (v, prev) => {
1597
+ prev.push(v);
1598
+ return prev;
1599
+ };
1600
+ function buildArgs(opts) {
1601
+ let out = {};
1602
+ if (opts.args) {
1603
+ try {
1604
+ out = JSON.parse(opts.args);
1605
+ } catch {
1606
+ throw new InputError("--args must be valid JSON");
1607
+ }
1608
+ }
1609
+ for (const pair of opts.arg ?? []) {
1610
+ const idx = pair.indexOf("=");
1611
+ if (idx <= 0) throw new InputError(`--arg must be key=value (got: ${pair})`);
1612
+ const key = pair.slice(0, idx);
1613
+ const raw = pair.slice(idx + 1);
1614
+ try {
1615
+ out[key] = JSON.parse(raw);
1616
+ } catch {
1617
+ out[key] = raw;
1618
+ }
1619
+ }
1620
+ return out;
1621
+ }
1622
+ function printToolList(res, out) {
1623
+ if (out.json || out.compact) {
1624
+ printJson(res, out);
1625
+ return;
1626
+ }
1627
+ const r = res ?? {};
1628
+ const arr = [res, r.tools, r.result?.tools, r.data].find(Array.isArray);
1629
+ if (!arr) {
1630
+ printJson(res, out);
1631
+ return;
1632
+ }
1633
+ const rows = arr.map((t) => ({
1634
+ name: t.name ?? t.tool_name ?? t.key ?? "",
1635
+ description: typeof t.description === "string" ? t.description : ""
1636
+ }));
1637
+ printJson(rows, out);
1638
+ }
1544
1639
  function contextCommand() {
1545
1640
  const cmd = new Command7("context").description(
1546
1641
  "Context loader (MCP) \u2014 schema discovery, instance query, skill recall"
@@ -1567,17 +1662,35 @@ function contextCommand() {
1567
1662
  outputOptions(cmd2)
1568
1663
  );
1569
1664
  });
1570
- cmd.command("tools <kn-id>").description("List MCP tools").action(async (knId, _opts, cmd2) => {
1571
- printJson(await clientFrom(cmd2).context.tools(knId), outputOptions(cmd2));
1665
+ cmd.command("info").description("List the deploy's MCP tool catalog (global \u2014 no KN needed)").action(async (_opts, cmd2) => {
1666
+ printToolList(await clientFrom(cmd2).context.info(), outputOptions(cmd2));
1572
1667
  });
1573
- cmd.command("tool-call <kn-id> <name>").description("Call any MCP tool directly (--args JSON)").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, name, opts, cmd2) => {
1574
- let args;
1575
- try {
1576
- args = JSON.parse(opts.args);
1577
- } catch {
1578
- throw new InputError("--args must be valid JSON");
1579
- }
1580
- printJson(await clientFrom(cmd2).context.toolCall(knId, name, args), outputOptions(cmd2));
1668
+ cmd.command("tools <kn-id>").description("List MCP tools advertised for a KN session").action(async (knId, _opts, cmd2) => {
1669
+ printToolList(await clientFrom(cmd2).context.tools(knId), outputOptions(cmd2));
1670
+ });
1671
+ cmd.command("tool-call <kn-id> <name>").description("Call any MCP tool by name \u2014 current or future (use `tools` to discover)").option("--args <json>", "tool arguments as JSON").option(
1672
+ "--arg <key=value>",
1673
+ "one argument (repeatable; value parsed as JSON, else string)",
1674
+ collectArg,
1675
+ []
1676
+ ).action(async (knId, name, opts, cmd2) => {
1677
+ printJson(
1678
+ await clientFrom(cmd2).context.toolCall(knId, name, buildArgs(opts)),
1679
+ outputOptions(cmd2)
1680
+ );
1681
+ });
1682
+ cmd.command("call-method <kn-id> <method>").description(
1683
+ "Call any MCP method by name (e.g. tools/list, resources/read) \u2014 current or future"
1684
+ ).option("--args <json>", "method params as JSON").option(
1685
+ "--arg <key=value>",
1686
+ "one param (repeatable; value parsed as JSON, else string)",
1687
+ collectArg,
1688
+ []
1689
+ ).action(async (knId, method, opts, cmd2) => {
1690
+ printJson(
1691
+ await clientFrom(cmd2).context.callMethod(knId, method, buildArgs(opts)),
1692
+ outputOptions(cmd2)
1693
+ );
1581
1694
  });
1582
1695
  cmd.command("resources <kn-id>").description("List MCP resources").action(async (knId, _opts, cmd2) => {
1583
1696
  printJson(await clientFrom(cmd2).context.resources(knId), outputOptions(cmd2));
@@ -1609,19 +1722,19 @@ function contextCommand() {
1609
1722
  throw new InputError("--args must be valid JSON");
1610
1723
  }
1611
1724
  };
1612
- cmd.command("query-instance-subgraph <kn-id>").description("Layer-2: query an instance subgraph across relation-type paths").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
1725
+ cmd.command("query-instance-subgraph <kn-id>").description("Query an instance subgraph across relation-type paths").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
1613
1726
  printJson(
1614
1727
  await clientFrom(cmd2).context.queryInstanceSubgraph(knId, jsonArgs(opts.args)),
1615
1728
  outputOptions(cmd2)
1616
1729
  );
1617
1730
  });
1618
- cmd.command("get-logic-properties <kn-id>").description("Layer-3: compute logic-property values for instances").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
1731
+ cmd.command("get-logic-properties <kn-id>").description("Compute logic-property values for instances").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
1619
1732
  printJson(
1620
1733
  await clientFrom(cmd2).context.logicProperties(knId, jsonArgs(opts.args)),
1621
1734
  outputOptions(cmd2)
1622
1735
  );
1623
1736
  });
1624
- cmd.command("get-action-info <kn-id>").description("Layer-3: fetch action info / dynamic tools for an instance").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
1737
+ cmd.command("get-action-info <kn-id>").description("Fetch action info / dynamic tools for an instance").requiredOption("--args <json>", "tool arguments as JSON").action(async (knId, opts, cmd2) => {
1625
1738
  printJson(
1626
1739
  await clientFrom(cmd2).context.actionInfo(knId, jsonArgs(opts.args)),
1627
1740
  outputOptions(cmd2)
@@ -1705,7 +1818,7 @@ function dataflowCommand() {
1705
1818
  }
1706
1819
 
1707
1820
  // src/commands/explore.ts
1708
- import { createServer as createServer2 } from "http";
1821
+ import { createServer } from "http";
1709
1822
  import { Command as Command9 } from "commander";
1710
1823
  var int6 = (v) => Number.parseInt(v, 10);
1711
1824
  var ROUTES = {
@@ -1757,7 +1870,7 @@ function exploreCommand() {
1757
1870
  );
1758
1871
  cmd.option("--port <n>", "port to listen on", int6, 7777).option("--host <h>", "host to bind", "127.0.0.1").action(async (opts, command) => {
1759
1872
  const client = clientFrom(command);
1760
- const server = createServer2((reqMsg, res) => {
1873
+ const server = createServer((reqMsg, res) => {
1761
1874
  void handle(client, reqMsg, res);
1762
1875
  });
1763
1876
  server.listen(opts.port, opts.host, () => {
@@ -1872,18 +1985,18 @@ import { Command as Command11 } from "commander";
1872
1985
  var int8 = (v) => Number.parseInt(v, 10);
1873
1986
  function resourceCommand() {
1874
1987
  const cmd = new Command11("resource").alias("res").description("Resources \u2014 list, find, get, query, delete");
1875
- cmd.command("list").description("List resources under a datasource/catalog").option("--datasource-id <id>", "filter by datasource/catalog id").option("--type <category>", "resource category (table | logicview)").option("--limit <n>", "page size", int8, DEFAULT_LIST_LIMIT).action(async (opts, cmd2) => {
1988
+ cmd.command("list").description("List resources under a catalog").option("--catalog-id <id>", "filter by catalog id").option("--datasource-id <id>", "alias of --catalog-id").option("--category <c>", "resource category (table | logicview | dataset)").option("--type <c>", "alias of --category").option("--limit <n>", "page size", int8, DEFAULT_LIST_LIMIT).action(async (opts, cmd2) => {
1876
1989
  const data = await clientFrom(cmd2).resource.list({
1877
- datasourceId: opts.datasourceId,
1878
- category: opts.type,
1990
+ datasourceId: opts.catalogId ?? opts.datasourceId,
1991
+ category: opts.category ?? opts.type,
1879
1992
  limit: opts.limit
1880
1993
  });
1881
1994
  printJson(data, outputOptions(cmd2));
1882
1995
  });
1883
- cmd.command("find").description("Search resources by name (fuzzy; --exact for strict)").requiredOption("--name <name>", "resource name to search").option("--exact", "exact name match").option("--datasource-id <id>", "limit to a datasource/catalog").action(async (opts, cmd2) => {
1996
+ cmd.command("find").description("Search resources by name (fuzzy; --exact for strict)").requiredOption("--name <name>", "resource name to search").option("--exact", "exact name match").option("--catalog-id <id>", "limit to a catalog").option("--datasource-id <id>", "alias of --catalog-id").action(async (opts, cmd2) => {
1884
1997
  const data = await clientFrom(cmd2).resource.find(opts.name, {
1885
1998
  exact: opts.exact,
1886
- datasourceId: opts.datasourceId
1999
+ datasourceId: opts.catalogId ?? opts.datasourceId
1887
2000
  });
1888
2001
  printJson(data, outputOptions(cmd2));
1889
2002
  });
@@ -2098,11 +2211,11 @@ function toolCommand() {
2098
2211
  }
2099
2212
 
2100
2213
  // src/commands/trace.ts
2101
- import { readFileSync as readFileSync5, writeFileSync } from "fs";
2214
+ import { readFileSync as readFileSync4, writeFileSync } from "fs";
2102
2215
  import { Command as Command14 } from "commander";
2103
2216
 
2104
2217
  // src/trace-ai/schema-validate.ts
2105
- import { readFileSync as readFileSync4 } from "fs";
2218
+ import { readFileSync as readFileSync3 } from "fs";
2106
2219
  import { extname } from "path";
2107
2220
  import yaml from "js-yaml";
2108
2221
  import { z } from "zod";
@@ -2139,7 +2252,7 @@ var DiagnosisRule = z.object({
2139
2252
  params: z.record(z.string(), z.unknown()).optional()
2140
2253
  });
2141
2254
  function parseFile(file) {
2142
- const text = readFileSync4(file, "utf8");
2255
+ const text = readFileSync3(file, "utf8");
2143
2256
  const ext = extname(file).toLowerCase();
2144
2257
  if (ext === ".yaml" || ext === ".yml") return yaml.load(text);
2145
2258
  return JSON.parse(text);
@@ -2206,7 +2319,7 @@ function traceCommand() {
2206
2319
  });
2207
2320
  const evalSet = cmd.command("eval-set").description("Build + run trace eval sets");
2208
2321
  evalSet.command("build <queries-file>").description("Build eval cases from a queries JSON file").option("--out <file>", "write the cases JSON here (default: stdout)").action(async (queriesFile, opts, cmd2) => {
2209
- const raw = JSON.parse(readFileSync5(queriesFile, "utf8"));
2322
+ const raw = JSON.parse(readFileSync4(queriesFile, "utf8"));
2210
2323
  const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
2211
2324
  if (opts.out) {
2212
2325
  writeFileSync(opts.out, JSON.stringify({ cases }, null, 2));
@@ -2216,7 +2329,7 @@ function traceCommand() {
2216
2329
  }
2217
2330
  });
2218
2331
  evalSet.command("test <cases-file>").description("Run an eval set against an agent (--llm enables semantic_match)").requiredOption("--agent <id>", "agent id to run the queries against").option("--version <v>", "agent version", "v0").option("--llm", "enable semantic_match assertions via the local `claude` CLI").action(async (casesFile, opts, cmd2) => {
2219
- const raw = JSON.parse(readFileSync5(casesFile, "utf8"));
2332
+ const raw = JSON.parse(readFileSync4(casesFile, "utf8"));
2220
2333
  const cases = clientFrom(cmd2).trace.evalSetBuild(raw);
2221
2334
  const result = await clientFrom(cmd2).trace.evalSetTest(opts.agent, cases, {
2222
2335
  version: opts.version,
@@ -2338,7 +2451,7 @@ function vegaCommand() {
2338
2451
 
2339
2452
  // src/cli.ts
2340
2453
  var program = new Command16();
2341
- program.name("openbkn").description("Operate the BKN platform from the CLI").version(package_default.version, "-V, --version", "output the version number").option("--base-url <url>", "platform base URL (env: BKN_BASE_URL)").option("--token <value>", "access token (env: BKN_TOKEN)").option("--user <id|name>", "use specific user credentials (env: BKN_USER)").option("--json", "machine-readable JSON output").option("--compact", "single-line JSON output").option("--biz-domain <s>", "business domain (alias: -bd)").option("-k, --insecure", "skip TLS verification (dev / self-signed only)").showHelpAfterError();
2454
+ program.name("openbkn").description("Operate the BKN platform from the CLI").version(package_default.version, "-V, --version", "output the version number").option("--base-url <url>", "platform base URL (env: BKN_BASE_URL)").option("--token <value>", "access token (env: BKN_TOKEN)").option("--user <id|name>", "use specific user credentials (env: BKN_USER)").option("--json", "machine-readable JSON output").option("--compact", "single-line JSON output").option("--full", "human view: show all columns (default trims to the key ones)").option("--biz-domain <s>", "business domain (alias: -bd)").option("-k, --insecure", "skip TLS verification (dev / self-signed only)").showHelpAfterError();
2342
2455
  program.addCommand(authCommand());
2343
2456
  program.addCommand(callCommand());
2344
2457
  program.addCommand(configCommand());