@seclai/cli 1.1.1 → 1.2.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/README.md CHANGED
@@ -42,10 +42,14 @@ npx add-mcp https://api.seclai.com/mcp --header "X-API-Key: $SECLAI_API_KEY" --n
42
42
 
43
43
  ## Documentation
44
44
 
45
- Command reference (latest): https://seclai.github.io/seclai-cli/1.1.1/
45
+ Command reference (latest): https://seclai.github.io/seclai-cli/1.2.0/
46
46
 
47
47
  ## Authentication
48
48
 
49
+ The CLI supports two authentication methods:
50
+
51
+ ### API Key
52
+
49
53
  Set the `SECLAI_API_KEY` environment variable, or pass `--api-key` per-command:
50
54
 
51
55
  ```bash
@@ -55,18 +59,61 @@ export SECLAI_API_KEY="sk-..."
55
59
  seclai --api-key "$SECLAI_API_KEY" agents list
56
60
  ```
57
61
 
62
+ ### SSO (OAuth2 Bearer Token)
63
+
64
+ SSO works out of the box with built-in production defaults — no configuration needed:
65
+
66
+ ```bash
67
+ # Authenticate via browser (Authorization Code + PKCE)
68
+ seclai auth login
69
+
70
+ # Check authentication status
71
+ seclai auth status
72
+
73
+ # Refresh tokens manually
74
+ seclai auth refresh
75
+
76
+ # Log out (clears cached tokens)
77
+ seclai auth logout
78
+ ```
79
+
80
+ For custom SSO settings (e.g. staging environment), use `seclai configure sso`
81
+ or set environment variables:
82
+
83
+ | Variable | Description | Default |
84
+ |---|---|---|
85
+ | `SECLAI_SSO_DOMAIN` | Cognito domain | `auth.seclai.com` |
86
+ | `SECLAI_SSO_CLIENT_ID` | Cognito app client ID | `4bgf8v9qmc5puivbaqon9n5lmr` |
87
+ | `SECLAI_SSO_REGION` | AWS region | `us-west-2` |
88
+
89
+ Use a named profile with `--profile`:
90
+
91
+ ```bash
92
+ seclai --profile staging agents list
93
+ ```
94
+
95
+ Tokens are cached in `~/.seclai/sso/cache/` and auto-refreshed when expired.
96
+
58
97
  ## Environment Variables
59
98
 
60
99
  | Variable | Description |
61
100
  |---|---|
62
101
  | `SECLAI_API_KEY` | Default API key (alternative to `--api-key`) |
63
102
  | `SECLAI_API_URL` | Override API base URL (default: `https://api.seclai.com`) |
103
+ | `SECLAI_PROFILE` | Default SSO profile name (default: `default`) |
104
+ | `SECLAI_CONFIG_DIR` | Config directory path (default: `~/.seclai`) |
105
+ | `SECLAI_SSO_DOMAIN` | Override SSO domain (default: `auth.seclai.com`) |
106
+ | `SECLAI_SSO_CLIENT_ID` | Override SSO client ID (default: `4bgf8v9qmc5puivbaqon9n5lmr`) |
107
+ | `SECLAI_SSO_REGION` | Override SSO region (default: `us-west-2`) |
64
108
 
65
109
  ## Global Options
66
110
 
67
111
  | Flag | Description |
68
112
  |---|---|
69
113
  | `--api-key <key>` | Seclai API key |
114
+ | `--profile <name>` | SSO profile name |
115
+ | `--account-id <id>` | Account ID (`X-Account-Id` header) |
116
+ | `--config-dir <path>` | Config directory path |
70
117
  | `--compact` | Output compact (single-line) JSON |
71
118
  | `-V, --version` | Print version |
72
119
 
