@kweaver-ai/kweaver-sdk 0.4.12 → 0.4.14

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.
@@ -5,6 +5,87 @@ const TOKEN_TTL_SECONDS = 3600;
5
5
  const REFRESH_THRESHOLD_SEC = 60;
6
6
  const DEFAULT_REDIRECT_PORT = 9010;
7
7
  const DEFAULT_SCOPE = "openid offline all";
8
+ /** POSIX shell single-quote escaping for copy-paste commands. */
9
+ export function shellQuoteForShell(value) {
10
+ return `'${value.replace(/'/g, `'\\''`)}'`;
11
+ }
12
+ /**
13
+ * Build a one-line `kweaver auth login ...` command for headless / other machines.
14
+ * Omits `--client-secret` when empty (PKCE-only client); headless refresh may still require a confidential client.
15
+ */
16
+ export function buildCopyCommand(baseUrl, clientId, clientSecret, refreshToken, tlsInsecure) {
17
+ const parts = ["kweaver", "auth", "login", shellQuoteForShell(normalizeBaseUrl(baseUrl)), "--client-id", shellQuoteForShell(clientId)];
18
+ if (clientSecret) {
19
+ parts.push("--client-secret", shellQuoteForShell(clientSecret));
20
+ }
21
+ if (refreshToken) {
22
+ parts.push("--refresh-token", shellQuoteForShell(refreshToken));
23
+ }
24
+ if (tlsInsecure) {
25
+ parts.push("--insecure");
26
+ }
27
+ return parts.join(" ");
28
+ }
29
+ function escapeHtml(value) {
30
+ return value
31
+ .replace(/&/g, "&")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;")
34
+ .replace(/"/g, "&quot;");
35
+ }
36
+ /**
37
+ * HTML shown after successful OAuth callback with a copyable headless login command.
38
+ */
39
+ export function buildCallbackHtml(copyCommand) {
40
+ const safeCmd = escapeHtml(copyCommand);
41
+ return `<!DOCTYPE html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="utf-8"/>
45
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
46
+ <title>Login successful</title>
47
+ <style>
48
+ body { font-family: system-ui, sans-serif; max-width: 52rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }
49
+ pre { background: #f4f4f5; padding: 1rem; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
50
+ button { margin-top: 0.75rem; padding: 0.5rem 1rem; cursor: pointer; }
51
+ .warn { color: #b45309; margin-top: 1.5rem; font-size: 0.9rem; }
52
+ </style>
53
+ </head>
54
+ <body>
55
+ <h2>Login successful</h2>
56
+ <p>You can close this tab.</p>
57
+ <h3>Headless machine</h3>
58
+ <p>On the computer that has no browser (SSH server, CI runner, container), run:</p>
59
+ <pre id="kw-cmd">${safeCmd}</pre>
60
+ <button type="button" id="kw-copy">Copy command</button>
61
+ <p class="warn">Keep these credentials secure. Anyone with the refresh token and client secret can obtain new access tokens.</p>
62
+ <script>
63
+ (function () {
64
+ var btn = document.getElementById("kw-copy");
65
+ var pre = document.getElementById("kw-cmd");
66
+ if (btn && pre) {
67
+ btn.addEventListener("click", function () {
68
+ var text = pre.textContent || "";
69
+ if (navigator.clipboard && navigator.clipboard.writeText) {
70
+ navigator.clipboard.writeText(text.trim()).then(function () {
71
+ btn.textContent = "Copied";
72
+ setTimeout(function () { btn.textContent = "Copy command"; }, 2000);
73
+ });
74
+ } else {
75
+ window.prompt("Copy this command:", text.trim());
76
+ }
77
+ });
78
+ }
79
+ })();
80
+ </script>
81
+ </body>
82
+ </html>`;
83
+ }
84
+ function buildCallbackExchangeErrorHtml(message) {
85
+ return `<!DOCTYPE html>
86
+ <html lang="en"><head><meta charset="utf-8"/><title>Login error</title></head>
87
+ <body><h2>Login error</h2><pre>${escapeHtml(message)}</pre></body></html>`;
88
+ }
8
89
  export function normalizeBaseUrl(value) {
9
90
  return value.replace(/\/+$/, "");
10
91
  }
@@ -96,35 +177,62 @@ export async function oauth2Login(baseUrl, options) {
96
177
  authParams.set("code_challenge_method", "S256");
97
178
  }
98
179
  const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
99
- // Step 4: Start local callback server, wait for code
100
- const code = await new Promise((resolve, reject) => {
180
+ // Step 4: Start local callback server; exchange code inside handler, then show credentials HTML
181
+ const token = await new Promise((resolve, reject) => {
182
+ let server;
101
183
  const timeoutId = setTimeout(() => {
102
- server.close();
184
+ server?.close();
103
185
  reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
104
186
  }, 120_000);
105
- const server = createServer((req, res) => {
106
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
107
- if (url.pathname === "/callback") {
108
- const receivedState = url.searchParams.get("state");
109
- const receivedCode = url.searchParams.get("code");
110
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
111
- res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
112
- clearTimeout(timeoutId);
113
- server.close();
114
- if (receivedState !== state) {
115
- reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
116
- }
117
- else if (!receivedCode) {
118
- reject(new Error("No authorization code received in callback."));
187
+ server = createServer((req, res) => {
188
+ void (async () => {
189
+ try {
190
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
191
+ if (url.pathname !== "/callback") {
192
+ res.writeHead(404);
193
+ res.end();
194
+ return;
195
+ }
196
+ const receivedState = url.searchParams.get("state");
197
+ const receivedCode = url.searchParams.get("code");
198
+ if (receivedState !== state) {
199
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
200
+ res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch possible CSRF attack."));
201
+ clearTimeout(timeoutId);
202
+ server.close();
203
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
204
+ return;
205
+ }
206
+ if (!receivedCode) {
207
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
208
+ res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
209
+ clearTimeout(timeoutId);
210
+ server.close();
211
+ reject(new Error("No authorization code received in callback."));
212
+ return;
213
+ }
214
+ const exchanged = await exchangeCodeForToken(base, receivedCode, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
215
+ const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
216
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
217
+ res.end(buildCallbackHtml(copyCommand));
218
+ clearTimeout(timeoutId);
219
+ server.close();
220
+ resolve(exchanged);
119
221
  }
120
- else {
121
- resolve(receivedCode);
222
+ catch (err) {
223
+ const message = err instanceof Error ? err.message : String(err);
224
+ try {
225
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
226
+ res.end(buildCallbackExchangeErrorHtml(message));
227
+ }
228
+ catch {
229
+ /* response may already be sent */
230
+ }
231
+ clearTimeout(timeoutId);
232
+ server.close();
233
+ reject(err instanceof Error ? err : new Error(message));
122
234
  }
123
- }
124
- else {
125
- res.writeHead(404);
126
- res.end();
127
- }
235
+ })();
128
236
  });
129
237
  server.listen(port, "127.0.0.1", () => {
130
238
  // Step 5: Open browser (uses spawn with proper Windows quoting)
@@ -134,8 +242,6 @@ export async function oauth2Login(baseUrl, options) {
134
242
  process.stderr.write(`If the wrong browser opens, copy this URL to your correct browser:\n ${authUrl}\n`);
135
243
  });
136
244
  });
137
- // Step 6: Exchange code for tokens
138
- const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
139
245
  setCurrentPlatform(base);
140
246
  return token;
141
247
  });
@@ -275,40 +381,70 @@ export async function playwrightLogin(baseUrl, options) {
275
381
  product: "adp",
276
382
  });
277
383
  const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
278
- // Step 4: Start local callback server to capture the authorization code
279
- const code = await new Promise((resolve, reject) => {
384
+ // Step 4: Start local callback server; exchange code inside handler, then show credentials HTML
385
+ let browser;
386
+ const token = await new Promise((resolve, reject) => {
280
387
  const TIMEOUT_MS = hasCredentials ? 30_000 : 120_000;
388
+ let server;
281
389
  const timeoutId = setTimeout(() => {
282
- server.close();
390
+ server?.close();
283
391
  browser?.close();
284
392
  reject(new Error(`OAuth2 login timed out (${TIMEOUT_MS / 1000}s). No authorization code received.`));
285
393
  }, TIMEOUT_MS);
286
- const server = createServer((req, res) => {
287
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
288
- if (url.pathname === "/callback") {
289
- const receivedState = url.searchParams.get("state");
290
- const receivedCode = url.searchParams.get("code");
291
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
292
- res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
293
- clearTimeout(timeoutId);
294
- server.close();
295
- browser?.close();
296
- if (receivedState !== state) {
297
- reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
298
- }
299
- else if (!receivedCode) {
300
- reject(new Error("No authorization code received in callback."));
394
+ server = createServer((req, res) => {
395
+ void (async () => {
396
+ try {
397
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
398
+ if (url.pathname !== "/callback") {
399
+ res.writeHead(404);
400
+ res.end();
401
+ return;
402
+ }
403
+ const receivedState = url.searchParams.get("state");
404
+ const receivedCode = url.searchParams.get("code");
405
+ if (receivedState !== state) {
406
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
407
+ res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
408
+ clearTimeout(timeoutId);
409
+ server.close();
410
+ browser?.close();
411
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
412
+ return;
413
+ }
414
+ if (!receivedCode) {
415
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
416
+ res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
417
+ clearTimeout(timeoutId);
418
+ server.close();
419
+ browser?.close();
420
+ reject(new Error("No authorization code received in callback."));
421
+ return;
422
+ }
423
+ const exchanged = await exchangeCodeForToken(base, receivedCode, client.clientId, client.clientSecret, redirectUri, undefined, options?.tlsInsecure);
424
+ const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
425
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
426
+ res.end(buildCallbackHtml(copyCommand));
427
+ clearTimeout(timeoutId);
428
+ server.close();
429
+ browser?.close();
430
+ resolve(exchanged);
301
431
  }
302
- else {
303
- resolve(receivedCode);
432
+ catch (err) {
433
+ const message = err instanceof Error ? err.message : String(err);
434
+ try {
435
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
436
+ res.end(buildCallbackExchangeErrorHtml(message));
437
+ }
438
+ catch {
439
+ /* response may already be sent */
440
+ }
441
+ clearTimeout(timeoutId);
442
+ server.close();
443
+ browser?.close();
444
+ reject(err instanceof Error ? err : new Error(message));
304
445
  }
305
- }
306
- else {
307
- res.writeHead(404);
308
- res.end();
309
- }
446
+ })();
310
447
  });
311
- let browser;
312
448
  server.listen(port, "127.0.0.1", async () => {
313
449
  try {
314
450
  browser = await chromium.launch({ headless: hasCredentials });
@@ -334,12 +470,48 @@ export async function playwrightLogin(baseUrl, options) {
334
470
  }
335
471
  });
336
472
  });
337
- // Step 5: Exchange authorization code for tokens (includes refresh_token)
338
- const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, undefined, options?.tlsInsecure);
473
+ if (hasCredentials) {
474
+ const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, token.refreshToken, options?.tlsInsecure);
475
+ process.stderr.write("\nHeadless login: copy this command and run it on a machine without a browser, or use `kweaver auth export`:\n\n" +
476
+ copyCommand +
477
+ "\n\n");
478
+ }
339
479
  setCurrentPlatform(base);
340
480
  return token;
341
481
  });
342
482
  }
483
+ /**
484
+ * Log in on a headless machine using OAuth2 client credentials and a refresh token (no browser).
485
+ * Exchanges the refresh token for a new access token and persists ~/.kweaver/ state.
486
+ */
487
+ export async function refreshTokenLogin(baseUrl, options) {
488
+ const base = normalizeBaseUrl(baseUrl);
489
+ const redirectUri = `http://127.0.0.1:${DEFAULT_REDIRECT_PORT}/callback`;
490
+ const client = {
491
+ baseUrl: base,
492
+ clientId: options.clientId,
493
+ clientSecret: options.clientSecret,
494
+ redirectUri,
495
+ logoutRedirectUri: redirectUri.replace("/callback", "/successful-logout"),
496
+ scope: DEFAULT_SCOPE,
497
+ lang: "zh-cn",
498
+ product: "adp",
499
+ xForwardedPrefix: "",
500
+ };
501
+ saveClientConfig(base, client);
502
+ const synthetic = {
503
+ baseUrl: base,
504
+ accessToken: "",
505
+ tokenType: "Bearer",
506
+ scope: "",
507
+ refreshToken: options.refreshToken,
508
+ obtainedAt: new Date().toISOString(),
509
+ ...(options.tlsInsecure ? { tlsInsecure: true } : {}),
510
+ };
511
+ const token = await runWithTlsInsecure(options.tlsInsecure, () => refreshAccessToken(synthetic));
512
+ setCurrentPlatform(base);
513
+ return token;
514
+ }
343
515
  function tokenNeedsRefresh(token) {
344
516
  if (!token.expiresAt) {
345
517
  return false;
@@ -420,6 +592,15 @@ export async function refreshAccessToken(token) {
420
592
  saveTokenConfig(newToken);
421
593
  return newToken;
422
594
  }
595
+ /**
596
+ * Resolve a usable access token for the current platform.
597
+ *
598
+ * **Default behavior** (saved `~/.kweaver/` session from OAuth2 code login): when the access
599
+ * token is expired or near expiry, automatically exchanges the saved **refresh_token** for a new
600
+ * access token (OAuth2 `refresh_token` grant) and persists it. No extra flags are required.
601
+ *
602
+ * Static env `KWEAVER_TOKEN` bypasses refresh (see implementation).
603
+ */
423
604
  export async function ensureValidToken(opts) {
424
605
  const envToken = process.env.KWEAVER_TOKEN;
425
606
  const envBaseUrl = process.env.KWEAVER_BASE_URL;
package/dist/cli.js CHANGED
@@ -18,6 +18,8 @@ Usage:
18
18
 
19
19
  kweaver auth <platform-url> [--alias name] [-u user] [-p pass] [--playwright] [--insecure|-k]
20
20
  kweaver auth login <platform-url> (alias for auth <url>)
21
+ kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (run on host without browser)
22
+ kweaver auth export [platform-url|alias] [--json]
21
23
  kweaver auth status [platform-url|alias]
22
24
  kweaver auth list
23
25
  kweaver auth use <platform-url|alias>
@@ -51,6 +53,7 @@ Usage:
51
53
  kweaver dataview list [--datasource-id id] [--type atomic|custom] [--limit n] [-bd value] [--pretty]
52
54
  kweaver dataview find --name <name> [--exact] [--datasource-id id] [--wait] [--timeout ms] [-bd value] [--pretty]
53
55
  kweaver dataview get <id> [-bd value] [--pretty]
56
+ kweaver dataview query <id> [--sql sql] [--limit n] [--offset n] [--need-total] [-bd value] [--pretty]
54
57
  kweaver dataview delete <id> [-y] [-bd value]
55
58
 
56
59
  kweaver bkn list [options]
@@ -74,6 +77,7 @@ Usage:
74
77
  kweaver bkn action-log list|get|cancel <kn-id> ...
75
78
 
76
79
  kweaver config set-bd <value>
80
+ kweaver config list-bd
77
81
  kweaver config show
78
82
 
79
83
  kweaver vega health|stats|inspect
@@ -96,7 +100,7 @@ Commands:
96
100
  call (curl) Call an API with curl-style flags and auto-injected token headers
97
101
  agent Agent CRUD, chat, sessions, history, publish/unpublish
98
102
  ds Manage datasources (list, get, delete, tables, connect)
99
- dataview List, find, get, delete data views (atomic / custom)
103
+ dataview|dv List, find, get, query (SQL), delete data views (atomic / custom)
100
104
  bkn Knowledge network (CRUD, build, validate, export, stats, push/pull,
101
105
  object-type, relation-type, subgraph, action-type, action-execution, action-log)
102
106
  config Per-platform configuration (business domain)
@@ -127,7 +131,7 @@ export async function run(argv) {
127
131
  if (command === "ds") {
128
132
  return runDsCommand(rest);
129
133
  }
130
- if (command === "dataview") {
134
+ if (command === "dataview" || command === "dv") {
131
135
  return runDataviewCommand(rest);
132
136
  }
133
137
  if (command === "token") {
@@ -45,7 +45,7 @@ export function formatSimpleAgentList(text, pretty) {
45
45
  export function parseAgentListArgs(args) {
46
46
  let name = "";
47
47
  let offset = 0;
48
- let limit = 50;
48
+ let limit = 30;
49
49
  let category_id = "";
50
50
  let custom_space_id = "";
51
51
  let is_to_square = 1;
@@ -70,9 +70,9 @@ export function parseAgentListArgs(args) {
70
70
  continue;
71
71
  }
72
72
  if (arg === "--limit") {
73
- limit = parseInt(args[i + 1] ?? "50", 10);
73
+ limit = parseInt(args[i + 1] ?? "30", 10);
74
74
  if (Number.isNaN(limit) || limit < 1)
75
- limit = 50;
75
+ limit = 30;
76
76
  i += 1;
77
77
  continue;
78
78
  }
@@ -134,7 +134,7 @@ export function parseAgentSessionsArgs(args) {
134
134
  throw new Error("Missing agent_id");
135
135
  }
136
136
  let businessDomain = "";
137
- let limit;
137
+ let limit = 30;
138
138
  let pretty = true;
139
139
  for (let i = 1; i < args.length; i += 1) {
140
140
  const arg = args[i];
@@ -150,9 +150,9 @@ export function parseAgentSessionsArgs(args) {
150
150
  continue;
151
151
  }
152
152
  if (arg === "--limit") {
153
- limit = parseInt(args[i + 1] ?? "0", 10);
153
+ limit = parseInt(args[i + 1] ?? "30", 10);
154
154
  if (Number.isNaN(limit) || limit < 1)
155
- limit = undefined;
155
+ limit = 30;
156
156
  i += 1;
157
157
  continue;
158
158
  }
@@ -176,7 +176,7 @@ export function parseAgentHistoryArgs(args) {
176
176
  throw new Error("Missing conversation_id");
177
177
  }
178
178
  let businessDomain = "";
179
- let limit;
179
+ let limit = 30;
180
180
  let pretty = true;
181
181
  for (let i = 1; i < args.length; i += 1) {
182
182
  const arg = args[i];
@@ -192,9 +192,9 @@ export function parseAgentHistoryArgs(args) {
192
192
  continue;
193
193
  }
194
194
  if (arg === "--limit") {
195
- limit = parseInt(args[i + 1] ?? "0", 10);
195
+ limit = parseInt(args[i + 1] ?? "30", 10);
196
196
  if (Number.isNaN(limit) || limit < 1)
197
- limit = undefined;
197
+ limit = 30;
198
198
  i += 1;
199
199
  continue;
200
200
  }
@@ -309,7 +309,7 @@ List published agents from the agent-factory API.
309
309
  Options:
310
310
  --name <text> Filter by name
311
311
  --offset <n> Pagination offset (default: 0)
312
- --limit <n> Max items to return (default: 50)
312
+ --limit <n> Max items to return (default: 30)
313
313
  --category-id <id> Filter by category
314
314
  --custom-space-id <id> Filter by custom space
315
315
  --is-to-square <0|1> Is to square (default: 1)
@@ -326,7 +326,7 @@ Options:
326
326
  List all conversations for an agent.
327
327
 
328
328
  Options:
329
- --limit <n> Max conversations to return
329
+ --limit <n> Max conversations to return (default: 30)
330
330
  -bd, --biz-domain <value> Business domain (default: bd_public)
331
331
  --pretty Pretty-print JSON output (default)`);
332
332
  return 0;
@@ -339,7 +339,7 @@ Options:
339
339
  Show message history for a conversation.
340
340
 
341
341
  Options:
342
- --limit <n> Max messages to return
342
+ --limit <n> Max messages to return (default: 30)
343
343
  -bd, --biz-domain <value> Business domain (default: bd_public)
344
344
  --pretty Pretty-print JSON output (default)`);
345
345
  return 0;
@@ -462,7 +462,7 @@ List published agents from the agent-factory API.
462
462
  Options:
463
463
  --name <text> Filter by name
464
464
  --offset <n> Pagination offset (default: 0)
465
- --limit <n> Max items to return (default: 50)
465
+ --limit <n> Max items to return (default: 30)
466
466
  --category-id <id> Filter by category
467
467
  --custom-space-id <id> Filter by custom space
468
468
  --is-to-square <0|1> Is to square (default: 1)
@@ -509,7 +509,7 @@ async function runAgentSessionsCommand(args) {
509
509
  List all conversations for an agent.
510
510
 
511
511
  Options:
512
- --limit <n> Max conversations to return
512
+ --limit <n> Max conversations to return (default: 30)
513
513
  -bd, --biz-domain <value> Business domain (default: bd_public)
514
514
  --pretty Pretty-print JSON output (default)`);
515
515
  return 0;
@@ -546,7 +546,7 @@ async function runAgentHistoryCommand(args) {
546
546
  Show message history for a conversation.
547
547
 
548
548
  Options:
549
- --limit <n> Max messages to return
549
+ --limit <n> Max messages to return (default: 30)
550
550
  -bd, --biz-domain <value> Business domain (default: bd_public)
551
551
  --pretty Pretty-print JSON output (default)`);
552
552
  return 0;
@@ -1,11 +1,12 @@
1
- import { clearPlatformSession, deletePlatform, getConfigDir, getCurrentPlatform, getPlatformAlias, hasPlatform, listPlatforms, loadTokenConfig, resolvePlatformIdentifier, setCurrentPlatform, setPlatformAlias, } from "../config/store.js";
2
- import { formatHttpError, normalizeBaseUrl, oauth2Login, playwrightLogin, } from "../auth/oauth.js";
1
+ import { autoSelectBusinessDomain, clearPlatformSession, deletePlatform, getConfigDir, getCurrentPlatform, getPlatformAlias, hasPlatform, listPlatforms, loadClientConfig, loadTokenConfig, resolvePlatformIdentifier, setCurrentPlatform, setPlatformAlias, } from "../config/store.js";
2
+ import { buildCopyCommand, formatHttpError, normalizeBaseUrl, oauth2Login, playwrightLogin, refreshTokenLogin, } from "../auth/oauth.js";
3
3
  export async function runAuthCommand(args) {
4
4
  const target = args[0];
5
5
  const rest = args.slice(1);
6
6
  if (!target || target === "--help" || target === "-h") {
7
7
  console.log(`kweaver auth login <url> [options] Login to a platform (browser OAuth2 by default)
8
8
  kweaver auth <url> Login (shorthand; same options as login)
9
+ kweaver auth export [url|alias] [--json] Export credentials; run printed command on a headless host
9
10
  kweaver auth status [url|alias] Show current auth status
10
11
  kweaver auth list List saved platforms
11
12
  kweaver auth use <url|alias> Switch active platform
@@ -18,6 +19,9 @@ Login options:
18
19
  Use the platform's web app client ID to get the same permissions
19
20
  as the browser. Find it in DevTools: /oauth2/auth?client_id=<id>
20
21
  --client-secret <s> Client secret (omit for public/PKCE clients)
22
+ --refresh-token <t> Use on a machine without a browser: exchange refresh token for access token.
23
+ Requires --client-id and --client-secret.
24
+ Get these from the callback page after browser login or \`auth export\`.
21
25
  -u, --username Username (with -p triggers Playwright headless login)
22
26
  -p, --password Password
23
27
  --playwright Force Playwright browser login even without -u/-p
@@ -26,7 +30,7 @@ Login options:
26
30
  }
27
31
  if (target === "login") {
28
32
  if (rest[0] === "--help" || rest[0] === "-h") {
29
- console.log(`kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]`);
33
+ console.log(`kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright] [--refresh-token T --client-id ID --client-secret S]`);
30
34
  return 0;
31
35
  }
32
36
  const url = rest[0];
@@ -36,7 +40,11 @@ Login options:
36
40
  }
37
41
  return runAuthCommand([url, ...rest.slice(1)]);
38
42
  }
39
- if (target && target !== "status" && target !== "list" && target !== "use" && target !== "delete" && target !== "logout") {
43
+ if (target === "export") {
44
+ return runAuthExportCommand(rest);
45
+ }
46
+ const LOGIN_SUBCOMMANDS = new Set(["status", "list", "use", "delete", "logout", "export"]);
47
+ if (target && !LOGIN_SUBCOMMANDS.has(target)) {
40
48
  try {
41
49
  const normalizedTarget = normalizeBaseUrl(target);
42
50
  const alias = readOption(args, "--alias");
@@ -45,9 +53,21 @@ Login options:
45
53
  const usePlaywright = args.includes("--playwright");
46
54
  const clientId = readOption(args, "--client-id");
47
55
  const clientSecret = readOption(args, "--client-secret");
56
+ const refreshToken = readOption(args, "--refresh-token");
48
57
  const tlsInsecure = args.includes("--insecure") || args.includes("-k");
49
58
  let token;
50
- if (username && password) {
59
+ if (refreshToken) {
60
+ if (!clientId || !clientSecret) {
61
+ console.error("--refresh-token requires --client-id and --client-secret.\n");
62
+ console.error("Get these values from the callback page after a browser login or `kweaver auth export`.");
63
+ return 1;
64
+ }
65
+ console.log("Logging in with refresh token (no browser)...");
66
+ token = await refreshTokenLogin(normalizedTarget, {
67
+ clientId, clientSecret, refreshToken, tlsInsecure,
68
+ });
69
+ }
70
+ else if (username && password) {
51
71
  // Headless Playwright login with credentials
52
72
  console.log("Logging in (headless)...");
53
73
  token = await playwrightLogin(normalizedTarget, { username, password, tlsInsecure });
@@ -95,6 +115,10 @@ Login options:
95
115
  if (token.expiresAt) {
96
116
  console.log(`Token expires at: ${token.expiresAt}`);
97
117
  }
118
+ const selectedBd = await autoSelectBusinessDomain(normalizedTarget, token.accessToken, {
119
+ tlsInsecure: token.tlsInsecure,
120
+ });
121
+ console.log(`Business domain: ${selectedBd}`);
98
122
  return 0;
99
123
  }
100
124
  catch (error) {
@@ -213,7 +237,7 @@ Login options:
213
237
  return 0;
214
238
  }
215
239
  console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
216
- console.error(" kweaver auth <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
240
+ console.error(" kweaver auth export [platform-url|alias] [--json]");
217
241
  console.error(" kweaver auth status [platform-url|alias]");
218
242
  console.error(" kweaver auth list");
219
243
  console.error(" kweaver auth use <platform-url|alias>");
@@ -221,6 +245,60 @@ Login options:
221
245
  console.error(" kweaver auth delete <platform-url|alias>");
222
246
  return 1;
223
247
  }
248
+ async function runAuthExportCommand(args) {
249
+ if (args[0] === "--help" || args[0] === "-h") {
250
+ console.log(`kweaver auth export [platform-url|alias] [--json]
251
+
252
+ Export OAuth2 credentials for copying to a headless host (no browser there).
253
+ Prints clientId, clientSecret, refreshToken, and a command to run on that machine.
254
+
255
+ Options:
256
+ --json Output as JSON (machine-readable)`);
257
+ return 0;
258
+ }
259
+ const jsonOutput = args.includes("--json");
260
+ const positional = args.find((a) => !a.startsWith("-"));
261
+ const resolved = positional ? resolvePlatformIdentifier(positional) : null;
262
+ const platform = resolved && /^https?:\/\//.test(resolved) ? normalizeBaseUrl(resolved) : resolved ?? getCurrentPlatform();
263
+ if (!platform) {
264
+ console.error("No active platform. Run `kweaver auth login <platform-url>` first.");
265
+ return 1;
266
+ }
267
+ const client = loadClientConfig(platform);
268
+ const token = loadTokenConfig(platform);
269
+ const clientId = client?.clientId ?? "";
270
+ const clientSecret = client?.clientSecret ?? "";
271
+ const refreshToken = token?.refreshToken ?? "";
272
+ const tlsInsecure = token?.tlsInsecure;
273
+ if (!clientId || !refreshToken) {
274
+ console.error(`Incomplete credentials for ${platform}.\n` +
275
+ (!clientId ? " Missing: client registration (client.json)\n" : "") +
276
+ (!refreshToken ? " Missing: refresh token (token.json)\n" : "") +
277
+ `Run \`kweaver auth login ${platform}\` first.`);
278
+ return 1;
279
+ }
280
+ if (jsonOutput) {
281
+ console.log(JSON.stringify({
282
+ baseUrl: platform,
283
+ clientId,
284
+ clientSecret,
285
+ refreshToken,
286
+ ...(tlsInsecure ? { tlsInsecure: true } : {}),
287
+ }));
288
+ return 0;
289
+ }
290
+ const cmd = buildCopyCommand(platform, clientId, clientSecret, refreshToken, tlsInsecure);
291
+ console.log(`Platform: ${platform}`);
292
+ console.log(`Client ID: ${clientId}`);
293
+ console.log(`Client Secret: ${clientSecret || "(none)"}`);
294
+ console.log(`Refresh Token: ${refreshToken}`);
295
+ console.log("");
296
+ console.log("On a machine without a browser, run:\n");
297
+ console.log(` ${cmd}`);
298
+ console.log("");
299
+ console.log("Keep these credentials secure.");
300
+ return 0;
301
+ }
224
302
  function readOption(args, name) {
225
303
  const index = args.findIndex((arg) => arg === name);
226
304
  if (index === -1) {