@kweaver-ai/kweaver-sdk 0.4.4 → 0.4.6

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.
@@ -254,7 +254,8 @@ export async function sendChatRequest(options) {
254
254
  };
255
255
  if (verbose) {
256
256
  console.error(`POST ${url}`);
257
- console.error(`Headers: ${JSON.stringify(headers)}`);
257
+ const safeHeaders = Object.fromEntries(Object.entries(headers).map(([k, v]) => k.toLowerCase() === "authorization" ? [k, "Bearer ***"] : [k, v]));
258
+ console.error(`Headers: ${JSON.stringify(safeHeaders)}`);
258
259
  console.error(`Body: ${JSON.stringify(body)}`);
259
260
  }
260
261
  let response;
@@ -312,7 +313,8 @@ export async function sendChatRequestStream(options, callbacks) {
312
313
  };
313
314
  if (verbose) {
314
315
  console.error(`POST ${url}`);
315
- console.error(`Headers: ${JSON.stringify(headers)}`);
316
+ const safeHeaders = Object.fromEntries(Object.entries(headers).map(([k, v]) => k.toLowerCase() === "authorization" ? [k, "Bearer ***"] : [k, v]));
317
+ console.error(`Headers: ${JSON.stringify(safeHeaders)}`);
316
318
  console.error(`Body: ${JSON.stringify(body)}`);
317
319
  }
318
320
  let response;
@@ -1,5 +1,6 @@
1
1
  import { fetchTextOrThrow } from "../utils/http.js";
2
2
  const MCP_PROTOCOL_VERSION = "2024-11-05";
3
+ const SESSION_TTL_MS = 300_000; // 5 minutes
3
4
  const sessionCache = new Map();