package/dist/cli.js CHANGED
@@ -72,6 +72,9 @@ function getCliVersion() {
72
72
  function createClient(opts) {
73
73
  const seclaiOpts = {};
74
74
  if (opts.apiKey !== void 0) seclaiOpts.apiKey = opts.apiKey;
75
+ if (opts.profile !== void 0) seclaiOpts.profile = opts.profile;
76
+ if (opts.configDir !== void 0) seclaiOpts.configDir = opts.configDir;
77
+ if (opts.accountId !== void 0) seclaiOpts.accountId = opts.accountId;
75
78
  const envUrl = process2.env.SECLAI_API_URL;
76
79
  seclaiOpts.baseUrl = envUrl && envUrl.length > 0 ? envUrl : "https://api.seclai.com";
77
80
  return new Seclai(seclaiOpts);
@@ -302,8 +305,8 @@ function register(program, rt) {
302
305
  agents.command("upload-input").description("Upload a file as agent input.").argument("<agentId>", "Agent ID.").requiredOption("--file <path>", "File to upload.").option("--file-name <name>", "Override filename.").option("--mime-type <type>", "MIME type.").action(async (agentId, opts) => {
303
306
  await run(rt, async () => {
304
307
  const client = createClient(program.opts());
305
- const { readFile: readFile3 } = await import("fs/promises");
306
- const bytes = new Uint8Array(await readFile3(opts.file));
308
+ const { readFile: readFile5 } = await import("fs/promises");
309
+ const bytes = new Uint8Array(await readFile5(opts.file));
307
310
  const o = { file: bytes };
308
311
  if (opts.fileName) o.fileName = opts.fileName;
309
312
  if (opts.mimeType) o.mimeType = opts.mimeType;
@@ -362,9 +365,11 @@ function register2(program, rt) {
362
365
  const sources = program.command("sources").alias("source").description("Manage content sources.");
363
366
  sources.command("list").description("List sources.").option("--page <n>", "Page number.", (v) => Number(v)).option("--limit <n>", "Page size.", (v) => Number(v)).option("--sort <field>", "Sort field.").option("--order <asc|desc>", "Sort direction.").option("--account-id <id>", "Filter by account ID.").action(async (opts) => {
364
367
  await run(rt, async () => {
365
- const client = createClient(program.opts());
368
+ const globalOpts = program.opts();
369
+ const client = createClient(globalOpts);
366
370
  const o = listOpts(opts);
367
- if (opts.accountId) o.accountId = opts.accountId;
371
+ const acctId = opts.accountId || globalOpts.accountId;
372
+ if (acctId) o.accountId = acctId;
368
373
  printJson(rt, await client.listSources(o));
369
374
  });
370
375
  });
@@ -2165,6 +2170,453 @@ function register15(program, rt) {
2165
2170
  });
2166
2171
  }
2167
2172
 
2173
+ // src/commands/auth.ts
2174
+ import { randomBytes, createHash } from "crypto";
2175
+ import { createServer } from "http";
2176
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2177
+ import { join as join3 } from "path";
2178
+ import { URL as URL2, URLSearchParams } from "url";
2179
+ import process3 from "process";
2180
+ import {
2181
+ loadSsoProfile,
2182
+ readSsoCache,
2183
+ writeSsoCache,
2184
+ deleteSsoCache,
2185
+ isTokenValid
2186
+ } from "@seclai/sdk";
2187
+ function generateCodeVerifier() {
2188
+ return randomBytes(32).toString("base64url");
2189
+ }
2190
+ function computeCodeChallenge(verifier) {
2191
+ return createHash("sha256").update(verifier).digest("base64url");
2192
+ }
2193
+ function waitForAuthCode(port, state) {
2194
+ return new Promise((resolve, reject) => {
2195
+ const server = createServer((req, res) => {
2196
+ const url = new URL2(req.url ?? "/", `http://localhost:${port}`);
2197
+ if (url.pathname !== "/callback") {
2198
+ res.writeHead(404);
2199
+ res.end();
2200
+ return;
2201
+ }
2202
+ const code = url.searchParams.get("code");
2203
+ const returnedState = url.searchParams.get("state");
2204
+ const error = url.searchParams.get("error");
2205
+ if (error) {
2206
+ res.writeHead(200, { "Content-Type": "text/html" });
2207
+ res.end("<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>");
2208
+ reject(new Error(`OAuth error: ${error}`));
2209
+ server.close();
2210
+ return;
2211
+ }
2212
+ if (!code || returnedState !== state) {
2213
+ res.writeHead(400, { "Content-Type": "text/html" });
2214
+ res.end("<html><body><h2>Invalid callback</h2></body></html>");
2215
+ reject(new Error("Invalid callback: missing code or state mismatch"));
2216
+ server.close();
2217
+ return;
2218
+ }
2219
+ res.writeHead(200, { "Content-Type": "text/html" });
2220
+ res.end("<html><body><h2>Authenticated successfully!</h2><p>You can close this tab.</p></body></html>");
2221
+ resolve({
2222
+ code,
2223
+ cleanup: () => server.close()
2224
+ });
2225
+ });
2226
+ server.listen(port, "127.0.0.1");
2227
+ server.on("error", reject);
2228
+ });
2229
+ }
2230
+ async function exchangeCodeForTokens(profile, code, codeVerifier, redirectUri) {
2231
+ const tokenUrl = `https://${profile.ssoDomain}/oauth2/token`;
2232
+ const body = new URLSearchParams({
2233
+ grant_type: "authorization_code",
2234
+ client_id: profile.ssoClientId,
2235
+ code,
2236
+ redirect_uri: redirectUri,
2237
+ code_verifier: codeVerifier
2238
+ });
2239
+ const resp = await fetch(tokenUrl, {
2240
+ method: "POST",
2241
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2242
+ body: body.toString()
2243
+ });
2244
+ if (!resp.ok) {
2245
+ const text = await resp.text();
2246
+ throw new Error(`Token exchange failed (HTTP ${resp.status}): ${text}`);
2247
+ }
2248
+ const data = await resp.json();
2249
+ const expiresAt = new Date(Date.now() + data.expires_in * 1e3).toISOString();
2250
+ const entry = {
2251
+ accessToken: data.access_token,
2252
+ expiresAt,
2253
+ clientId: profile.ssoClientId,
2254
+ region: profile.ssoRegion,
2255
+ cognitoDomain: profile.ssoDomain
2256
+ };
2257
+ if (data.refresh_token) entry.refreshToken = data.refresh_token;
2258
+ if (data.id_token) entry.idToken = data.id_token;
2259
+ return entry;
2260
+ }
2261
+ var DEFAULT_CALLBACK_PORT = 9876;
2262
+ function resolveBaseUrl() {
2263
+ const envUrl = process3.env.SECLAI_API_URL;
2264
+ return envUrl && envUrl.length > 0 ? envUrl : "https://api.seclai.com";
2265
+ }
2266
+ async function fetchAccountId(accessToken) {
2267
+ const baseUrl = resolveBaseUrl();
2268
+ const resp = await fetch(`${baseUrl}/me`, {
2269
+ headers: { authorization: `Bearer ${accessToken}` }
2270
+ });
2271
+ if (!resp.ok) {
2272
+ const text = await resp.text();
2273
+ throw new Error(`Failed to resolve account ID from /me (HTTP ${resp.status}): ${text}`);
2274
+ }
2275
+ const data = await resp.json();
2276
+ return data.account_id;
2277
+ }
2278
+ async function updateConfigKey(configDir, profileName, key, value) {
2279
+ const configPath = join3(configDir, "config");
2280
+ let content = "";
2281
+ try {
2282
+ content = await readFile3(configPath, "utf-8");
2283
+ } catch {
2284
+ }
2285
+ const sectionHeader = profileName === "default" ? "[default]" : `[profile ${profileName}]`;
2286
+ const sectionIdx = content.indexOf(sectionHeader);
2287
+ if (sectionIdx === -1) {
2288
+ if (content.length > 0 && !content.endsWith("\n")) {
2289
+ content += "\n";
2290
+ }
2291
+ content += `
2292
+ ${sectionHeader}
2293
+ ${key} = ${value}
2294
+ `;
2295
+ } else {
2296
+ const afterHeader = sectionIdx + sectionHeader.length;
2297
+ const nextSectionMatch = content.slice(afterHeader).match(/\n\[/);
2298
+ const sectionEnd = nextSectionMatch ? afterHeader + nextSectionMatch.index : content.length;
2299
+ const sectionBody = content.slice(afterHeader, sectionEnd);
2300
+ const keyRegex = new RegExp(`^${key}\\s*=.*$`, "m");
2301
+ let newSectionBody;
2302
+ if (keyRegex.test(sectionBody)) {
2303
+ newSectionBody = sectionBody.replace(keyRegex, `${key} = ${value}`);
2304
+ } else {
2305
+ const trimmed = sectionBody.trimEnd();
2306
+ newSectionBody = `${trimmed}
2307
+ ${key} = ${value}
2308
+ `;
2309
+ }
2310
+ content = content.slice(0, afterHeader) + newSectionBody + content.slice(sectionEnd);
2311
+ }
2312
+ const { mkdir: mkdir4 } = await import("fs/promises");
2313
+ await mkdir4(configDir, { recursive: true });
2314
+ await writeFile3(configPath, content, { mode: 384 });
2315
+ }
2316
+ function resolveProfile(opts) {
2317
+ const profileName = opts.profile || process3.env.SECLAI_PROFILE || "default";
2318
+ let configDir = opts.configDir || process3.env.SECLAI_CONFIG_DIR;
2319
+ if (!configDir) {
2320
+ const home = process3.env.HOME ?? process3.env.USERPROFILE;
2321
+ if (home && home.trim() !== "") {
2322
+ configDir = join3(home, ".seclai");
2323
+ } else {
2324
+ configDir = join3(process3.cwd(), ".seclai");
2325
+ }
2326
+ }
2327
+ return { profileName, configDir };
2328
+ }
2329
+ async function loadProfile(rt, opts) {
2330
+ const { profileName, configDir } = resolveProfile(opts);
2331
+ const profile = await loadSsoProfile(configDir, profileName);
2332
+ return { profile, profileName, configDir };
2333
+ }
2334
+ function register16(program, rt) {
2335
+ const group = program.command("auth").description("SSO authentication (login/logout/status/refresh).");
2336
+ group.command("login").description("Authenticate via SSO using Authorization Code + PKCE flow.").option("--port <port>", "Local callback port", String(DEFAULT_CALLBACK_PORT)).option("--no-browser", "Print the URL instead of opening a browser").action(async (opts) => {
2337
+ await run(rt, async () => {
2338
+ const globalOpts = program.opts();
2339
+ const { profile, profileName, configDir } = await loadProfile(rt, globalOpts);
2340
+ const port = parseInt(opts.port ?? String(DEFAULT_CALLBACK_PORT), 10);
2341
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
2342
+ throw new Error(`Invalid port: ${opts.port}. Must be an integer between 1 and 65535.`);
2343
+ }
2344
+ const redirectUri = `http://localhost:${port}/callback`;
2345
+ const codeVerifier = generateCodeVerifier();
2346
+ const codeChallenge = computeCodeChallenge(codeVerifier);
2347
+ const state = randomBytes(16).toString("hex");
2348
+ const authUrl = new URL2(`https://${profile.ssoDomain}/oauth2/authorize`);
2349
+ authUrl.searchParams.set("response_type", "code");
2350
+ authUrl.searchParams.set("client_id", profile.ssoClientId);
2351
+ authUrl.searchParams.set("redirect_uri", redirectUri);
2352
+ authUrl.searchParams.set("scope", "openid profile email");
2353
+ authUrl.searchParams.set("state", state);
2354
+ authUrl.searchParams.set("code_challenge", codeChallenge);
2355
+ authUrl.searchParams.set("code_challenge_method", "S256");
2356
+ const authUrlStr = authUrl.toString();
2357
+ const codePromise = waitForAuthCode(port, state);
2358
+ if (opts.browser !== false) {
2359
+ const { spawn } = await import("child_process");
2360
+ const openCmd = process3.platform === "darwin" ? { cmd: "open", args: [authUrlStr] } : process3.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", authUrlStr] } : { cmd: "xdg-open", args: [authUrlStr] };
2361
+ spawn(openCmd.cmd, openCmd.args, { stdio: "ignore", detached: true }).unref();
2362
+ rt.writeErr(`Opening browser for authentication...
2363
+ `);
2364
+ } else {
2365
+ rt.writeErr(`Open this URL in your browser:
2366
+
2367
+ ${authUrlStr}
2368
+
2369
+ `);
2370
+ }
2371
+ rt.writeErr("Waiting for authentication callback...\n");
2372
+ const { code, cleanup } = await codePromise;
2373
+ let tokens;
2374
+ try {
2375
+ rt.writeErr("Exchanging code for tokens...\n");
2376
+ tokens = await exchangeCodeForTokens(profile, code, codeVerifier, redirectUri);
2377
+ await writeSsoCache(configDir, profile, tokens);
2378
+ } finally {
2379
+ cleanup();
2380
+ }
2381
+ let accountId = profile.ssoAccountId;
2382
+ try {
2383
+ rt.writeErr("Resolving account ID...\n");
2384
+ accountId = await fetchAccountId(tokens.accessToken);
2385
+ await updateConfigKey(configDir, profileName, "sso_account_id", accountId);
2386
+ } catch {
2387
+ rt.writeErr("Warning: Could not resolve account ID from /me. You can set it manually with `seclai configure sso`.\n");
2388
+ }
2389
+ rt.writeErr("Successfully authenticated!\n");
2390
+ const loginResult = {
2391
+ status: "authenticated",
2392
+ profile: profileName,
2393
+ expiresAt: tokens.expiresAt
2394
+ };
2395
+ if (accountId) loginResult.accountId = accountId;
2396
+ printJson(rt, loginResult);
2397
+ });
2398
+ });
2399
+ group.command("logout").description("Remove cached SSO tokens for the current profile.").action(async () => {
2400
+ await run(rt, async () => {
2401
+ const globalOpts = program.opts();
2402
+ const { profile, profileName, configDir } = await loadProfile(rt, globalOpts);
2403
+ await deleteSsoCache(configDir, profile);
2404
+ rt.writeErr("Logged out successfully.\n");
2405
+ printJson(rt, { status: "logged_out", profile: profileName });
2406
+ });
2407
+ });
2408
+ group.command("status").description("Show current authentication status for the active profile.").action(async () => {
2409
+ await run(rt, async () => {
2410
+ const globalOpts = program.opts();
2411
+ const { profile: profileLoaded, profileName, configDir } = await loadProfile(rt, globalOpts);
2412
+ const cached = await readSsoCache(configDir, profileLoaded);
2413
+ if (!cached) {
2414
+ const notAuthResult = {
2415
+ profile: profileName,
2416
+ status: "not_authenticated"
2417
+ };
2418
+ if (profileLoaded.ssoAccountId) notAuthResult.accountId = profileLoaded.ssoAccountId;
2419
+ printJson(rt, notAuthResult);
2420
+ return;
2421
+ }
2422
+ const valid = isTokenValid(cached);
2423
+ const statusResult = {
2424
+ profile: profileName,
2425
+ status: valid ? "authenticated" : "expired",
2426
+ expiresAt: cached.expiresAt,
2427
+ hasRefreshToken: Boolean(cached.refreshToken)
2428
+ };
2429
+ if (profileLoaded.ssoAccountId) statusResult.accountId = profileLoaded.ssoAccountId;
2430
+ printJson(rt, statusResult);
2431
+ });
2432
+ });
2433
+ group.command("refresh").description("Manually refresh the SSO token for the current profile.").action(async () => {
2434
+ await run(rt, async () => {
2435
+ const globalOpts = program.opts();
2436
+ const { profile, profileName, configDir } = await loadProfile(rt, globalOpts);
2437
+ const cached = await readSsoCache(configDir, profile);
2438
+ if (!cached?.refreshToken) {
2439
+ throw new Error("No cached token with refresh token. Run `seclai auth login` first.");
2440
+ }
2441
+ const tokenUrl = `https://${profile.ssoDomain}/oauth2/token`;
2442
+ const body = new URLSearchParams({
2443
+ grant_type: "refresh_token",
2444
+ client_id: profile.ssoClientId,
2445
+ refresh_token: cached.refreshToken
2446
+ });
2447
+ const resp = await fetch(tokenUrl, {
2448
+ method: "POST",
2449
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2450
+ body: body.toString()
2451
+ });
2452
+ if (!resp.ok) {
2453
+ const text = await resp.text();
2454
+ throw new Error(`Token refresh failed (HTTP ${resp.status}): ${text}`);
2455
+ }
2456
+ const data = await resp.json();
2457
+ const refreshed = {
2458
+ accessToken: data.access_token,
2459
+ refreshToken: data.refresh_token ?? cached.refreshToken,
2460
+ expiresAt: new Date(Date.now() + data.expires_in * 1e3).toISOString(),
2461
+ clientId: profile.ssoClientId,
2462
+ region: profile.ssoRegion,
2463
+ cognitoDomain: profile.ssoDomain
2464
+ };
2465
+ if (data.id_token) refreshed.idToken = data.id_token;
2466
+ await writeSsoCache(configDir, profile, refreshed);
2467
+ rt.writeErr("Token refreshed successfully.\n");
2468
+ printJson(rt, {
2469
+ status: "refreshed",
2470
+ profile: profileName,
2471
+ expiresAt: refreshed.expiresAt
2472
+ });
2473
+ });
2474
+ });
2475
+ }
2476
+
2477
+ // src/commands/configure.ts
2478
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2479
+ import { join as join4 } from "path";
2480
+ import process4 from "process";
2481
+ import { createInterface } from "readline";
2482
+ import { DEFAULT_SSO_DOMAIN, DEFAULT_SSO_CLIENT_ID, DEFAULT_SSO_REGION } from "@seclai/sdk";
2483
+ function prompt(rt, question, defaultValue) {
2484
+ return new Promise((resolve) => {
2485
+ const rl = createInterface({
2486
+ input: rt.stdin,
2487
+ output: { write: (s) => {
2488
+ rt.writeErr(s);
2489
+ return true;
2490
+ } },
2491
+ terminal: false
2492
+ });
2493
+ const display = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
2494
+ rt.writeErr(display);
2495
+ rl.once("line", (answer) => {
2496
+ rl.close();
2497
+ resolve(answer.trim() || defaultValue || "");
2498
+ });
2499
+ });
2500
+ }
2501
+ function resolveConfigDir(opts) {
2502
+ if (opts.configDir) return opts.configDir;
2503
+ const env = process4.env.SECLAI_CONFIG_DIR;
2504
+ if (env) return env;
2505
+ const home = process4.env.HOME ?? process4.env.USERPROFILE ?? "";
2506
+ return join4(home, ".seclai");
2507
+ }
2508
+ function register17(program, rt) {
2509
+ const group = program.command("configure").description("Configure CLI profiles and settings.");
2510
+ group.command("sso").description("Configure an SSO profile with optional overrides. Defaults to production Seclai SSO.").option("--profile-name <name>", "Profile name to configure (default: from --profile flag)").action(async (opts) => {
2511
+ await run(rt, async () => {
2512
+ const globalOpts = program.opts();
2513
+ const profileName = opts.profileName || globalOpts.profile || "default";
2514
+ const configDir = resolveConfigDir(globalOpts);
2515
+ const configPath = join4(configDir, "config");
2516
+ rt.writeErr(`
2517
+ Configuring SSO profile "${profileName}".
2518
+ `);
2519
+ rt.writeErr(`Defaults: domain=${DEFAULT_SSO_DOMAIN}, region=${DEFAULT_SSO_REGION}
2520
+ `);
2521
+ rt.writeErr(`Press Enter to accept defaults.
2522
+
2523
+ `);
2524
+ const domain = await prompt(rt, "SSO domain", DEFAULT_SSO_DOMAIN);
2525
+ const clientId = await prompt(rt, "SSO client ID", DEFAULT_SSO_CLIENT_ID);
2526
+ const region = await prompt(rt, "SSO region", DEFAULT_SSO_REGION);
2527
+ const accountId = await prompt(rt, "Account ID (optional, resolved after login)");
2528
+ const isDefault = domain === DEFAULT_SSO_DOMAIN && clientId === DEFAULT_SSO_CLIENT_ID && region === DEFAULT_SSO_REGION && !accountId;
2529
+ if (isDefault && profileName === "default") {
2530
+ rt.writeErr(`
2531
+ Using built-in defaults \u2014 no config file needed.
2532
+ `);
2533
+ rt.writeErr(`Run \`seclai auth login\` to authenticate.
2534
+ `);
2535
+ printJson(rt, {
2536
+ profile: profileName,
2537
+ sso_domain: domain,
2538
+ sso_client_id: clientId,
2539
+ sso_region: region,
2540
+ note: "using built-in defaults"
2541
+ });
2542
+ return;
2543
+ }
2544
+ let content = "";
2545
+ try {
2546
+ content = await readFile4(configPath, "utf-8");
2547
+ } catch {
2548
+ }
2549
+ const sectionHeader = profileName === "default" ? "[default]" : `[profile ${profileName}]`;
2550
+ const lines = [];
2551
+ if (domain !== DEFAULT_SSO_DOMAIN) lines.push(`sso_domain = ${domain}`);
2552
+ if (clientId !== DEFAULT_SSO_CLIENT_ID) lines.push(`sso_client_id = ${clientId}`);
2553
+ if (region !== DEFAULT_SSO_REGION) lines.push(`sso_region = ${region}`);
2554
+ if (accountId) lines.push(`sso_account_id = ${accountId}`);
2555
+ const sectionBody = lines.join("\n");
2556
+ const sectionRegex = profileName === "default" ? /^\[default\][^\[]*(?=\[|$(?![\s\S]))/m : new RegExp(`^\\[profile ${escapeRegExp(profileName)}\\][^\\[]*(?=\\[|$(?![\\s\\S]))`, "m");
2557
+ if (sectionRegex.test(content)) {
2558
+ content = content.replace(sectionRegex, `${sectionHeader}
2559
+ ${sectionBody}
2560
+ `);
2561
+ } else {
2562
+ if (content.length > 0 && !content.endsWith("\n")) {
2563
+ content += "\n";
2564
+ }
2565
+ content += `
2566
+ ${sectionHeader}
2567
+ ${sectionBody}
2568
+ `;
2569
+ }
2570
+ await mkdir3(configDir, { recursive: true });
2571
+ await writeFile4(configPath, content, { mode: 384 });
2572
+ rt.writeErr(`
2573
+ Profile "${profileName}" saved to ${configPath}
2574
+ `);
2575
+ rt.writeErr(`Run \`seclai auth login --profile ${profileName}\` to authenticate.
2576
+ `);
2577
+ const result = {
2578
+ profile: profileName,
2579
+ configFile: configPath,
2580
+ sso_domain: domain,
2581
+ sso_client_id: clientId,
2582
+ sso_region: region
2583
+ };
2584
+ if (accountId) result.sso_account_id = accountId;
2585
+ printJson(rt, result);
2586
+ });
2587
+ });
2588
+ group.command("list").description("List all configured profiles.").action(async () => {
2589
+ await run(rt, async () => {
2590
+ const globalOpts = program.opts();
2591
+ const configDir = resolveConfigDir(globalOpts);
2592
+ const configPath = join4(configDir, "config");
2593
+ let content;
2594
+ try {
2595
+ content = await readFile4(configPath, "utf-8");
2596
+ } catch {
2597
+ printJson(rt, { profiles: [], configFile: configPath });
2598
+ return;
2599
+ }
2600
+ const profiles = [];
2601
+ for (const line of content.split("\n")) {
2602
+ const trimmed = line.trim();
2603
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
2604
+ const raw = trimmed.slice(1, -1).trim();
2605
+ if (raw.startsWith("profile ")) {
2606
+ profiles.push(raw.slice("profile ".length).trim());
2607
+ } else {
2608
+ profiles.push(raw);
2609
+ }
2610
+ }
2611
+ }
2612
+ printJson(rt, { profiles, configFile: configPath });
2613
+ });
2614
+ });
2615
+ }
2616
+ function escapeRegExp(s) {
2617
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2618
+ }
2619
+
2168
2620
  // src/cli.ts
2169
2621
  function createProgram(rt = defaultRuntime()) {
2170
2622
  const program = new Command();
@@ -2178,6 +2630,15 @@ All commands return JSON to stdout, making it easy to pipe into jq or other tool
2178
2630
  ).version(cliVersion, "-V, --version", "output the version").option(
2179
2631
  "--api-key <key>",
2180
2632
  "Seclai API key (defaults to SECLAI_API_KEY)."
2633
+ ).option(
2634
+ "--profile <name>",
2635
+ "SSO profile name (defaults to SECLAI_PROFILE, then 'default')."
2636
+ ).option(
2637
+ "--account-id <id>",
2638
+ "Account ID for multi-org targeting (X-Account-Id header)."
2639
+ ).option(
2640
+ "--config-dir <path>",
2641
+ "Config directory (defaults to SECLAI_CONFIG_DIR, then ~/.seclai)."
2181
2642
  ).option(
2182
2643
  "--compact",
2183
2644
  "Output compact JSON (no indentation)."
@@ -2186,16 +2647,19 @@ All commands return JSON to stdout, making it easy to pipe into jq or other tool
2186
2647
  "after",
2187
2648
  `
2188
2649
  Environment:
2189
- SECLAI_API_KEY Default API key (alternative to --api-key)
2190
- SECLAI_API_URL Override API base URL (default: https://api.seclai.com)
2650
+ SECLAI_API_KEY Default API key (alternative to --api-key)
2651
+ SECLAI_API_URL Override API base URL (default: https://api.seclai.com)
2652
+ SECLAI_PROFILE Default SSO profile (alternative to --profile)
2653
+ SECLAI_CONFIG_DIR Config directory (alternative to --config-dir)
2191
2654
 
2192
2655
  Examples:
2193
2656
  seclai agents list
2194
2657
  seclai agents run <agentId> --json '{"input":"Hello"}'
2195
2658
  seclai agents run <agentId> --json '{"input":"Hi"}' --events
2196
- seclai sources list
2197
- seclai kb list
2198
- seclai search --query "deployment guide"
2659
+ seclai configure sso
2660
+ seclai auth login
2661
+ seclai auth status
2662
+ seclai sources list --profile dev
2199
2663
  npx @seclai/cli agents list
2200
2664
  `
2201
2665
  );
@@ -2223,6 +2687,8 @@ Examples:
2223
2687
  register13(program, rt);
2224
2688
  register14(program, rt);
2225
2689
  register15(program, rt);
2690
+ register16(program, rt);
2691
+ register17(program, rt);
2226
2692
  return program;
2227
2693
  }
2228
2694
  async function runCli(argv, rt = defaultRuntime()) {