@kweaver-ai/kweaver-sdk 0.4.7 → 0.4.9

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.
@@ -13,12 +13,21 @@ export declare function oauth2Login(baseUrl: string, options?: {
13
13
  scope?: string;
14
14
  }): Promise<TokenConfig>;
15
15
  /**
16
- * Playwright cookie login (legacy fallback).
17
- * Does NOT produce a refresh_token — token expires in 1 hour with no auto-refresh.
16
+ * Playwright-automated OAuth2 login.
17
+ *
18
+ * Uses the full OAuth2 authorization code flow (same as `oauth2Login`) but
19
+ * automates the browser interaction with Playwright. This produces a
20
+ * refresh_token so the CLI can auto-refresh without re-login.
21
+ *
22
+ * When `username` and `password` are provided the browser runs headless and
23
+ * fills the login form automatically. Otherwise it opens a visible browser
24
+ * window for manual login (same UX as the old cookie-based flow).
18
25
  */
19
26
  export declare function playwrightLogin(baseUrl: string, options?: {
20
27
  username?: string;
21
28
  password?: string;
29
+ port?: number;
30
+ scope?: string;
22
31
  }): Promise<TokenConfig>;
23
32
  /**
24
33
  * Exchange refresh_token for a new access token (OAuth2 password grant style, same as Python ConfigAuth).
@@ -74,10 +74,9 @@ export async function oauth2Login(baseUrl, options) {
74
74
  }
75
75
  });
76
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}"`);
77
+ // Step 5: Open browser (uses spawn with proper Windows quoting)
78
+ import("../utils/browser.js").then(({ openBrowser }) => {
79
+ openBrowser(authUrl);
81
80
  });
82
81
  });
83
82
  });
@@ -161,10 +160,19 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
161
160
  return token;
162
161
  }
163
162
  /**
164
- * Playwright cookie login (legacy fallback).
165
- * Does NOT produce a refresh_token — token expires in 1 hour with no auto-refresh.
163
+ * Playwright-automated OAuth2 login.
164
+ *
165
+ * Uses the full OAuth2 authorization code flow (same as `oauth2Login`) but
166
+ * automates the browser interaction with Playwright. This produces a
167
+ * refresh_token so the CLI can auto-refresh without re-login.
168
+ *
169
+ * When `username` and `password` are provided the browser runs headless and
170
+ * fills the login form automatically. Otherwise it opens a visible browser
171
+ * window for manual login (same UX as the old cookie-based flow).
166
172
  */