4
5
  function sessionKey(options) {
5
6
  return `${options.mcpUrl}:${options.knId}`;
@@ -20,8 +21,11 @@ function buildHeaders(options, sessionId) {
20
21
  async function ensureSession(options) {
21
22
  const key = sessionKey(options);
22
23
  const cached = sessionCache.get(key);
24
+ if (cached && Date.now() - cached.createdAt < SESSION_TTL_MS)
25
+ return cached.id;
26
+ // Remove stale entry if expired
23
27
  if (cached)
24
- return cached;
28
+ sessionCache.delete(key);
25
29
  const initBody = JSON.stringify({
26
30
  jsonrpc: "2.0",
27
31
  id: 1,
@@ -50,7 +54,7 @@ async function ensureSession(options) {
50
54
  headers: buildHeaders(options, sessionId),
51
55
  body: initNotifBody,
52
56
  });
53
- sessionCache.set(key, sessionId);
57
+ sessionCache.set(key, { id: sessionId, createdAt: Date.now() });
54
58
  return sessionId;
55
59
  }
56
60
  function isMissingInputParams(obj) {
@@ -30,7 +30,7 @@ export async function listConversations(opts) {
30
30
  }
31
31
  const body = await response.text();
32
32
  if (!response.ok) {
33
- return "[]";
33
+ throw new Error(`listConversations failed: HTTP ${response.status} ${response.statusText} — ${body.slice(0, 200)}`);
34
34
  }
35
35
  return body || "[]";
36
36
  }
@@ -58,7 +58,7 @@ export async function listMessages(opts) {
58
58
  }
59
59
  const body = await response.text();
60
60
  if (!response.ok) {
61
- return "[]";
61
+ throw new Error(`listMessages failed: HTTP ${response.status} ${response.statusText} — ${body.slice(0, 200)}`);
62
62
  }
63
63
  return body || "[]";
64
64
  }
@@ -204,7 +204,8 @@ export async function scanMetadata(options) {
204
204
  const scanResult = await scanResponse.json();
205
205
  const taskId = scanResult.id ?? "";
206
206
  for (let i = 0; i < 30; i += 1) {
207
- await new Promise((r) => setTimeout(r, 2000));
207
+ const delay = Math.min(2000 * Math.pow(1.5, i), 15000);
208
+ await new Promise((r) => setTimeout(r, delay));
208
209
  const statusResponse = await fetch(statusUrl(taskId), {
209
210
  method: "GET",
210
211
  headers: buildHeaders(accessToken, businessDomain),
@@ -1,11 +1,40 @@
1
1
  import { type TokenConfig } from "../config/store.js";
2
2
  export declare function normalizeBaseUrl(value: string): string;
3
+ /**
4
+ * OAuth2 Authorization Code login flow.
5
+ * 1. Register client (if not already registered)
6
+ * 2. Open browser to /oauth2/auth
7
+ * 3. Receive authorization code via local HTTP callback
8
+ * 4. Exchange code for access_token + refresh_token
9
+ * 5. Save token.json + client.json to ~/.kweaver/
10
+ */
11
+ export declare function oauth2Login(baseUrl: string, options?: {
12
+ port?: number;
13
+ scope?: string;
14
+ }): Promise<TokenConfig>;
15
+ /**
16
+ * Playwright cookie login (legacy fallback).
17
+ * Does NOT produce a refresh_token — token expires in 1 hour with no auto-refresh.
18
+ */
3
19
  export declare function playwrightLogin(baseUrl: string, options?: {
4
20
  username?: string;
5
21
  password?: string;
6
22
  }): Promise<TokenConfig>;
23
+ /**
24
+ * Exchange refresh_token for a new access token (OAuth2 password grant style, same as Python ConfigAuth).
25
+ * Persists the new token to ~/.kweaver/ and returns it.
26
+ */
27
+ export declare function refreshAccessToken(token: TokenConfig): Promise<TokenConfig>;
7
28
  export declare function ensureValidToken(opts?: {
8
29
  forceRefresh?: boolean;
9
30
  }): Promise<TokenConfig>;
31
+ /**
32
+ * Run an operation; on HTTP 401, refresh the access token once and retry.
33
+ * Does not call `ensureValidToken` first — use for CLI routers so `--help` works without login.
34
+ */
35
+ export declare function with401RefreshRetry<T>(fn: () => Promise<T>): Promise<T>;
36
+ /**
37
+ * Load a valid token, run `fn(token)`, and on 401 refresh once and retry with the new token.
38
+ */
10
39
  export declare function withTokenRetry<T>(fn: (token: TokenConfig) => Promise<T>): Promise<T>;
11
40
  export declare function formatHttpError(error: unknown): string;
@@ -1,9 +1,169 @@
1
- import { getCurrentPlatform, loadTokenConfig, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
1
+ import { getCurrentPlatform, loadClientConfig, loadTokenConfig, saveClientConfig, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
2
2
  import { HttpError, NetworkRequestError } from "../utils/http.js";
3
3
  const TOKEN_TTL_SECONDS = 3600;
4
+ /** Seconds before access token expiry to trigger refresh (matches Python ConfigAuth). */
5
+ const REFRESH_THRESHOLD_SEC = 60;
6
+ const DEFAULT_REDIRECT_PORT = 9010;
7
+ const DEFAULT_SCOPE = "openid offline all";
4
8
  export function normalizeBaseUrl(value) {
5
9
  return value.replace(/\/+$/, "");
6
10
  }
11
+ /**
12
+ * OAuth2 Authorization Code login flow.
13
+ * 1. Register client (if not already registered)
14
+ * 2. Open browser to /oauth2/auth
15
+ * 3. Receive authorization code via local HTTP callback
16
+ * 4. Exchange code for access_token + refresh_token
17
+ * 5. Save token.json + client.json to ~/.kweaver/
18
+ */
19
+ export async function oauth2Login(baseUrl, options) {
20
+ const { createServer } = await import("node:http");
21
+ const { randomBytes } = await import("node:crypto");
22
+ const base = normalizeBaseUrl(baseUrl);
23
+ const port = options?.port ?? DEFAULT_REDIRECT_PORT;
24
+ const scope = options?.scope ?? DEFAULT_SCOPE;
25
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
26
+ // Step 1: Ensure registered client
27
+ let client = loadClientConfig(base);
28
+ if (!client?.clientId) {
29
+ client = await registerOAuth2Client(base, redirectUri, scope);
30
+ saveClientConfig(base, client);
31
+ }
32
+ // Step 2: Generate CSRF state
33
+ const state = randomBytes(12).toString("hex");
34
+ // Step 3: Build authorization URL
35
+ const authParams = new URLSearchParams({
36
+ redirect_uri: redirectUri,
37
+ "x-forwarded-prefix": "",
38
+ client_id: client.clientId,
39
+ scope,
40
+ response_type: "code",
41
+ state,
42
+ lang: "zh-cn",
43
+ product: "adp",
44
+ });
45
+ const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
46
+ // Step 4: Start local callback server, wait for code
47
+ const code = await new Promise((resolve, reject) => {
48
+ const timeoutId = setTimeout(() => {
49
+ server.close();
50
+ reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
51
+ }, 120_000);
52
+ const server = createServer((req, res) => {
53
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
54
+ if (url.pathname === "/callback") {
55
+ const receivedState = url.searchParams.get("state");
56
+ const receivedCode = url.searchParams.get("code");
57
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
58
+ res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
59
+ clearTimeout(timeoutId);
60
+ server.close();
61
+ if (receivedState !== state) {
62
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
63
+ }
64
+ else if (!receivedCode) {
65
+ reject(new Error("No authorization code received in callback."));
66
+ }
67
+ else {
68
+ resolve(receivedCode);
69
+ }
70
+ }
71
+ else {
72
+ res.writeHead(404);
73
+ res.end();
74
+ }
75
+ });
76
+ server.listen(port, "127.0.0.1", () => {
77
+ // Step 5: Open browser
78
+ import("node:child_process").then(({ exec }) => {
79
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
80
+ exec(`${cmd} "${authUrl}"`);
81
+ });
82
+ });
83
+ });
84
+ // Step 6: Exchange code for tokens
85
+ const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri);
86
+ setCurrentPlatform(base);
87
+ return token;
88
+ }
89
+ async function registerOAuth2Client(baseUrl, redirectUri, scope) {
90
+ const logoutUri = redirectUri.replace("/callback", "/successful-logout");
91
+ const response = await fetch(`${baseUrl}/oauth2/clients`, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
94
+ body: JSON.stringify({
95
+ client_name: "kweaver-sdk",
96
+ grant_types: ["authorization_code", "implicit", "refresh_token"],
97
+ response_types: ["token id_token", "code", "token"],
98
+ scope: "openid offline all",
99
+ redirect_uris: [redirectUri],
100
+ post_logout_redirect_uris: [logoutUri],
101
+ metadata: {
102
+ device: {
103
+ name: "kweaver-sdk",
104
+ client_type: "web",
105
+ description: "KWeaver TypeScript SDK",
106
+ },
107
+ },
108
+ }),
109
+ });
110
+ const text = await response.text();
111
+ if (!response.ok) {
112
+ throw new HttpError(response.status, response.statusText, text);
113
+ }
114
+ const data = JSON.parse(text);
115
+ return {
116
+ baseUrl,
117
+ clientId: data.client_id,
118
+ clientSecret: data.client_secret,
119
+ redirectUri,
120
+ logoutRedirectUri: logoutUri,
121
+ scope,
122
+ lang: "zh-cn",
123
+ product: "adp",
124
+ xForwardedPrefix: "",
125
+ };
126
+ }
127
+ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redirectUri) {
128
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
129
+ const response = await fetch(`${baseUrl}/oauth2/token`, {
130
+ method: "POST",
131
+ headers: {
132
+ Authorization: `Basic ${credentials}`,
133
+ "Content-Type": "application/x-www-form-urlencoded",
134
+ Accept: "application/json",
135
+ },
136
+ body: new URLSearchParams({
137
+ grant_type: "authorization_code",
138
+ code,
139
+ redirect_uri: redirectUri,
140
+ }).toString(),
141
+ });
142
+ const text = await response.text();
143
+ if (!response.ok) {
144
+ throw new HttpError(response.status, response.statusText, text);
145
+ }
146
+ const data = JSON.parse(text);
147
+ const now = new Date();
148
+ const expiresIn = typeof data.expires_in === "number" ? data.expires_in : 3600;
149
+ const token = {
150
+ baseUrl,
151
+ accessToken: data.access_token,
152
+ tokenType: data.token_type ?? "Bearer",
153
+ scope: data.scope ?? "",
154
+ expiresIn,
155
+ expiresAt: new Date(now.getTime() + expiresIn * 1000).toISOString(),
156
+ refreshToken: data.refresh_token ?? "",
157
+ idToken: data.id_token ?? "",
158
+ obtainedAt: now.toISOString(),
159
+ };
160
+ saveTokenConfig(token);
161
+ return token;
162
+ }
163
+ /**
164
+ * Playwright cookie login (legacy fallback).
165
+ * Does NOT produce a refresh_token — token expires in 1 hour with no auto-refresh.
166
+ */
7
167
  export async function playwrightLogin(baseUrl, options) {
8
168
  let chromium;
9
169
  try {
@@ -80,6 +240,85 @@ export async function playwrightLogin(baseUrl, options) {
80
240
  await browser.close();
81
241
  }
82
242
  }
243
+ function tokenNeedsRefresh(token) {
244
+ if (!token.expiresAt) {
245
+ return false;
246
+ }
247
+ const expiresAtMs = Date.parse(token.expiresAt);
248
+ if (Number.isNaN(expiresAtMs)) {
249
+ return false;
250
+ }
251
+ const thresholdMs = REFRESH_THRESHOLD_SEC * 1000;
252
+ return expiresAtMs - thresholdMs <= Date.now();
253
+ }
254
+ /**
255
+ * Exchange refresh_token for a new access token (OAuth2 password grant style, same as Python ConfigAuth).
256
+ * Persists the new token to ~/.kweaver/ and returns it.
257
+ */
258
+ export async function refreshAccessToken(token) {
259
+ const baseUrl = normalizeBaseUrl(token.baseUrl);
260
+ const refreshToken = token.refreshToken?.trim();
261
+ if (!refreshToken) {
262
+ throw new Error(`Token expired and no refresh_token available for ${baseUrl}. Run \`kweaver auth login ${baseUrl}\` again.`);
263
+ }
264
+ const client = loadClientConfig(baseUrl);
265
+ const clientId = client?.clientId?.trim() ?? "";
266
+ const clientSecret = client?.clientSecret?.trim() ?? "";
267
+ if (!clientId || !clientSecret) {
268
+ throw new Error(`Token refresh requires OAuth client credentials (client.json). Run \`kweaver auth login ${baseUrl}\` again.`);
269
+ }
270
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
271
+ const url = `${baseUrl}/oauth2/token`;
272
+ const body = new URLSearchParams({
273
+ grant_type: "refresh_token",
274
+ refresh_token: refreshToken,
275
+ });
276
+ let response;
277
+ try {
278
+ response = await fetch(url, {
279
+ method: "POST",
280
+ headers: {
281
+ Authorization: `Basic ${credentials}`,
282
+ "Content-Type": "application/x-www-form-urlencoded",
283
+ Accept: "application/json",
284
+ },
285
+ body: body.toString(),
286
+ });
287
+ }
288
+ catch (cause) {
289
+ const hint = cause instanceof Error ? cause.message : String(cause);
290
+ throw new NetworkRequestError("POST", url, hint, "Check network connectivity and that the platform exposes /oauth2/token.");
291
+ }
292
+ const text = await response.text();
293
+ if (!response.ok) {
294
+ throw new HttpError(response.status, response.statusText, text);
295
+ }
296
+ let data;
297
+ try {
298
+ data = JSON.parse(text);
299
+ }
300
+ catch {
301
+ throw new Error(`Invalid JSON from ${url} during token refresh.`);
302
+ }
303
+ if (typeof data.access_token !== "string") {
304
+ throw new Error(`Token refresh response missing access_token from ${url}.`);
305
+ }
306
+ const now = new Date();
307
+ const expiresIn = typeof data.expires_in === "number" ? data.expires_in : 3600;
308
+ const newToken = {
309
+ baseUrl,
310
+ accessToken: data.access_token,
311
+ tokenType: data.token_type ?? "Bearer",
312
+ scope: data.scope ?? token.scope ?? "",
313
+ expiresIn,
314
+ expiresAt: new Date(now.getTime() + expiresIn * 1000).toISOString(),
315
+ refreshToken: data.refresh_token ?? refreshToken,
316
+ idToken: data.id_token ?? token.idToken ?? "",
317
+ obtainedAt: now.toISOString(),
318
+ };
319
+ saveTokenConfig(newToken);
320
+ return newToken;
321
+ }
83
322
  export async function ensureValidToken(opts) {
84
323
  const envToken = process.env.KWEAVER_TOKEN;
85
324
  const envBaseUrl = process.env.KWEAVER_BASE_URL;
@@ -97,21 +336,61 @@ export async function ensureValidToken(opts) {
97
336
  if (!currentPlatform) {
98
337
  throw new Error("No active platform selected. Run `kweaver auth login <platform-url>` first.");
99
338
  }
100
- if (opts?.forceRefresh) {
101
- throw new Error(`Token refresh is not supported. Run \`kweaver auth login ${currentPlatform}\` again.`);
102
- }
103
- const token = loadTokenConfig(currentPlatform);
339
+ let token = loadTokenConfig(currentPlatform);
104
340
  if (!token) {
105
341
  throw new Error(`No saved token for ${currentPlatform}. Run \`kweaver auth login ${currentPlatform}\` first.`);
106
342
  }
107
- if (token.expiresAt) {
108
- const expiresAtMs = Date.parse(token.expiresAt);
109
- if (!Number.isNaN(expiresAtMs) && expiresAtMs - 60_000 <= Date.now()) {
110
- throw new Error(`Access token expired. Run \`kweaver auth login ${currentPlatform}\` again.`);
343
+ if (opts?.forceRefresh) {
344
+ return refreshAccessToken(token);
345
+ }
346
+ if (tokenNeedsRefresh(token)) {
347
+ try {
348
+ return await refreshAccessToken(token);
349
+ }
350
+ catch (err) {
351
+ throw new Error(`Access token expired or near expiry and refresh failed for ${currentPlatform}.\n` +
352
+ (err instanceof Error ? `${err.message}\n` : "") +
353
+ `Run \`kweaver auth login ${currentPlatform}\` again.`, { cause: err });
111
354
  }
112
355
  }
113
356
  return token;
114
357
  }
358
+ /**
359
+ * Run an operation; on HTTP 401, refresh the access token once and retry.
360
+ * Does not call `ensureValidToken` first — use for CLI routers so `--help` works without login.
361
+ */
362
+ export async function with401RefreshRetry(fn) {
363
+ try {
364
+ return await fn();
365
+ }
366
+ catch (error) {
367
+ if (error instanceof HttpError && error.status === 401) {
368
+ const currentPlatform = getCurrentPlatform();
369
+ if (!currentPlatform) {
370
+ throw error;
371
+ }
372
+ const platformUrl = normalizeBaseUrl(currentPlatform);
373
+ const latest = loadTokenConfig(platformUrl);
374
+ if (!latest) {
375
+ throw error;
376
+ }
377
+ try {
378
+ await refreshAccessToken(latest);
379
+ }
380
+ catch (retryErr) {
381
+ const oauthHint = formatOAuthErrorBody(retryErr instanceof HttpError ? retryErr.body : "");
382
+ const extra = oauthHint ? `\n\n${oauthHint}` : "";
383
+ throw new Error(`Authentication failed (401). Token refresh did not succeed for ${platformUrl}.${extra}\n` +
384
+ `Run \`kweaver auth login ${platformUrl}\` again.`, { cause: retryErr });
385
+ }
386
+ return await fn();
387
+ }
388
+ throw error;
389
+ }
390
+ }
391
+ /**
392
+ * Load a valid token, run `fn(token)`, and on 401 refresh once and retry with the new token.
393
+ */
115
394
  export async function withTokenRetry(fn) {
116
395
  const token = await ensureValidToken();
117
396
  try {
@@ -119,9 +398,18 @@ export async function withTokenRetry(fn) {
119
398
  }
120
399
  catch (error) {
121
400
  if (error instanceof HttpError && error.status === 401) {
122
- const platform = token.baseUrl;
123
- throw new Error(`Authentication failed (401). Token may be expired or revoked.\n` +
124
- `Run \`kweaver auth login ${platform}\` again.`);
401
+ const platformUrl = normalizeBaseUrl(token.baseUrl);
402
+ const latest = loadTokenConfig(platformUrl) ?? token;
403
+ try {
404
+ const refreshed = await refreshAccessToken(latest);
405
+ return await fn(refreshed);
406
+ }
407
+ catch (retryErr) {
408
+ const oauthHint = formatOAuthErrorBody(retryErr instanceof HttpError ? retryErr.body : "");
409
+ const extra = oauthHint ? `\n\n${oauthHint}` : "";
410
+ throw new Error(`Authentication failed (401). Token refresh did not succeed for ${platformUrl}.${extra}\n` +
411
+ `Run \`kweaver auth login ${platformUrl}\` again.`, { cause: retryErr });
412
+ }
125
413
  }
126
414
  throw error;
127
415
  }
@@ -1,5 +1,4 @@
1
- import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
2
- import { HttpError } from "../utils/http.js";
1
+ import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
3
2
  import { runAgentChatCommand } from "./agent-chat.js";
4
3
  import { listAgents, getAgent, getAgentByKey, createAgent, updateAgent, deleteAgent, publishAgent, unpublishAgent, } from "../api/agent-list.js";
5
4
  import { listConversations, listMessages } from "../api/conversations.js";
@@ -347,24 +346,16 @@ Options:
347
346
  }
348
347
  }
349
348
  try {
350
- const code = await dispatch();
351
- if (code === -1) {
352
- console.error(`Unknown agent subcommand: ${subcommand}`);
353
- return 1;
354
- }
355
- return code;
356
- }
357
- catch (error) {
358
- if (error instanceof HttpError && error.status === 401) {
359
- try {
360
- await ensureValidToken({ forceRefresh: true });
361
- return await dispatch();
362
- }
363
- catch (retryError) {
364
- console.error(formatHttpError(retryError));
349
+ return await with401RefreshRetry(async () => {
350
+ const code = await dispatch();
351
+ if (code === -1) {
352
+ console.error(`Unknown agent subcommand: ${subcommand}`);
365
353
  return 1;
366
354
  }
367
- }
355
+ return code;
356
+ });
357
+ }
358
+ catch (error) {
368
359
  console.error(formatHttpError(error));
369
360
  return 1;
370
361
  }
@@ -1,5 +1,5 @@
1
1
  import { clearPlatformSession, deletePlatform, getConfigDir, getCurrentPlatform, getPlatformAlias, hasPlatform, listPlatforms, loadTokenConfig, resolvePlatformIdentifier, setCurrentPlatform, setPlatformAlias, } from "../config/store.js";
2
- import { formatHttpError, normalizeBaseUrl, playwrightLogin, } from "../auth/oauth.js";
2
+ import { formatHttpError, normalizeBaseUrl, oauth2Login, playwrightLogin, } from "../auth/oauth.js";
3
3
  export async function runAuthCommand(args) {
4
4
  const target = args[0];
5
5
  const rest = args.slice(1);
@@ -27,13 +27,23 @@ kweaver auth delete <url> Delete saved credentials`);
27
27
  const alias = readOption(args, "--alias");
28
28
  const username = readOption(args, "--username") ?? readOption(args, "-u");
29
29
  const password = readOption(args, "--password") ?? readOption(args, "-p");
30
+ const usePlaywright = args.includes("--playwright");
31
+ let token;
30
32
  if (username && password) {
33
+ // Headless Playwright login with credentials
31
34
  console.log("Logging in (headless)...");
35
+ token = await playwrightLogin(normalizedTarget, { username, password });
36
+ }
37
+ else if (usePlaywright) {
38
+ // Explicit Playwright fallback
39
+ console.log("Opening browser for login (Playwright)...");
40
+ token = await playwrightLogin(normalizedTarget);
32
41
  }
33
42
  else {
34
- console.log("Opening browser for login...");
43
+ // Default: OAuth2 authorization code flow (supports refresh_token)
44
+ console.log("Opening browser for OAuth2 login...");
45
+ token = await oauth2Login(normalizedTarget);
35
46
  }
36
- const token = await playwrightLogin(normalizedTarget, username && password ? { username, password } : undefined);
37
47
  if (alias) {
38
48
  setPlatformAlias(normalizedTarget, alias);
39
49
  }
@@ -49,6 +59,12 @@ kweaver auth delete <url> Delete saved credentials`);
49
59
  }
50
60
  console.log(`Current platform: ${normalizedTarget}`);
51
61
  console.log(`Access token saved: yes`);
62
+ if (token.refreshToken) {
63
+ console.log(`Refresh token: yes (auto-refresh enabled)`);
64
+ }
65
+ else {
66
+ console.log(`Refresh token: no (token will expire in 1 hour)`);
67
+ }
52
68
  if (token.expiresAt) {
53
69
  console.log(`Token expires at: ${token.expiresAt}`);
54
70
  }
@@ -79,6 +95,7 @@ kweaver auth delete <url> Delete saved credentials`);
79
95
  `Current platform: ${token.baseUrl === currentPlatform ? "yes" : "no"}`,
80
96
  `Token present: yes`,
81
97
  ];
98
+ lines.push(`Refresh token: ${token.refreshToken ? "yes (auto-refresh enabled)" : "no"}`);
82
99
  if (token.expiresAt) {
83
100
  const expiry = new Date(token.expiresAt);
84
101
  const remainingMs = expiry.getTime() - Date.now();
@@ -86,6 +103,9 @@ kweaver auth delete <url> Delete saved credentials`);
86
103
  const remainingMin = Math.ceil(remainingMs / 60_000);
87
104
  lines.push(`Token status: active (expires in ${remainingMin} min)`);
88
105
  }
106
+ else if (token.refreshToken) {
107
+ lines.push(`Token status: expired (will auto-refresh on next command)`);
108
+ }
89
109
  else {
90
110
  lines.push(`Token status: expired (run \`kweaver auth login ${token.baseUrl}\` again)`);
91
111
  }
@@ -3,8 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { loadNetwork, allObjects, allRelations, allActions, generateChecksum, validateNetwork } from "@kweaver-ai/bkn";
6
- import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
7
- import { HttpError } from "../utils/http.js";
6
+ import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
8
7
  import { listKnowledgeNetworks, getKnowledgeNetwork, createKnowledgeNetwork, updateKnowledgeNetwork, deleteKnowledgeNetwork, listObjectTypes, listRelationTypes, listActionTypes, getObjectType, createObjectTypes, updateObjectType, deleteObjectTypes, getRelationType, createRelationTypes, updateRelationType, deleteRelationTypes, buildKnowledgeNetwork, getBuildStatus, } from "../api/knowledge-networks.js";
9
8
  import { objectTypeQuery, objectTypeProperties, subgraph, actionTypeQuery, actionTypeExecute, actionExecutionGet, actionLogsList, actionLogGet, actionLogCancel, } from "../api/ontology-query.js";
10
9
  import { semanticSearch } from "../api/semantic-search.js";
@@ -529,7 +528,7 @@ export function parseKnObjectTypeQueryArgs(args) {
529
528
  body.search_after = searchAfter;
530
529
  }
531
530
  if (typeof body.limit !== "number" || !Number.isFinite(body.limit) || body.limit < 1) {
532
- throw new Error("Missing limit. Provide it in body JSON or via --limit <n>.");
531
+ body.limit = 30;
533
532
  }
534
533
  if (!businessDomain)
535
534
  businessDomain = resolveBusinessDomain();
@@ -627,26 +626,16 @@ export async function runKnCommand(args) {
627
626
  return Promise.resolve(-1);
628
627
  };
629
628
  try {
630
- const code = await dispatch();
631
- if (code === -1) {
632
- console.error(`Unknown bkn subcommand: ${subcommand}`);
633
- return 1;
634
- }
635
- return code;
636
- }
637
- catch (error) {
638
- // Auto-retry on 401: force-refresh the token and re-run the subcommand.
639
- // The subcommand will call ensureValidToken() again and pick up the fresh token.
640
- if (error instanceof HttpError && error.status === 401) {
641
- try {
642
- await ensureValidToken({ forceRefresh: true });
643
- return await dispatch();
644
- }
645
- catch (retryError) {
646
- console.error(formatHttpError(retryError));
629
+ return await with401RefreshRetry(async () => {
630
+ const code = await dispatch();
631
+ if (code === -1) {
632
+ console.error(`Unknown bkn subcommand: ${subcommand}`);
647
633
  return 1;
648
634
  }
649
- }
635
+ return code;
636
+ });
637
+ }
638
+ catch (error) {
650
639
  console.error(formatHttpError(error));
651
640
  return 1;
652
641
  }
@@ -927,7 +916,8 @@ kweaver bkn object-type properties <kn-id> <ot-id> '<json>' [--pretty] [-bd valu
927
916
  list: List object types (schema) from ontology-manager.
928
917
  get: Get single object type details.
929
918
  create/update/delete: Schema CRUD (create requires dataview-id).
930
- query/properties: Query via ontology-query API. For query, --limit and --search-after are merged into the JSON body.
919
+ query: Query via ontology-query API. Default limit is 30 if not specified. Use --search-after for pagination.
920
+ properties: Query instance properties by primary key.
931
921
 
932
922
  properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"properties":["prop1","prop2"]}`);
933
923
  return 0;
@@ -1027,6 +1017,10 @@ properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"p
1027
1017
  body: options.body,
1028
1018
  businessDomain: options.businessDomain,
1029
1019
  });
1020
+ const OUTPUT_WARN_BYTES = 100_000;
1021
+ if (result.length > OUTPUT_WARN_BYTES) {
1022
+ console.error(`[warn] Response is ${(result.length / 1024).toFixed(0)}KB. Use a smaller --limit or --search-after to paginate.`);
1023
+ }
1030
1024
  console.log(formatCallOutput(result, options.pretty));
1031
1025
  return 0;
1032
1026
  }
@@ -1354,6 +1348,9 @@ Query subgraph via ontology-query API. JSON body format see references/json-form
1354
1348
  businessDomain,
1355
1349
  queryType,
1356
1350
  });
1351
+ if (result.length > 100_000) {
1352
+ console.error(`[warn] Response is ${(result.length / 1024).toFixed(0)}KB. Consider narrowing the subgraph query.`);
1353
+ }
1357
1354
  console.log(formatCallOutput(result, pretty));
1358
1355
  return 0;
1359
1356
  }
@@ -1,4 +1,4 @@
1
- import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
1
+ import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
2
2
  import { HttpError } from "../utils/http.js";
3
3
  import { resolveBusinessDomain } from "../config/store.js";
4
4
  export function parseCallArgs(args) {
@@ -168,19 +168,9 @@ Options:
168
168
  return 0;
169
169
  };
170
170
  try {
171
- return await execute();
171
+ return await with401RefreshRetry(async () => execute());
172
172
  }
173
173
  catch (error) {
174
- if (error instanceof HttpError && error.status === 401) {
175
- try {
176
- await ensureValidToken({ forceRefresh: true });
177
- return await execute();
178
- }
179
- catch (retryError) {
180
- console.error(formatHttpError(retryError));
181
- return 1;
182
- }
183
- }
184
174
  console.error(formatHttpError(error));
185
175
  return 1;
186
176
  }
@@ -1,5 +1,4 @@
1
- import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
2
- import { HttpError } from "../utils/http.js";
1
+ import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
3
2
  import { knSearch, knSchemaSearch, queryObjectInstance, queryInstanceSubgraph, getLogicPropertiesValues, getActionInfo, listTools, listResources, readResource, listResourceTemplates, listPrompts, getPrompt, } from "../api/context-loader.js";
4
3
  import { addContextLoaderEntry, getCurrentContextLoaderKn, getCurrentPlatform, loadContextLoaderConfig, removeContextLoaderEntry, setCurrentContextLoader, } from "../config/store.js";
5
4
  const MCP_NOT_CONFIGURED = "Context-loader MCP is not configured. Run: kweaver context-loader config set --kn-id <kn-id>";
@@ -92,24 +91,16 @@ Examples:
92
91
  return -1;
93
92
  };
94
93
  try {
95
- const code = await dispatch();
96
- if (code === -1) {
97
- console.error(`Unknown context-loader subcommand: ${subcommand}`);
98
- return 1;
99
- }
100
- return code;
101
- }
102
- catch (error) {
103
- if (error instanceof HttpError && error.status === 401) {
104
- try {
105
- await ensureValidToken({ forceRefresh: true });
106
- return await dispatch();
107
- }
108
- catch (retryError) {
109
- console.error(formatHttpError(retryError));
94
+ return await with401RefreshRetry(async () => {
95
+ const code = await dispatch();
96
+ if (code === -1) {
97
+ console.error(`Unknown context-loader subcommand: ${subcommand}`);
110
98
  return 1;
111
99
  }
112
- }
100
+ return code;
101
+ });
102
+ }
103
+ catch (error) {
113
104
  console.error(formatHttpError(error));
114
105
  return 1;
115
106
  }
@@ -1,6 +1,5 @@
1
1
  import { createInterface } from "node:readline";
2
- import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
3
- import { HttpError } from "../utils/http.js";
2
+ import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
4
3
  import { testDatasource, createDatasource, listDatasources, getDatasource, deleteDatasource, listTablesWithColumns, } from "../api/datasources.js";
5
4
  import { formatCallOutput } from "./call.js";
6
5
  import { resolveBusinessDomain } from "../config/store.js";
@@ -50,24 +49,16 @@ Subcommands:
50
49
  return Promise.resolve(-1);
51
50
  };
52
51
  try {
53
- const code = await dispatch();
54
- if (code === -1) {
55
- console.error(`Unknown ds subcommand: ${subcommand}`);
56
- return 1;
57
- }
58
- return code;
59
- }
60
- catch (error) {
61
- if (error instanceof HttpError && error.status === 401) {
62
- try {
63
- await ensureValidToken({ forceRefresh: true });
64
- return await dispatch();
65
- }
66
- catch (retryError) {
67
- console.error(formatHttpError(retryError));
52
+ return await with401RefreshRetry(async () => {
53
+ const code = await dispatch();
54
+ if (code === -1) {
55
+ console.error(`Unknown ds subcommand: ${subcommand}`);
68
56
  return 1;
69
57
  }
70
- }
58
+ return code;
59
+ });
60
+ }
61
+ catch (error) {
71
62
  console.error(formatHttpError(error));
72
63
  return 1;
73
64
  }
@@ -1,5 +1,4 @@
1
- import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
2
- import { HttpError } from "../utils/http.js";
1
+ import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
3
2
  import { vegaHealth, listVegaCatalogs, getVegaCatalog, vegaCatalogHealthStatus, testVegaCatalogConnection, discoverVegaCatalog, listVegaCatalogResources, listVegaResources, getVegaResource, queryVegaResourceData, previewVegaResource, listVegaConnectorTypes, getVegaConnectorType, listVegaDiscoverTasks, } from "../api/vega.js";
4
3
  import { formatCallOutput } from "./call.js";
5
4
  import { resolveBusinessDomain } from "../config/store.js";
@@ -78,24 +77,16 @@ export async function runVegaCommand(args) {
78
77
  return Promise.resolve(-1);
79
78
  };
80
79
  try {
81
- const code = await dispatch();
82
- if (code === -1) {
83
- console.error(`Unknown vega subcommand: ${subcommand}`);
84
- return 1;
85
- }
86
- return code;
87
- }
88
- catch (error) {
89
- if (error instanceof HttpError && error.status === 401) {
90
- try {
91
- await ensureValidToken({ forceRefresh: true });
92
- return await dispatch();
93
- }
94
- catch (retryError) {
95
- console.error(formatHttpError(retryError));
80
+ return await with401RefreshRetry(async () => {
81
+ const code = await dispatch();
82
+ if (code === -1) {
83
+ console.error(`Unknown vega subcommand: ${subcommand}`);
96
84
  return 1;
97
85
  }
98
- }
86
+ return code;
87
+ });
88
+ }
89
+ catch (error) {
99
90
  console.error(formatHttpError(error));
100
91
  return 1;
101
92
  }
@@ -9,6 +9,18 @@ export interface TokenConfig {
9
9
  idToken?: string;
10
10
  obtainedAt: string;
11
11
  }
12
+ /** OAuth2 client registration (per platform), used for refresh_token grant. */
13
+ export interface ClientConfig {
14
+ baseUrl: string;
15
+ clientId: string;
16
+ clientSecret: string;
17
+ redirectUri?: string;
18
+ logoutRedirectUri?: string;
19
+ scope?: string;
20
+ lang?: string;
21
+ product?: string;
22
+ xForwardedPrefix?: string;
23
+ }
12
24
  /** Single context-loader entry (named kn_id). */
13
25
  export interface ContextLoaderEntry {
14
26
  name: string;
@@ -34,6 +46,8 @@ export declare function getPlatformAlias(baseUrl: string): string | null;
34
46
  export declare function resolvePlatformIdentifier(value: string): string | null;
35
47
  export declare function loadTokenConfig(baseUrl?: string): TokenConfig | null;
36
48
  export declare function saveTokenConfig(config: TokenConfig): void;
49
+ export declare function loadClientConfig(baseUrl?: string): ClientConfig | null;
50
+ export declare function saveClientConfig(baseUrl: string, config: ClientConfig): void;
37
51
  export declare function loadContextLoaderConfig(baseUrl?: string): ContextLoaderConfig | null;
38
52
  export declare function saveContextLoaderConfig(baseUrl: string, config: ContextLoaderConfig): void;
39
53
  export interface CurrentContextLoaderKn {
@@ -188,6 +188,19 @@ export function saveTokenConfig(config) {
188
188
  ensurePlatformDir(config.baseUrl);
189
189
  writeJsonFile(getPlatformFile(config.baseUrl, "token.json"), config);
190
190
  }
191
+ export function loadClientConfig(baseUrl) {
192
+ ensureStoreReady();
193
+ const targetBaseUrl = baseUrl ?? getCurrentPlatform();
194
+ if (!targetBaseUrl) {
195
+ return null;
196
+ }
197
+ return readJsonFile(getPlatformFile(targetBaseUrl, "client.json"));
198
+ }
199
+ export function saveClientConfig(baseUrl, config) {
200
+ ensureStoreReady();
201
+ ensurePlatformDir(baseUrl);
202
+ writeJsonFile(getPlatformFile(baseUrl, "client.json"), { ...config, baseUrl });
203
+ }
191
204
  function migrateLegacyContextLoader(raw) {
192
205
  const leg = raw;
193
206
  if (leg?.knId && !Array.isArray(raw.configs)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kweaver-ai/kweaver-sdk",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "KWeaver TypeScript SDK — CLI tool and programmatic API for knowledge networks and Decision Agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",