@kweaver-ai/kweaver-sdk 0.4.5 → 0.4.7

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/bin/kweaver.js CHANGED
@@ -1,9 +1,24 @@
1
1
  #!/usr/bin/env node
2
+
3
+ function exit(code) {
4
+ if (process.stdout.writableNeedDrain || process.stderr.writableNeedDrain) {
5
+ const done = () => {
6
+ if (!process.stdout.writableNeedDrain && !process.stderr.writableNeedDrain) {
7
+ process.exit(code);
8
+ }
9
+ };
10
+ process.stdout.once("drain", done);
11
+ process.stderr.once("drain", done);
12
+ } else {
13
+ process.exit(code);
14
+ }
15
+ }
16
+
2
17
  import("../dist/cli.js").then(({ run }) => {
3
18
  run(process.argv.slice(2))
4
- .then((code) => process.exit(code))
19
+ .then((code) => exit(code))
5
20
  .catch((err) => {
6
21
  console.error(err instanceof Error ? err.message : String(err));
7
- process.exit(1);
22
+ exit(1);
8
23
  });
9
24
  });
@@ -1,5 +1,21 @@
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;
@@ -1,11 +1,169 @@
1
- import { getCurrentPlatform, loadClientConfig, 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
4
  /** Seconds before access token expiry to trigger refresh (matches Python ConfigAuth). */
5
5
  const REFRESH_THRESHOLD_SEC = 60;
6
+ const DEFAULT_REDIRECT_PORT = 9010;
7
+ const DEFAULT_SCOPE = "openid offline all";
6
8
  export function normalizeBaseUrl(value) {
7
9
  return value.replace(/\/+$/, "");
8
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
+ */
9
167
  export async function playwrightLogin(baseUrl, options) {
10
168
  let chromium;
11
169
  try {
package/dist/cli.js CHANGED
@@ -87,14 +87,28 @@ export async function run(argv) {
87
87
  printHelp();
88
88
  return 1;
89
89
  }
90
+ function safeExit(code) {
91
+ if (process.stdout.writableNeedDrain || process.stderr.writableNeedDrain) {
92
+ const done = () => {
93
+ if (!process.stdout.writableNeedDrain && !process.stderr.writableNeedDrain) {
94
+ process.exit(code);
95
+ }
96
+ };
97
+ process.stdout.once("drain", done);
98
+ process.stderr.once("drain", done);
99
+ }
100
+ else {
101
+ process.exit(code);
102
+ }
103
+ }
90
104
  if (import.meta.url === `file://${process.argv[1]}`) {
91
105
  run(process.argv.slice(2))
92
106
  .then((code) => {
93
- process.exit(code);
107
+ safeExit(code);
94
108
  })
95
109
  .catch((error) => {
96
110
  const message = error instanceof Error ? error.message : String(error);
97
111
  console.error(message);
98
- process.exit(1);
112
+ safeExit(1);
99
113
  });
100
114
  }
@@ -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
  }
@@ -456,6 +456,57 @@ function parseJsonObject(text, errorMessage) {
456
456
  }
457
457
  return parsed;
458
458
  }
459
+ const MAX_OUTPUT_BYTES = 100_000;
460
+ /**
461
+ * If a query response exceeds MAX_OUTPUT_BYTES, trim the datas array
462
+ * to fit, preserving valid JSON and the search_after cursor for pagination.
463
+ */
464
+ function truncateQueryResult(raw) {
465
+ if (raw.length <= MAX_OUTPUT_BYTES) {
466
+ return raw;
467
+ }
468
+ let parsed;
469
+ try {
470
+ parsed = JSON.parse(raw);
471
+ }
472
+ catch {
473
+ return raw;
474
+ }
475
+ const datas = parsed.datas;
476
+ if (!Array.isArray(datas) || datas.length === 0) {
477
+ return raw;
478
+ }
479
+ const originalCount = datas.length;
480
+ while (datas.length > 1) {
481
+ datas.pop();
482
+ const candidate = JSON.stringify(parsed);
483
+ if (candidate.length <= MAX_OUTPUT_BYTES) {
484
+ const remaining = originalCount - datas.length;
485
+ const sa = parsed.search_after;
486
+ parsed._truncated = {
487
+ returned: datas.length,
488
+ total_fetched: originalCount,
489
+ remaining,
490
+ next_search_after: sa ?? null,
491
+ hint: sa
492
+ ? `Pass --search-after '${JSON.stringify(sa)}' --limit ${datas.length} to fetch the next page.`
493
+ : `Reduce --limit to ${datas.length} or less to avoid truncation.`,
494
+ };
495
+ console.error(`[warn] Truncated ${originalCount} → ${datas.length} records (output exceeded ${Math.round(MAX_OUTPUT_BYTES / 1024)}KB). ${parsed._truncated.hint}`);
496
+ return JSON.stringify(parsed);
497
+ }
498
+ }
499
+ const sa = parsed.search_after;
500
+ parsed._truncated = {
501
+ returned: 1,
502
+ total_fetched: originalCount,
503
+ remaining: originalCount - 1,
504
+ next_search_after: sa ?? null,
505
+ hint: `Single record is very large. Use --limit 1 and --search-after to iterate.`,
506
+ };
507
+ console.error(`[warn] Truncated ${originalCount} → 1 record. Single record is very large. Use --limit 1 and --search-after to iterate.`);
508
+ return JSON.stringify(parsed);
509
+ }
459
510
  function parseSearchAfterArray(text) {
460
511
  let parsed;
461
512
  try {
@@ -528,7 +579,7 @@ export function parseKnObjectTypeQueryArgs(args) {
528
579
  body.search_after = searchAfter;
529
580
  }
530
581
  if (typeof body.limit !== "number" || !Number.isFinite(body.limit) || body.limit < 1) {
531
- throw new Error("Missing limit. Provide it in body JSON or via --limit <n>.");
582
+ body.limit = 30;
532
583
  }
533
584
  if (!businessDomain)
534
585
  businessDomain = resolveBusinessDomain();
@@ -916,7 +967,8 @@ kweaver bkn object-type properties <kn-id> <ot-id> '<json>' [--pretty] [-bd valu
916
967
  list: List object types (schema) from ontology-manager.
917
968
  get: Get single object type details.
918
969
  create/update/delete: Schema CRUD (create requires dataview-id).
919
- query/properties: Query via ontology-query API. For query, --limit and --search-after are merged into the JSON body.
970
+ query: Query via ontology-query API. Default limit is 30 if not specified. Use --search-after for pagination.
971
+ properties: Query instance properties by primary key.
920
972
 
921
973
  properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"properties":["prop1","prop2"]}`);
922
974
  return 0;
@@ -1016,7 +1068,7 @@ properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"p
1016
1068
  body: options.body,
1017
1069
  businessDomain: options.businessDomain,
1018
1070
  });
1019
- console.log(formatCallOutput(result, options.pretty));
1071
+ console.log(formatCallOutput(truncateQueryResult(result), options.pretty));
1020
1072
  return 0;
1021
1073
  }
1022
1074
  if (action === "properties") {
@@ -1343,6 +1395,9 @@ Query subgraph via ontology-query API. JSON body format see references/json-form
1343
1395
  businessDomain,
1344
1396
  queryType,
1345
1397
  });
1398
+ if (result.length > 100_000) {
1399
+ console.error(`[warn] Response is ${(result.length / 1024).toFixed(0)}KB. Consider narrowing the subgraph query.`);
1400
+ }
1346
1401
  console.log(formatCallOutput(result, pretty));
1347
1402
  return 0;
1348
1403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kweaver-ai/kweaver-sdk",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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",