167
173
  export async function playwrightLogin(baseUrl, options) {
174
+ const { createServer } = await import("node:http");
175
+ const { randomBytes } = await import("node:crypto");
168
176
  let chromium;
169
177
  try {
170
178
  const modName = "playwright";
@@ -174,71 +182,94 @@ export async function playwrightLogin(baseUrl, options) {
174
182
  catch {
175
183
  throw new Error("Playwright is not installed. Run:\n npm install playwright && npx playwright install chromium");
176
184
  }
177
- const hasCredentials = options?.username && options?.password;
178
- const browser = await chromium.launch({ headless: hasCredentials ? true : false });
179
- try {
180
- const context = await browser.newContext();
181
- const page = await context.newPage();
182
- await page.goto(`${baseUrl}/api/dip-hub/v1/login`, {
183
- waitUntil: "networkidle",
184
- timeout: 30_000,
185
- });
186
- if (hasCredentials) {
187
- // Headless mode: auto-fill credentials
188
- await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
189
- await page.fill('input[name="account"]', options.username);
190
- await page.fill('input[name="password"]', options.password);
191
- await page.click("button.ant-btn-primary");
192
- }
193
- // else: headed mode — user logs in manually in the browser window
194
- const TIMEOUT_SECONDS = hasCredentials ? 30 : 120;
195
- let accessToken = null;
196
- for (let i = 0; i < TIMEOUT_SECONDS; i++) {
197
- await new Promise((r) => setTimeout(r, 1000));
198
- // Check cookies (works even after navigation)
199
- for (const cookie of await context.cookies()) {
200
- if (cookie.name === "dip.oauth2_token") {
201
- accessToken = decodeURIComponent(cookie.value);
202
- break;
185
+ const base = normalizeBaseUrl(baseUrl);
186
+ const port = options?.port ?? DEFAULT_REDIRECT_PORT;
187
+ const scope = options?.scope ?? DEFAULT_SCOPE;
188
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
189
+ const hasCredentials = !!(options?.username && options?.password);
190
+ // Step 1: Ensure registered OAuth2 client
191
+ let client = loadClientConfig(base);
192
+ if (!client?.clientId) {
193
+ client = await registerOAuth2Client(base, redirectUri, scope);
194
+ saveClientConfig(base, client);
195
+ }
196
+ // Step 2: Generate CSRF state
197
+ const state = randomBytes(12).toString("hex");
198
+ // Step 3: Build authorization URL
199
+ const authParams = new URLSearchParams({
200
+ redirect_uri: redirectUri,
201
+ "x-forwarded-prefix": "",
202
+ client_id: client.clientId,
203
+ scope,
204
+ response_type: "code",
205
+ state,
206
+ lang: "zh-cn",
207
+ product: "adp",
208
+ });
209
+ const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
210
+ // Step 4: Start local callback server to capture the authorization code
211
+ const code = await new Promise((resolve, reject) => {
212
+ const TIMEOUT_MS = hasCredentials ? 30_000 : 120_000;
213
+ const timeoutId = setTimeout(() => {
214
+ server.close();
215
+ browser?.close();
216
+ reject(new Error(`OAuth2 login timed out (${TIMEOUT_MS / 1000}s). No authorization code received.`));
217
+ }, TIMEOUT_MS);
218
+ const server = createServer((req, res) => {
219
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
220
+ if (url.pathname === "/callback") {
221
+ const receivedState = url.searchParams.get("state");
222
+ const receivedCode = url.searchParams.get("code");
223
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
224
+ res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
225
+ clearTimeout(timeoutId);
226
+ server.close();
227
+ browser?.close();
228
+ if (receivedState !== state) {
229
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
203
230
  }
204
- }
205
- if (accessToken)
206
- break;
207
- // In headless mode, check for login error messages
208
- if (hasCredentials) {
209
- try {
210
- const errorEl = await page.$(".ant-message-error, .ant-alert-error");
211
- if (errorEl) {
212
- const errorText = await errorEl.textContent();
213
- throw new Error(`Login failed: ${errorText?.trim() || "unknown error"}`);
214
- }
231
+ else if (!receivedCode) {
232
+ reject(new Error("No authorization code received in callback."));
215
233
  }
216
- catch (e) {
217
- if (e instanceof Error && e.message.startsWith("Login failed:"))
218
- throw e;
234
+ else {
235
+ resolve(receivedCode);
219
236
  }
220
237
  }
221
- }
222
- if (!accessToken) {
223
- throw new Error(`Login timed out: dip.oauth2_token cookie not received within ${TIMEOUT_SECONDS} seconds.`);
224
- }
225
- const now = new Date();
226
- const tokenConfig = {
227
- baseUrl,
228
- accessToken,
229
- tokenType: "bearer",
230
- scope: "",
231
- expiresIn: TOKEN_TTL_SECONDS,
232
- expiresAt: new Date(now.getTime() + TOKEN_TTL_SECONDS * 1000).toISOString(),
233
- obtainedAt: now.toISOString(),
234
- };
235
- saveTokenConfig(tokenConfig);
236
- setCurrentPlatform(baseUrl);
237
- return tokenConfig;
238
- }
239
- finally {
240
- await browser.close();
241
- }
238
+ else {
239
+ res.writeHead(404);
240
+ res.end();
241
+ }
242
+ });
243
+ let browser;
244
+ server.listen(port, "127.0.0.1", async () => {
245
+ try {
246
+ browser = await chromium.launch({ headless: hasCredentials });
247
+ const context = await browser.newContext();
248
+ const page = await context.newPage();
249
+ // Navigate to OAuth2 auth URL — redirects to signin page
250
+ await page.goto(authUrl, { waitUntil: "networkidle", timeout: 30_000 });
251
+ if (hasCredentials) {
252
+ // Auto-fill credentials
253
+ await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
254
+ await page.fill('input[name="account"]', options.username);
255
+ await page.fill('input[name="password"]', options.password);
256
+ await page.click("button.ant-btn-primary");
257
+ }
258
+ // else: visible browser — user logs in manually
259
+ // The OAuth2 callback will fire when login completes, resolving the promise above
260
+ }
261
+ catch (err) {
262
+ clearTimeout(timeoutId);
263
+ server.close();
264
+ browser?.close();
265
+ reject(err);
266
+ }
267
+ });
268
+ });
269
+ // Step 5: Exchange authorization code for tokens (includes refresh_token)
270
+ const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri);
271
+ setCurrentPlatform(base);
272
+ return token;
242
273
  }
243
274
  function tokenNeedsRefresh(token) {
244
275
  if (!token.expiresAt) {
package/dist/cli.js CHANGED
@@ -11,47 +11,100 @@ function printHelp() {
11
11
  console.log(`kweaver
12
12
 
13
13
  Usage:
14
- kweaver auth <platform-url>
15
- kweaver auth login <platform-url>
16
- kweaver auth <platform-url> [--alias name] [--no-open] [--host host] [--redirect-uri uri]
17
- kweaver auth status [platform-url]
14
+ kweaver --version | -V
15
+ kweaver --help | -h
16
+
17
+ kweaver auth <platform-url> [--alias name] [-u user] [-p pass] [--playwright]
18
+ kweaver auth login <platform-url> (alias for auth <url>)
19
+ kweaver auth status [platform-url|alias]
18
20
  kweaver auth list
19
- kweaver auth use <platform-url>
20
- kweaver auth logout [platform-url]
21
- kweaver auth delete <platform-url>
21
+ kweaver auth use <platform-url|alias>
22
+ kweaver auth logout [platform-url|alias]
23
+ kweaver auth delete <platform-url|alias>
22
24
  kweaver token
23
- kweaver call <url> [-X METHOD] [-H "Name: value"] [-d BODY] [--pretty] [--verbose] [-bd value]
24
- kweaver agent chat <agent_id> [-m "message"] [--version value] [--conversation-id id] [--stream] [--no-stream] [--verbose] [-bd value]
25
- kweaver agent list [options]
26
- kweaver agent get <agent_id> [options]
27
- kweaver ds list [options]
25
+
26
+ kweaver call <url> [-X METHOD] [-H "Name: value"] [-d BODY] [--data-raw BODY]
27
+ [--url URL] [--pretty] [--verbose] [-bd value]
28
+ (alias: kweaver curl ...)
29
+
30
+ kweaver agent chat <agent_id> [-m "message"] [--version value] [--conversation-id id]
31
+ [--stream] [--no-stream] [--verbose] [-bd value]
32
+ kweaver agent list [--name X] [--limit N] [--offset N] [-bd value] [--pretty]
33
+ kweaver agent get <agent_id> [-bd value] [--pretty]
34
+ kweaver agent get-by-key <key> [-bd value] [--pretty]
35
+ kweaver agent sessions <agent_id> [-bd value] [--limit N] [--pretty]
36
+ kweaver agent history <agent_id> <session_id> [-bd value] [--limit N] [--pretty]
37
+ kweaver agent create [options]
38
+ kweaver agent update <agent_id> [options]
39
+ kweaver agent delete <agent_id> [-bd value]
40
+ kweaver agent publish <agent_id> [-bd value]
41
+ kweaver agent unpublish <agent_id> [-bd value]
42
+
43
+ kweaver ds list [--keyword X] [--type T] [-bd value] [--pretty]
28
44
  kweaver ds get <id>
29
- kweaver ds connect <db_type> <host> <port> <database> --account X --password Y
45
+ kweaver ds delete <id> [-y]
46
+ kweaver ds tables <id> [--keyword X] [--pretty]
47
+ kweaver ds connect <db_type> <host> <port> <database> --account X --password Y [--schema S] [--name N]
48
+
30
49
  kweaver bkn list [options]
31
50
  kweaver bkn get <kn-id> [options]
32
- kweaver bkn search <kn-id> <query> [options]
51
+ kweaver bkn search <kn-id> <query> [--max-concepts N] [--mode M] [--pretty] [-bd value]
33
52
  kweaver bkn create [options]
53
+ kweaver bkn create-from-ds [options]
34
54
  kweaver bkn update <kn-id> [options]
35
- kweaver bkn delete <kn-id>
36
- kweaver config [set-bd|show]
37
- kweaver vega [health|stats|inspect|catalog|resource|connector-type]
38
- kweaver context-loader [config|kn-search|...]
39
- kweaver --help
55
+ kweaver bkn delete <kn-id> [-y]
56
+ kweaver bkn build <kn-id> [--wait] [--no-wait] [--timeout N]
57
+ kweaver bkn validate <directory> [--detect-encoding|--no-detect-encoding] [--source-encoding name]
58
+ kweaver bkn export <kn-id>
59
+ kweaver bkn stats <kn-id>
60
+ kweaver bkn push <directory> [--branch main] [-bd value] [--detect-encoding|--no-detect-encoding] [--source-encoding name]
61
+ kweaver bkn pull <kn-id> [directory] [--branch main] [-bd value]
62
+ kweaver bkn object-type list|get|create|update|delete|query|properties <kn-id> ...
63
+ kweaver bkn relation-type list|get|create|update|delete <kn-id> ...
64
+ kweaver bkn subgraph <kn-id> <body-json>
65
+ kweaver bkn action-type list|query|execute <kn-id> ... [--wait] [--no-wait] [--timeout N]
66
+ kweaver bkn action-execution get <kn-id> <execution-id>
67
+ kweaver bkn action-log list|get|cancel <kn-id> ...
68
+
69
+ kweaver config set-bd <value>
70
+ kweaver config show
71
+
72
+ kweaver vega health|stats|inspect
73
+ kweaver vega catalog list|get|health|test-connection|discover|resources [options]
74
+ kweaver vega resource list|get|query|preview [options]
75
+ kweaver vega connector-type list|get [options]
76
+
77
+ kweaver context-loader config set|use|list|remove|show [options]
78
+ kweaver context-loader tools|resources|templates|prompts [--cursor]
79
+ kweaver context-loader resource <uri>
80
+ kweaver context-loader prompt <name> [--args json]
81
+ kweaver context-loader kn-search <query> [--only-schema]
82
+ kweaver context-loader kn-schema-search <query> [--max N]
83
+ kweaver context-loader query-object-instance|query-instance-subgraph|get-logic-properties|get-action-info ...
84
+ (alias: kweaver context ...)
40
85
 
41
86
  Commands:
42
87
  auth Login, list, inspect, and switch saved platform auth profiles
43
88
  token Print the current access token, refreshing it first if needed
44
- call Call an API with curl-style flags and auto-injected token headers
89
+ call (curl) Call an API with curl-style flags and auto-injected token headers
90
+ agent Agent CRUD, chat, sessions, history, publish/unpublish
45
91
  ds Manage datasources (list, get, delete, tables, connect)
46
- agent Chat with a KWeaver agent (agent chat <id>), list published agents (agent list)
47
- bkn Business knowledge network (list/get/create/update/delete/export/stats; object-type, subgraph, action-type, action-log)
92
+ bkn Knowledge network (CRUD, build, validate, export, stats, push/pull,
93
+ object-type, relation-type, subgraph, action-type, action-execution, action-log)
48
94
  config Per-platform configuration (business domain)
49
- vega Vega observability platform (catalogs, resources, connector-types, health)
50
- context-loader Call context-loader MCP (tools, resources, prompts; kn-search, query-*, etc.)
95
+ vega Vega observability (catalog, resource, connector-type, health/stats/inspect)
96
+ context-loader Context-loader MCP (config, tools, resources, prompts, kn-search, query-*, etc.)
51
97
  help Show this message`);
52
98
  }
53
99
  export async function run(argv) {
54
100
  const [command, ...rest] = argv;
101
+ if (command === "--version" || command === "-V" || command === "version") {
102
+ const { createRequire } = await import("node:module");
103
+ const require = createRequire(import.meta.url);
104
+ const pkg = require("../package.json");
105
+ console.log(pkg.version);
106
+ return 0;
107
+ }
55
108
  if (argv.length === 0 || !command || command === "--help" || command === "-h" || command === "help") {
56
109
  printHelp();
57
110
  return 0;
@@ -101,7 +154,9 @@ function safeExit(code) {
101
154
  process.exit(code);
102
155
  }
103
156
  }
104
- if (import.meta.url === `file://${process.argv[1]}`) {
157
+ import { fileURLToPath } from "node:url";
158
+ import { resolve } from "node:path";
159
+ if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
105
160
  run(process.argv.slice(2))
106
161
  .then((code) => {
107
162
  safeExit(code);
@@ -1,3 +1,4 @@
1
+ import { type BknEncodingImportOptions } from "../utils/bkn-encoding.js";
1
2
  export interface KnListOptions {
2
3
  offset: number;
3
4
  limit: number;
@@ -46,6 +47,7 @@ export interface KnPushOptions {
46
47
  branch: string;
47
48
  businessDomain: string;
48
49
  pretty: boolean;
50
+ encodingOptions: BknEncodingImportOptions;
49
51
  }
50
52
  export declare function parseKnPushArgs(args: string[]): KnPushOptions;
51
53
  export interface KnPullOptions {
@@ -64,6 +66,42 @@ export interface KnObjectTypeQueryOptions {
64
66
  }
65
67
  export declare function parseKnObjectTypeQueryArgs(args: string[]): KnObjectTypeQueryOptions;
66
68
  export declare function runKnCommand(args: string[]): Promise<number>;
69
+ /** Fields merged via GET → modify → PUT (not raw body mode). */
70
+ export interface ObjectTypeMergeFields {
71
+ name?: string;
72
+ displayKey?: string;
73
+ addProperties: Record<string, unknown>[];
74
+ removeProperties: string[];
75
+ tags?: string[];
76
+ comment?: string;
77
+ icon?: string;
78
+ color?: string;
79
+ }
80
+ export type ObjectTypeUpdateParsed = {
81
+ mode: "body";
82
+ knId: string;
83
+ otId: string;
84
+ body: string;
85
+ businessDomain: string;
86
+ pretty: boolean;
87
+ } | {
88
+ mode: "merge";
89
+ knId: string;
90
+ otId: string;
91
+ merge: ObjectTypeMergeFields;
92
+ businessDomain: string;
93
+ pretty: boolean;
94
+ branch: string;
95
+ };
96
+ /** Prepare a GET response entry for PUT (drop read-only fields). */
97
+ export declare function stripObjectTypeForPut(entry: Record<string, unknown>): Record<string, unknown>;
98
+ /**
99
+ * Apply merge flags onto a stripped object-type object (mutates copy).
100
+ * - Add: property `name` not in list → append.
101
+ * - Update: property `name` exists → replace entry (same as add; CLI also accepts `--update-property`).
102
+ * - Delete: `--remove-property` removes by `name` before adds are applied.
103
+ */
104
+ export declare function applyObjectTypeMerge(target: Record<string, unknown>, merge: ObjectTypeMergeFields): Record<string, unknown>;
67
105
  export interface KnActionTypeExecuteOptions {
68
106
  knId: string;
69
107
  atId: string;
@@ -3,6 +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 { prepareBknDirectoryForImport, stripBknEncodingCliArgs, } from "../utils/bkn-encoding.js";
6
7
  import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
7
8
  import { listKnowledgeNetworks, getKnowledgeNetwork, createKnowledgeNetwork, updateKnowledgeNetwork, deleteKnowledgeNetwork, listObjectTypes, listRelationTypes, listActionTypes, getObjectType, createObjectTypes, updateObjectType, deleteObjectTypes, getRelationType, createRelationTypes, updateRelationType, deleteRelationTypes, buildKnowledgeNetwork, getBuildStatus, } from "../api/knowledge-networks.js";
8
9
  import { objectTypeQuery, objectTypeProperties, subgraph, actionTypeQuery, actionTypeExecute, actionExecutionGet, actionLogsList, actionLogGet, actionLogCancel, } from "../api/ontology-query.js";
@@ -354,12 +355,13 @@ export function parseKnDeleteArgs(args) {
354
355
  return { knId, businessDomain, yes };
355
356
  }
356
357
  export function parseKnPushArgs(args) {
358
+ const { rest, options: encodingOptions } = stripBknEncodingCliArgs(args);
357
359
  let directory = "";
358
360
  let branch = "main";
359
361
  let businessDomain = "";
360
362
  let pretty = true;
361
- for (let i = 0; i < args.length; i += 1) {
362
- const arg = args[i];
363
+ for (let i = 0; i < rest.length; i += 1) {
364
+ const arg = rest[i];
363
365
  if (arg === "--help" || arg === "-h") {
364
366
  throw new Error("help");
365
367
  }
@@ -394,7 +396,7 @@ export function parseKnPushArgs(args) {
394
396
  }
395
397
  if (!businessDomain)
396
398
  businessDomain = resolveBusinessDomain();
397
- return { directory, branch, businessDomain, pretty };
399
+ return { directory, branch, businessDomain, pretty, encodingOptions };
398
400
  }
399
401
  export function parseKnPullArgs(args) {
400
402
  let knId = "";
@@ -767,12 +769,84 @@ function parseObjectTypeCreateArgs(args) {
767
769
  businessDomain = resolveBusinessDomain();
768
770
  return { knId, body, businessDomain, branch, pretty };
769
771
  }
770
- /** Parse object-type update args: --name X [--display-key Y] */
772
+ const OBJECT_TYPE_PUT_STRIP_KEYS = new Set([
773
+ "status",
774
+ "creator",
775
+ "updater",
776
+ "create_time",
777
+ "update_time",
778
+ "module_type",
779
+ "kn_id",
780
+ ]);
781
+ /** Prepare a GET response entry for PUT (drop read-only fields). */
782
+ export function stripObjectTypeForPut(entry) {
783
+ const out = { ...entry };
784
+ for (const k of OBJECT_TYPE_PUT_STRIP_KEYS) {
785
+ delete out[k];
786
+ }
787
+ return out;
788
+ }
789
+ /**
790
+ * Apply merge flags onto a stripped object-type object (mutates copy).
791
+ * - Add: property `name` not in list → append.
792
+ * - Update: property `name` exists → replace entry (same as add; CLI also accepts `--update-property`).
793
+ * - Delete: `--remove-property` removes by `name` before adds are applied.
794
+ */
795
+ export function applyObjectTypeMerge(target, merge) {
796
+ if (merge.name !== undefined)
797
+ target.name = merge.name;
798
+ if (merge.displayKey !== undefined)
799
+ target.display_key = merge.displayKey;
800
+ if (merge.comment !== undefined)
801
+ target.comment = merge.comment;
802
+ if (merge.icon !== undefined)
803
+ target.icon = merge.icon;
804
+ if (merge.color !== undefined)
805
+ target.color = merge.color;
806
+ if (merge.tags !== undefined)
807
+ target.tags = merge.tags;
808
+ let props = target.data_properties;
809
+ if (!Array.isArray(props)) {
810
+ props = [];
811
+ }
812
+ else {
813
+ props = props.map((p) => p && typeof p === "object" && !Array.isArray(p) ? { ...p } : p);
814
+ }
815
+ const list = props;
816
+ for (const rm of merge.removeProperties) {
817
+ for (let j = list.length - 1; j >= 0; j -= 1) {
818
+ const n = list[j]?.name;
819
+ if (typeof n === "string" && n === rm)
820
+ list.splice(j, 1);
821
+ }
822
+ }
823
+ for (const add of merge.addProperties) {
824
+ const nm = add.name;
825
+ if (typeof nm !== "string" || !nm) {
826
+ throw new Error("--add-property / --update-property JSON must include a non-empty string \"name\" field.");
827
+ }
828
+ const idx = list.findIndex((p) => p?.name === nm);
829
+ if (idx >= 0)
830
+ list[idx] = add;
831
+ else
832
+ list.push(add);
833
+ }
834
+ target.data_properties = list;
835
+ return target;
836
+ }
837
+ /** Parse object-type update: raw JSON body OR merge flags (GET-merge-PUT). */
771
838
  function parseObjectTypeUpdateArgs(args) {
772
839
  let name;
773
840
  let displayKey;
774
841
  let businessDomain = "";
775
842
  let pretty = true;
843
+ let branch = "main";
844
+ let comment;
845
+ let icon;
846
+ let color;
847
+ let tagsJson;
848
+ const addProperties = [];
849
+ const removeProperties = [];
776
850
  const positional = [];
777
851
  for (let i = 0; i < args.length; i += 1) {
778
852
  const arg = args[i];
@@ -786,6 +860,35 @@ function parseObjectTypeUpdateArgs(args) {
786
860
  displayKey = args[++i];
787
861
  continue;
788
862
  }
863
+ if ((arg === "--add-property" || arg === "--update-property") && args[i + 1]) {
864
+ const raw = args[++i];
865
+ addProperties.push(parseJsonObject(raw, `--add-property / --update-property must be valid JSON object: ${raw}`));
866
+ continue;
867
+ }
868
+ if (arg === "--remove-property" && args[i + 1]) {
869
+ removeProperties.push(args[++i]);
870
+ continue;
871
+ }
872
+ if (arg === "--tags" && args[i + 1]) {
873
+ tagsJson = args[++i];
874
+ continue;
875
+ }
876
+ if (arg === "--comment" && args[i + 1]) {
877
+ comment = args[++i];
878
+ continue;
879
+ }
880
+ if (arg === "--icon" && args[i + 1]) {
881
+ icon = args[++i];
882
+ continue;
883
+ }
884
+ if (arg === "--color" && args[i + 1]) {
885
+ color = args[++i];
886
+ continue;
887
+ }
888
+ if (arg === "--branch" && args[i + 1]) {
889
+ branch = args[++i];
890
+ continue;
891
+ }
789
892
  if ((arg === "-bd" || arg === "--biz-domain") && args[i + 1]) {
790
893
  businessDomain = args[++i];
791
894
  continue;
@@ -797,21 +900,72 @@ function parseObjectTypeUpdateArgs(args) {
797
900
  if (!arg.startsWith("-"))
798
901
  positional.push(arg);
799
902
  }
800
- const [knId, otId] = positional;
903
+ const [knId, otId, maybeBody] = positional;
801
904
  if (!knId || !otId) {
802
- throw new Error("Usage: kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y]");
905
+ throw new Error("Usage: kweaver bkn object-type update <kn-id> <ot-id> [ '<full-json-body>' ] [--name ...] [--add-property|--update-property '<json>' ...] [--remove-property <name> ...]");
906
+ }
907
+ const hasMergeFlags = name !== undefined ||
908
+ displayKey !== undefined ||
909
+ addProperties.length > 0 ||
910
+ removeProperties.length > 0 ||
911
+ tagsJson !== undefined ||
912
+ comment !== undefined ||
913
+ icon !== undefined ||
914
+ color !== undefined;
915
+ if (maybeBody !== undefined && maybeBody.trim().startsWith("{")) {
916
+ if (hasMergeFlags) {
917
+ throw new Error("Do not combine a raw JSON body with --name/--add-property/--update-property/--remove-property and other merge flags.");
918
+ }
919
+ if (!businessDomain)
920
+ businessDomain = resolveBusinessDomain();
921
+ return {
922
+ mode: "body",
923
+ knId,
924
+ otId,
925
+ body: maybeBody.trim(),
926
+ businessDomain,
927
+ pretty,
928
+ };
929
+ }
930
+ if (maybeBody !== undefined) {
931
+ throw new Error(`Unexpected third argument "${maybeBody}". For raw PUT body, pass a single JSON object starting with "{".`);
803
932
  }
804
- const payload = {};
805
- if (name !== undefined)
806
- payload.name = name;
807
- if (displayKey !== undefined)
808
- payload.display_key = displayKey;
809
- if (Object.keys(payload).length === 0) {
810
- throw new Error("No update fields. Use --name or --display-key.");
933
+ let tags;
934
+ if (tagsJson !== undefined) {
935
+ try {
936
+ const t = JSON.parse(tagsJson);
937
+ if (!Array.isArray(t) || !t.every((x) => typeof x === "string")) {
938
+ throw new Error("invalid");
939
+ }
940
+ tags = t;
941
+ }
942
+ catch {
943
+ throw new Error(`--tags must be a JSON array of strings, e.g. '["足球","球员"]'`);
944
+ }
945
+ }
946
+ const merge = {
947
+ addProperties,
948
+ removeProperties,
949
+ ...(name !== undefined ? { name } : {}),
950
+ ...(displayKey !== undefined ? { displayKey } : {}),
951
+ ...(tags !== undefined ? { tags } : {}),
952
+ ...(comment !== undefined ? { comment } : {}),
953
+ ...(icon !== undefined ? { icon } : {}),
954
+ ...(color !== undefined ? { color } : {}),
955
+ };
956
+ if (merge.name === undefined &&
957
+ merge.displayKey === undefined &&
958
+ merge.addProperties.length === 0 &&
959
+ merge.removeProperties.length === 0 &&
960
+ merge.tags === undefined &&
961
+ merge.comment === undefined &&
962
+ merge.icon === undefined &&
963
+ merge.color === undefined) {
964
+ throw new Error("No update fields. Use --name, --display-key, --add-property (new), --update-property (same as add; replaces by name), --remove-property, --tags, --comment, --icon, --color, or pass a full JSON object as the third argument.");
811
965
  }
812
966
  if (!businessDomain)
813
967
  businessDomain = resolveBusinessDomain();
814
- return { knId, otId, body: JSON.stringify(payload), businessDomain, pretty };
968
+ return { mode: "merge", knId, otId, merge, businessDomain, pretty, branch };
815
969
  }
816
970
  /** Parse object-type delete args: <kn-id> <ot-ids> [-y] */
817
971
  function parseObjectTypeDeleteArgs(args) {
@@ -959,14 +1113,15 @@ async function runKnObjectTypeCommand(args) {
959
1113
  console.log(`kweaver bkn object-type list <kn-id> [--pretty] [-bd value]
960
1114
  kweaver bkn object-type get <kn-id> <ot-id> [--pretty] [-bd value]
961
1115
  kweaver bkn object-type create <kn-id> --name X --dataview-id Y --primary-key Z --display-key W [--property '<json>' ...]
962
- kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y]
1116
+ kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y] [--add-property|--update-property '<json>' ...] [--remove-property N ...] [--tags '["a","b"]'] [--comment S] [--icon I] [--color C] [--branch main]
1117
+ kweaver bkn object-type update <kn-id> <ot-id> '<full-json-body>'
963
1118
  kweaver bkn object-type delete <kn-id> <ot-ids> [-y]
964
1119
  kweaver bkn object-type query <kn-id> <ot-id> ['<json>'] [--limit <n>] [--search-after '<json-array>'] [--pretty] [-bd value]
965
1120
  kweaver bkn object-type properties <kn-id> <ot-id> '<json>' [--pretty] [-bd value]
966
1121
 
967
1122
  list: List object types (schema) from ontology-manager.
968
1123
  get: Get single object type details.
969
- create/update/delete: Schema CRUD (create requires dataview-id).
1124
+ create/update/delete: Schema CRUD (create requires dataview-id). update: merge flags (--add-property / --update-property / --remove-property, etc.) GET-merge-PUT; or full JSON as third arg.
970
1125
  query: Query via ontology-query API. Default limit is 30 if not specified. Use --search-after for pagination.
971
1126
  properties: Query instance properties by primary key.
972
1127
 
@@ -1009,12 +1164,37 @@ properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"p
1009
1164
  if (action === "update") {
1010
1165
  const opts = parseObjectTypeUpdateArgs(rest);
1011
1166
  const token = await ensureValidToken();
1167
+ let putBody;
1168
+ if (opts.mode === "body") {
1169
+ putBody = opts.body;
1170
+ }
1171
+ else {
1172
+ const raw = await getObjectType({
1173
+ baseUrl: token.baseUrl,
1174
+ accessToken: token.accessToken,
1175
+ knId: opts.knId,
1176
+ otId: opts.otId,
1177
+ businessDomain: opts.businessDomain,
1178
+ branch: opts.branch,
1179
+ });
1180
+ const parsed = JSON.parse(raw);
1181
+ const entryUnknown = parsed.entries;
1182
+ const entry = Array.isArray(entryUnknown) && entryUnknown.length > 0 && entryUnknown[0] && typeof entryUnknown[0] === "object"
1183
+ ? entryUnknown[0]
1184
+ : parsed;
1185
+ if (!entry || typeof entry !== "object") {
1186
+ throw new Error("Unexpected object-type GET response shape.");
1187
+ }
1188
+ const stripped = stripObjectTypeForPut(entry);
1189
+ applyObjectTypeMerge(stripped, opts.merge);
1190
+ putBody = JSON.stringify(stripped);
1191
+ }
1012
1192
  const body = await updateObjectType({
1013
1193
  baseUrl: token.baseUrl,
1014
1194
  accessToken: token.accessToken,
1015
1195
  knId: opts.knId,
1016
1196
  otId: opts.otId,
1017
- body: opts.body,
1197
+ body: putBody,
1018
1198
  businessDomain: opts.businessDomain,
1019
1199
  });
1020
1200
  console.log(formatCallOutput(body, opts.pretty));
@@ -1097,7 +1277,8 @@ JSON: {"_instance_identities":[{"<primary-key>":"<value>"}],"properties":["prop1
1097
1277
  catch (error) {
1098
1278
  if (error instanceof Error && error.message === "help") {
1099
1279
  console.log(`kweaver bkn object-type create <kn-id> --name X --dataview-id Y --primary-key Z --display-key W [--property '<json>' ...]
1100
- kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y]
1280
+ kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y] [--add-property|--update-property '<json>' ...] [--remove-property N ...] [--tags '["a"]'] [--comment S] [--icon I] [--color C] [--branch main]
1281
+ kweaver bkn object-type update <kn-id> <ot-id> '<full-json-body>'
1101
1282
  kweaver bkn object-type delete <kn-id> <ot-ids> [-y]`);
1102
1283
  return 0;
1103
1284
  }
@@ -2264,8 +2445,13 @@ export function packDirectoryToTar(dirPath) {
2264
2445
  encoding: "buffer",
2265
2446
  env: { ...process.env, COPYFILE_DISABLE: "1" },
2266
2447
  });
2267
- if (result.error)
2448
+ if (result.error) {
2449
+ if ("code" in result.error && result.error.code === "ENOENT") {
2450
+ throw new Error("tar executable not found. On Windows, ensure tar.exe is in PATH " +
2451
+ "(ships with Windows 10 1803+) or install GNU tar via Git for Windows / scoop.");
2452
+ }
2268
2453
  throw result.error;
2454
+ }
2269
2455
  if (result.status !== 0) {
2270
2456
  throw new Error(`tar pack failed: ${result.stderr?.toString() ?? result.status}`);
2271
2457
  }
@@ -2278,6 +2464,10 @@ export function extractTarToDirectory(tarBuffer, dirPath) {
2278
2464
  input: tarBuffer,
2279
2465
  });
2280
2466
  if (result.error) {
2467
+ if ("code" in result.error && result.error.code === "ENOENT") {
2468
+ throw new Error("tar executable not found. On Windows, ensure tar.exe is in PATH " +
2469
+ "(ships with Windows 10 1803+) or install GNU tar via Git for Windows / scoop.");
2470
+ }
2281
2471
  throw result.error;
2282
2472
  }
2283
2473
  if (result.status !== 0) {
@@ -2291,7 +2481,10 @@ Pack a BKN directory into a tar and upload to import as a knowledge network.
2291
2481
  Options:
2292
2482
  --branch <s> Branch name (default: main)
2293
2483
  -bd, --biz-domain Business domain (default: bd_public)
2294
- --pretty Pretty-print JSON output`;
2484
+ --pretty Pretty-print JSON output
2485
+ --detect-encoding Detect .bkn encoding and normalize to UTF-8 (default: on)
2486
+ --no-detect-encoding Do not detect; require UTF-8 .bkn files
2487
+ --source-encoding <name> Decode all .bkn files with this encoding (e.g. gb18030); overrides detection`;
2295
2488
  const KN_PULL_HELP = `kweaver bkn pull <kn-id> [<directory>] [options]
2296
2489
 
2297
2490
  Download a BKN tar from a knowledge network and extract to a local directory.
@@ -2302,12 +2495,28 @@ Options:
2302
2495
  -bd, --biz-domain Business domain (default: bd_public)`;
2303
2496
  async function runKnValidateCommand(args) {
2304
2497
  if (args.includes("--help") || args.includes("-h")) {
2305
- console.log("Usage: kweaver bkn validate <directory>\n\nValidate a local BKN directory without uploading.");
2498
+ console.log("Usage: kweaver bkn validate <directory> [options]\n\n" +
2499
+ "Validate a local BKN directory without uploading.\n\n" +
2500
+ "Options:\n" +
2501
+ " --detect-encoding Detect .bkn encoding and normalize to UTF-8 (default: on)\n" +
2502
+ " --no-detect-encoding Require UTF-8 .bkn files\n" +
2503
+ " --source-encoding <n> Decode all .bkn with this encoding (e.g. gb18030)");
2306
2504
  return 0;
2307
2505
  }
2308
- const directory = args.find((a) => !a.startsWith("-"));
2506
+ let encodingOptions;
2507
+ let restArgs;
2508
+ try {
2509
+ const stripped = stripBknEncodingCliArgs(args);
2510
+ encodingOptions = stripped.options;
2511
+ restArgs = stripped.rest;
2512
+ }
2513
+ catch (e) {
2514
+ console.error(e instanceof Error ? e.message : String(e));
2515
+ return 1;
2516
+ }
2517
+ const directory = restArgs.find((a) => !a.startsWith("-"));
2309
2518
  if (!directory) {
2310
- console.error("Missing directory. Usage: kweaver bkn validate <directory>");
2519
+ console.error("Missing directory. Usage: kweaver bkn validate <directory> [options]");
2311
2520
  return 1;
2312
2521
  }
2313
2522
  const absDir = resolve(directory);
@@ -2325,8 +2534,9 @@ async function runKnValidateCommand(args) {
2325
2534
  }
2326
2535
  throw err;
2327
2536
  }
2537
+ const prepared = prepareBknDirectoryForImport(absDir, encodingOptions);
2328
2538
  try {
2329
- const network = await loadNetwork(absDir);
2539
+ const network = await loadNetwork(prepared.dir);
2330
2540
  const result = validateNetwork(network);
2331
2541
  if (!result.ok) {
2332
2542
  for (const e of result.errors)
@@ -2344,6 +2554,9 @@ async function runKnValidateCommand(args) {
2344
2554
  console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2345
2555
  return 1;
2346
2556
  }
2557
+ finally {
2558
+ prepared.cleanup();
2559
+ }
2347
2560
  }
2348
2561
  async function runKnPushCommand(args) {
2349
2562
  let options;
@@ -2373,41 +2586,48 @@ async function runKnPushCommand(args) {
2373
2586
  }
2374
2587
  throw err;
2375
2588
  }
2589
+ const prepared = prepareBknDirectoryForImport(absDir, options.encodingOptions);
2590
+ const workDir = prepared.dir;
2376
2591
  try {
2377
- const network = await loadNetwork(absDir);
2378
- const objs = allObjects(network);
2379
- const rels = allRelations(network);
2380
- const acts = allActions(network);
2381
- console.error(`Validated: ${objs.length} object types, ${rels.length} relation types, ${acts.length} action types`);
2382
- }
2383
- catch (error) {
2384
- console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2385
- return 1;
2386
- }
2387
- try {
2388
- await generateChecksum(absDir);
2389
- console.error("Checksum generated");
2390
- }
2391
- catch (error) {
2392
- console.error(`Checksum generation failed: ${error instanceof Error ? error.message : String(error)}`);
2393
- return 1;
2394
- }
2395
- try {
2396
- const tarBuffer = packDirectoryToTar(absDir);
2397
- const token = await ensureValidToken();
2398
- const body = await uploadBkn({
2399
- baseUrl: token.baseUrl,
2400
- accessToken: token.accessToken,
2401
- tarBuffer,
2402
- businessDomain: options.businessDomain,
2403
- branch: options.branch,
2404
- });
2405
- console.log(formatCallOutput(body, options.pretty));
2406
- return 0;
2592
+ try {
2593
+ const network = await loadNetwork(workDir);
2594
+ const objs = allObjects(network);
2595
+ const rels = allRelations(network);
2596
+ const acts = allActions(network);
2597
+ console.error(`Validated: ${objs.length} object types, ${rels.length} relation types, ${acts.length} action types`);
2598
+ }
2599
+ catch (error) {
2600
+ console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2601
+ return 1;
2602
+ }
2603
+ try {
2604
+ await generateChecksum(workDir);
2605
+ console.error("Checksum generated");
2606
+ }
2607
+ catch (error) {
2608
+ console.error(`Checksum generation failed: ${error instanceof Error ? error.message : String(error)}`);
2609
+ return 1;
2610
+ }
2611
+ try {
2612
+ const tarBuffer = packDirectoryToTar(workDir);
2613
+ const token = await ensureValidToken();
2614
+ const body = await uploadBkn({
2615
+ baseUrl: token.baseUrl,
2616
+ accessToken: token.accessToken,
2617
+ tarBuffer,
2618
+ businessDomain: options.businessDomain,
2619
+ branch: options.branch,
2620
+ });
2621
+ console.log(formatCallOutput(body, options.pretty));
2622
+ return 0;
2623
+ }
2624
+ catch (error) {
2625
+ console.error(formatHttpError(error));
2626
+ return 1;
2627
+ }
2407
2628
  }
2408
- catch (error) {
2409
- console.error(formatHttpError(error));
2410
- return 1;
2629
+ finally {
2630
+ prepared.cleanup();
2411
2631
  }
2412
2632
  }
2413
2633
  async function runKnPullCommand(args) {
@@ -125,7 +125,7 @@ Options:
125
125
  <url> API path (e.g. /api/ontology-manager/v1/knowledge-networks)
126
126
  -X, --request HTTP method (default: GET)
127
127
  -H, --header Extra header (repeatable)
128
- -d, --data JSON request body
128
+ -d, --data, --data-raw JSON request body (sets Content-Type: application/json if not set)
129
129
  -bd, --biz-domain Override x-business-domain (default: bd_public)
130
130
  -v, --verbose Print request info to stderr
131
131
  --pretty Pretty-print JSON output (default)`);
@@ -147,6 +147,12 @@ Options:
147
147
  : invocation.url;
148
148
  const headers = new Headers(invocation.headers);
149
149
  injectAuthHeaders(headers, token.accessToken, invocation.businessDomain);
150
+ if (invocation.body !== undefined &&
151
+ invocation.body.length > 0 &&
152
+ !headers.has("content-type") &&
153
+ !headers.has("Content-Type")) {
154
+ headers.set("content-type", "application/json");
155
+ }
150
156
  if (invocation.verbose) {
151
157
  for (const line of formatVerboseRequest({ ...invocation, url, headers })) {
152
158
  console.error(line);
@@ -24,9 +24,10 @@ function getLegacyTokenFilePath() {
24
24
  function getLegacyCallbackFilePath() {
25
25
  return join(getConfigDirPath(), "callback.json");
26
26
  }
27
+ const IS_WIN32 = process.platform === "win32";
27
28
  function ensureDir(path) {
28
29
  if (!existsSync(path)) {
29
- mkdirSync(path, { recursive: true, mode: 0o700 });
30
+ mkdirSync(path, { recursive: true, ...(IS_WIN32 ? {} : { mode: 0o700 }) });
30
31
  }
31
32
  }
32
33
  function ensureConfigDir() {
@@ -41,8 +42,9 @@ function readJsonFile(filePath) {
41
42
  }
42
43
  function writeJsonFile(filePath, value) {
43
44
  ensureConfigDir();
44
- writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
45
- chmodSync(filePath, 0o600);
45
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, IS_WIN32 ? {} : { mode: 0o600 });
46
+ if (!IS_WIN32)
47
+ chmodSync(filePath, 0o600);
46
48
  }
47
49
  function encodePlatformKey(baseUrl) {
48
50
  return Buffer.from(baseUrl, "utf8")
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Normalize .bkn file bytes to UTF-8 for BKN import (validate / push).
3
+ * Used when --detect-encoding (default) or --source-encoding is active.
4
+ */
5
+ /** Minimum confidence (0–1) for charset detection before failing. */
6
+ export declare const BKN_DETECT_MIN_CONFIDENCE = 0.65;
7
+ export interface BknEncodingImportOptions {
8
+ /** When true (default), detect encoding for non-UTF-8 .bkn files. */
9
+ detectEncoding: boolean;
10
+ /** When set, decode all .bkn files with this encoding (overrides detection). */
11
+ sourceEncoding: string | null;
12
+ }
13
+ /**
14
+ * Parse --no-detect-encoding, --detect-encoding, --source-encoding <name> from argv.
15
+ * Remaining args are returned for positional parsing (directory, etc.).
16
+ */
17
+ export declare function stripBknEncodingCliArgs(args: string[]): {
18
+ rest: string[];
19
+ options: BknEncodingImportOptions;
20
+ };
21
+ /**
22
+ * Decode raw .bkn bytes to a UTF-8 Buffer (no BOM).
23
+ */
24
+ export declare function normalizeBknFileBytes(raw: Buffer, options: BknEncodingImportOptions, fileLabel: string): Buffer;
25
+ /**
26
+ * When normalization is needed, copy the tree to a temp dir with .bkn files normalized to UTF-8.
27
+ * Returns the directory to pass to loadNetwork and a cleanup function.
28
+ */
29
+ export declare function prepareBknDirectoryForImport(absDir: string, options: BknEncodingImportOptions): {
30
+ dir: string;
31
+ cleanup: () => void;
32
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Normalize .bkn file bytes to UTF-8 for BKN import (validate / push).
3
+ * Used when --detect-encoding (default) or --source-encoding is active.
4
+ */
5
+ import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join, relative, resolve } from "node:path";
8
+ import chardet from "chardet";
9
+ import iconv from "iconv-lite";
10
+ /** Minimum confidence (0–1) for charset detection before failing. */
11
+ export const BKN_DETECT_MIN_CONFIDENCE = 0.65;
12
+ /**
13
+ * Parse --no-detect-encoding, --detect-encoding, --source-encoding <name> from argv.
14
+ * Remaining args are returned for positional parsing (directory, etc.).
15
+ */
16
+ export function stripBknEncodingCliArgs(args) {
17
+ let detectEncoding = true;
18
+ let sourceEncoding = null;
19
+ const rest = [];
20
+ for (let i = 0; i < args.length; i += 1) {
21
+ const arg = args[i];
22
+ if (arg === "--no-detect-encoding") {
23
+ detectEncoding = false;
24
+ continue;
25
+ }
26
+ if (arg === "--detect-encoding") {
27
+ detectEncoding = true;
28
+ continue;
29
+ }
30
+ if (arg === "--source-encoding") {
31
+ const v = args[i + 1];
32
+ if (!v || v.startsWith("-")) {
33
+ throw new Error("Missing value for --source-encoding (e.g. gb18030)");
34
+ }
35
+ sourceEncoding = v;
36
+ i += 1;
37
+ continue;
38
+ }
39
+ rest.push(arg);
40
+ }
41
+ return {
42
+ rest,
43
+ options: { detectEncoding, sourceEncoding },
44
+ };
45
+ }
46
+ function isValidUtf8(buf) {
47
+ try {
48
+ new TextDecoder("utf-8", { fatal: true }).decode(buf);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ function stripUtf8Bom(buf) {
56
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
57
+ return buf.subarray(3);
58
+ }
59
+ return buf;
60
+ }
61
+ /**
62
+ * Decode raw .bkn bytes to a UTF-8 Buffer (no BOM).
63
+ */
64
+ export function normalizeBknFileBytes(raw, options, fileLabel) {
65
+ if (options.sourceEncoding) {
66
+ const enc = options.sourceEncoding.trim().toLowerCase();
67
+ if (enc === "utf-8" || enc === "utf8") {
68
+ const body = stripUtf8Bom(raw);
69
+ if (!isValidUtf8(body)) {
70
+ throw new Error(`Invalid UTF-8 in ${fileLabel} despite --source-encoding utf-8`);
71
+ }
72
+ return Buffer.from(body.toString("utf8"), "utf8");
73
+ }
74
+ if (!iconv.encodingExists(enc)) {
75
+ throw new Error(`Unsupported --source-encoding: ${options.sourceEncoding}`);
76
+ }
77
+ const text = iconv.decode(raw, enc);
78
+ return Buffer.from(text, "utf8");
79
+ }
80
+ if (!options.detectEncoding) {
81
+ const body = stripUtf8Bom(raw);
82
+ if (!isValidUtf8(body)) {
83
+ throw new Error(`Invalid UTF-8 in ${fileLabel}. Use --detect-encoding (default) or --source-encoding (e.g. gb18030).`);
84
+ }
85
+ return Buffer.from(body.toString("utf8"), "utf8");
86
+ }
87
+ let work = stripUtf8Bom(raw);
88
+ if (isValidUtf8(work)) {
89
+ return Buffer.from(work.toString("utf8"), "utf8");
90
+ }
91
+ const matches = chardet.analyse(work);
92
+ const best = matches[0];
93
+ if (!best || best.confidence < BKN_DETECT_MIN_CONFIDENCE) {
94
+ throw new Error(`Could not detect encoding confidently for ${fileLabel} (best confidence ${best?.confidence ?? 0}). ` +
95
+ `Try --source-encoding gb18030 or save files as UTF-8.`);
96
+ }
97
+ const name = best.name ?? "utf-8";
98
+ if (!iconv.encodingExists(name)) {
99
+ throw new Error(`Detected encoding "${name}" is not supported for ${fileLabel}. Try --source-encoding.`);
100
+ }
101
+ const text = iconv.decode(work, name);
102
+ return Buffer.from(text, "utf8");
103
+ }
104
+ /**
105
+ * When normalization is needed, copy the tree to a temp dir with .bkn files normalized to UTF-8.
106
+ * Returns the directory to pass to loadNetwork and a cleanup function.
107
+ */
108
+ export function prepareBknDirectoryForImport(absDir, options) {
109
+ const needWork = options.sourceEncoding != null || options.detectEncoding;
110
+ if (!needWork) {
111
+ return { dir: absDir, cleanup: () => { } };
112
+ }
113
+ const root = resolve(absDir);
114
+ const tmpRoot = mkdtempSync(join(tmpdir(), "kweaver-bkn-"));
115
+ function walk(srcDir, destDir) {
116
+ mkdirSync(destDir, { recursive: true });
117
+ const entries = readdirSync(srcDir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (entry.name === "." || entry.name === "..")
120
+ continue;
121
+ const srcPath = join(srcDir, entry.name);
122
+ const destPath = join(destDir, entry.name);
123
+ if (entry.isDirectory()) {
124
+ walk(srcPath, destPath);
125
+ continue;
126
+ }
127
+ if (!entry.isFile())
128
+ continue;
129
+ if (entry.name.endsWith(".bkn")) {
130
+ const raw = readFileSync(srcPath);
131
+ const rel = relative(root, srcPath) || entry.name;
132
+ const out = normalizeBknFileBytes(raw, options, rel);
133
+ writeFileSync(destPath, out);
134
+ }
135
+ else {
136
+ copyFileSync(srcPath, destPath);
137
+ }
138
+ }
139
+ }
140
+ walk(root, tmpRoot);
141
+ return {
142
+ dir: tmpRoot,
143
+ cleanup: () => {
144
+ try {
145
+ rmSync(tmpRoot, { recursive: true, force: true });
146
+ }
147
+ catch {
148
+ /* ignore */
149
+ }
150
+ },
151
+ };
152
+ }
@@ -1,20 +1,23 @@
1
1
  import { spawn } from "node:child_process";
2
2
  export function openBrowser(url) {
3
- const command = process.platform === "darwin"
4
- ? "open"
5
- : process.platform === "win32"
6
- ? "cmd"
7
- : "xdg-open";
8
- const args = process.platform === "win32"
9
- ? ["/c", "start", "", url]
3
+ const isWindows = process.platform === "win32";
4
+ const command = process.platform === "darwin" ? "open" : isWindows ? "rundll32.exe" : "xdg-open";
5
+ const args = isWindows
6
+ ? [
7
+ "url.dll,FileProtocolHandler",
8
+ url,
9
+ ]
10
10
  : [url];
11
11
  return new Promise((resolve) => {
12
12
  const child = spawn(command, args, {
13
- detached: true,
14
13
  stdio: "ignore",
14
+ detached: !isWindows,
15
+ windowsHide: true,
15
16
  });
16
- child.on("error", () => resolve(false));
17
- child.unref();
18
- resolve(true);
17
+ child.once("error", () => resolve(false));
18
+ child.once("spawn", () => resolve(true));
19
+ if (!isWindows) {
20
+ child.unref();
21
+ }
19
22
  });
20
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kweaver-ai/kweaver-sdk",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
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",
@@ -28,7 +28,8 @@
28
28
  "start": "node ./dist/cli.js",
29
29
  "lint": "tsc --noEmit -p tsconfig.json",
30
30
  "test": "node --import tsx --test test/*.test.ts",
31
- "test:e2e": "node --import tsx --test test/e2e/**/*.test.ts",
31
+ "test:e2e": "node --import tsx test/e2e/ensure-token.ts && node --import tsx --test --test-concurrency=1 test/e2e/**/*.test.ts",
32
+ "test:e2e:strict": "node test/e2e/run-e2e-strict.mjs",
32
33
  "prepublishOnly": "npm run build"
33
34
  },
34
35
  "keywords": [
@@ -50,6 +51,7 @@
50
51
  "devDependencies": {
51
52
  "@types/node": "^24.6.0",
52
53
  "@types/react": "^19.2.14",
54
+ "playwright": "^1.58.2",
53
55
  "tsx": "^4.20.5",
54
56
  "typescript": "^5.9.3"
55
57
  },
@@ -63,6 +65,9 @@
63
65
  },
64
66
  "dependencies": {
65
67
  "@kweaver-ai/bkn": "^0.1.0",
68
+ "@playwright/test": "^1.58.2",
69
+ "chardet": "^2.1.1",
70
+ "iconv-lite": "^0.7.2",
66
71
  "ink": "^6.8.0",
67
72
  "ink-spinner": "^5.0.0",
68
73
  "ink-text-input": "^6.0.0",