@kweaver-ai/kweaver-sdk 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,7 +98,7 @@ const results = await cl.search({ query: "hypertension treatment" });
98
98
  ## CLI Reference
99
99
 
100
100
  ```
101
- kweaver auth login/status/list/use/delete/logout
101
+ kweaver auth login <url> [--alias name] [-u user] [-p pass] [--playwright] — also: status, list, use, delete, logout
102
102
  kweaver token
103
103
  kweaver bkn list/get/stats/export/create/update/delete
104
104
  kweaver bkn object-type list/get/create/update/delete/query/properties
package/README.zh.md CHANGED
@@ -98,7 +98,7 @@ const results = await cl.search({ query: "高血压 治疗" });
98
98
  ## 命令速查
99
99
 
100
100
  ```
101
- kweaver auth login/status/list/use/delete/logout
101
+ kweaver auth login <url> [--alias name] [-u user] [-p pass] [--playwright] — 另有 statuslistusedeletelogout
102
102
  kweaver token
103
103
  kweaver bkn list/get/stats/export/create/update/delete
104
104
  kweaver bkn object-type list/get/create/update/delete/query/properties
@@ -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
  });
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ Usage:
33
33
  kweaver agent get <agent_id> [-bd value] [--pretty]
34
34
  kweaver agent get-by-key <key> [-bd value] [--pretty]
35
35
  kweaver agent sessions <agent_id> [-bd value] [--limit N] [--pretty]
36
- kweaver agent history <agent_id> <session_id> [-bd value] [--limit N] [--pretty]
36
+ kweaver agent history <conversation_id> [-bd value] [--limit N] [--pretty]
37
37
  kweaver agent create [options]
38
38
  kweaver agent update <agent_id> [options]
39
39
  kweaver agent delete <agent_id> [-bd value]
@@ -54,10 +54,10 @@ Usage:
54
54
  kweaver bkn update <kn-id> [options]
55
55
  kweaver bkn delete <kn-id> [-y]
56
56
  kweaver bkn build <kn-id> [--wait] [--no-wait] [--timeout N]
57
- kweaver bkn validate <kn-id>
57
+ kweaver bkn validate <directory> [--detect-encoding|--no-detect-encoding] [--source-encoding name]
58
58
  kweaver bkn export <kn-id>
59
59
  kweaver bkn stats <kn-id>
60
- kweaver bkn push <directory> [--branch main] [-bd value]
60
+ kweaver bkn push <directory> [--branch main] [-bd value] [--detect-encoding|--no-detect-encoding] [--source-encoding name]
61
61
  kweaver bkn pull <kn-id> [directory] [--branch main] [-bd value]
62
62
  kweaver bkn object-type list|get|create|update|delete|query|properties <kn-id> ...
63
63
  kweaver bkn relation-type list|get|create|update|delete <kn-id> ...
@@ -154,7 +154,9 @@ function safeExit(code) {
154
154
  process.exit(code);
155
155
  }
156
156
  }
157
- 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])) {
158
160
  run(process.argv.slice(2))
159
161
  .then((code) => {
160
162
  safeExit(code);
@@ -4,19 +4,29 @@ export async function runAuthCommand(args) {
4
4
  const target = args[0];
5
5
  const rest = args.slice(1);
6
6
  if (!target || target === "--help" || target === "-h") {
7
- console.log(`kweaver auth login <url> Login to a platform (browser login)
8
- kweaver auth <url> Login (shorthand)
9
- kweaver auth status [url] Show current auth status
7
+ console.log(`kweaver auth login <url> [--alias <name>] [-u user] [-p pass] [--playwright]
8
+ kweaver auth <url> Login (shorthand; same options as login)
9
+ kweaver auth status [url|alias]
10
10
  kweaver auth list List saved platforms
11
- kweaver auth use <url> Switch active platform
12
- kweaver auth logout [url] Logout (clear local token)
13
- kweaver auth delete <url> Delete saved credentials`);
11
+ kweaver auth use <url|alias> Switch active platform
12
+ kweaver auth logout [url|alias] Logout (clear local token)
13
+ kweaver auth delete <url|alias> Delete saved credentials
14
+
15
+ Login options (browser OAuth2 by default; use -u/-p for headless Playwright):
16
+ --alias <name> Short name for this platform (use with auth use / status / logout)
17
+ -u, --username Username (with -p triggers Playwright login)
18
+ -p, --password Password
19
+ --playwright Force browser (Playwright) login even without -u/-p`);
14
20
  return 0;
15
21
  }
16
22
  if (target === "login") {
23
+ if (rest[0] === "--help" || rest[0] === "-h") {
24
+ console.log(`kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]`);
25
+ return 0;
26
+ }
17
27
  const url = rest[0];
18
- if (!url) {
19
- console.error("Usage: kweaver auth login <platform-url>");
28
+ if (!url || url.startsWith("-")) {
29
+ console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
20
30
  return 1;
21
31
  }
22
32
  return runAuthCommand([url, ...rest.slice(1)]);
@@ -182,8 +192,8 @@ kweaver auth delete <url> Delete saved credentials`);
182
192
  console.log(`Run \`kweaver auth login ${logoutTarget}\` to sign in again.`);
183
193
  return 0;
184
194
  }
185
- console.error("Usage: kweaver auth login <platform-url>");
186
- console.error(" kweaver auth <platform-url> [--alias <name>]");
195
+ console.error("Usage: kweaver auth login <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
196
+ console.error(" kweaver auth <platform-url> [--alias <name>] [-u user] [-p pass] [--playwright]");
187
197
  console.error(" kweaver auth status [platform-url|alias]");
188
198
  console.error(" kweaver auth list");
189
199
  console.error(" kweaver auth use <platform-url|alias>");
@@ -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,14 @@ 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
+ /** Parse object-type create args: --name --dataview-id --primary-key --display-key [--property '<json>' ...] */
70
+ export declare function parseObjectTypeCreateArgs(args: string[]): {
71
+ knId: string;
72
+ body: string;
73
+ businessDomain: string;
74
+ branch: string;
75
+ pretty: boolean;
76
+ };
67
77
  /** Fields merged via GET → modify → PUT (not raw body mode). */
68
78
  export interface ObjectTypeMergeFields {
69
79
  name?: string;
@@ -110,6 +120,14 @@ export interface KnActionTypeExecuteOptions {
110
120
  timeout: number;
111
121
  }
112
122
  export declare function parseKnActionTypeExecuteArgs(args: string[]): KnActionTypeExecuteOptions;
123
+ /** Parse relation-type create args: --name --source --target [--mapping src:tgt ...] */
124
+ export declare function parseRelationTypeCreateArgs(args: string[]): {
125
+ knId: string;
126
+ body: string;
127
+ businessDomain: string;
128
+ branch: string;
129
+ pretty: boolean;
130
+ };
113
131
  export declare function parseKnBuildArgs(args: string[]): {
114
132
  knId: string;
115
133
  wait: boolean;
@@ -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 = "";
@@ -601,8 +603,8 @@ Subcommands:
601
603
  update <kn-id> [options] Update a knowledge network
602
604
  delete <kn-id> Delete a knowledge network
603
605
  build <kn-id> [--wait|--no-wait] [--timeout n] Trigger full build
604
- validate <directory> Validate a local BKN directory (no upload)
605
- push <directory> [--branch main] Upload BKN directory as tar
606
+ validate <directory> [--detect-encoding|--no-detect-encoding] [--source-encoding n] Validate local BKN (no upload)
607
+ push <directory> [--branch main] [--detect-encoding|--no-detect-encoding] [--source-encoding n] Upload BKN as tar
606
608
  pull <kn-id> [<directory>] [--branch main] Download BKN tar and extract
607
609
  export <kn-id> Export knowledge network (alias for get --export)
608
610
  stats <kn-id> Get statistics (alias for get --stats)
@@ -692,7 +694,7 @@ export async function runKnCommand(args) {
692
694
  }
693
695
  }
694
696
  /** Parse object-type create args: --name --dataview-id --primary-key --display-key [--property '<json>' ...] */
695
- function parseObjectTypeCreateArgs(args) {
697
+ export function parseObjectTypeCreateArgs(args) {
696
698
  let name = "";
697
699
  let dataviewId = "";
698
700
  let primaryKey = "";
@@ -746,6 +748,7 @@ function parseObjectTypeCreateArgs(args) {
746
748
  throw new Error("Usage: kweaver bkn object-type create <kn-id> --name X --dataview-id Y --primary-key Z --display-key W");
747
749
  }
748
750
  const entry = {
751
+ branch,
749
752
  name,
750
753
  data_source: { type: "data_view", id: dataviewId },
751
754
  primary_keys: [primaryKey],
@@ -762,7 +765,7 @@ function parseObjectTypeCreateArgs(args) {
762
765
  type: "string",
763
766
  }));
764
767
  }
765
- const body = JSON.stringify({ entries: [entry], branch });
768
+ const body = JSON.stringify({ entries: [entry] });
766
769
  if (!businessDomain)
767
770
  businessDomain = resolveBusinessDomain();
768
771
  return { knId, body, businessDomain, branch, pretty };
@@ -1285,7 +1288,7 @@ kweaver bkn object-type delete <kn-id> <ot-ids> [-y]`);
1285
1288
  }
1286
1289
  }
1287
1290
  /** Parse relation-type create args: --name --source --target [--mapping src:tgt ...] */
1288
- function parseRelationTypeCreateArgs(args) {
1291
+ export function parseRelationTypeCreateArgs(args) {
1289
1292
  let name = "";
1290
1293
  let source = "";
1291
1294
  let target = "";
@@ -1339,6 +1342,7 @@ function parseRelationTypeCreateArgs(args) {
1339
1342
  throw new Error("Usage: kweaver bkn relation-type create <kn-id> --name X --source <ot-id> --target <ot-id> [--mapping src:tgt ...]");
1340
1343
  }
1341
1344
  const entry = {
1345
+ branch,
1342
1346
  name,
1343
1347
  source_object_type_id: source,
1344
1348
  target_object_type_id: target,
@@ -1348,7 +1352,7 @@ function parseRelationTypeCreateArgs(args) {
1348
1352
  target_property: { name: t },
1349
1353
  })),
1350
1354
  };
1351
- const body = JSON.stringify({ entries: [entry], branch });
1355
+ const body = JSON.stringify({ entries: [entry] });
1352
1356
  if (!businessDomain)
1353
1357
  businessDomain = resolveBusinessDomain();
1354
1358
  return { knId, body, businessDomain, branch, pretty };
@@ -2194,6 +2198,7 @@ async function runKnCreateFromDsCommand(args) {
2194
2198
  const pk = detectPrimaryKey(t);
2195
2199
  const dk = detectDisplayKey(t, pk);
2196
2200
  const entry = {
2201
+ branch: "main",
2197
2202
  name: t.name,
2198
2203
  data_source: { type: "data_view", id: viewMap[t.name] },
2199
2204
  primary_keys: [pk],
@@ -2204,7 +2209,7 @@ async function runKnCreateFromDsCommand(args) {
2204
2209
  type: "string",
2205
2210
  })),
2206
2211
  };
2207
- const otBody = JSON.stringify({ entries: [entry], branch: "main" });
2212
+ const otBody = JSON.stringify({ entries: [entry] });
2208
2213
  const otResponse = await createObjectTypes({
2209
2214
  ...base,
2210
2215
  knId,
@@ -2443,8 +2448,13 @@ export function packDirectoryToTar(dirPath) {
2443
2448
  encoding: "buffer",
2444
2449
  env: { ...process.env, COPYFILE_DISABLE: "1" },
2445
2450
  });
2446
- if (result.error)
2451
+ if (result.error) {
2452
+ if ("code" in result.error && result.error.code === "ENOENT") {
2453
+ throw new Error("tar executable not found. On Windows, ensure tar.exe is in PATH " +
2454
+ "(ships with Windows 10 1803+) or install GNU tar via Git for Windows / scoop.");
2455
+ }
2447
2456
  throw result.error;
2457
+ }
2448
2458
  if (result.status !== 0) {
2449
2459
  throw new Error(`tar pack failed: ${result.stderr?.toString() ?? result.status}`);
2450
2460
  }
@@ -2457,6 +2467,10 @@ export function extractTarToDirectory(tarBuffer, dirPath) {
2457
2467
  input: tarBuffer,
2458
2468
  });
2459
2469
  if (result.error) {
2470
+ if ("code" in result.error && result.error.code === "ENOENT") {
2471
+ throw new Error("tar executable not found. On Windows, ensure tar.exe is in PATH " +
2472
+ "(ships with Windows 10 1803+) or install GNU tar via Git for Windows / scoop.");
2473
+ }
2460
2474
  throw result.error;
2461
2475
  }
2462
2476
  if (result.status !== 0) {
@@ -2470,7 +2484,10 @@ Pack a BKN directory into a tar and upload to import as a knowledge network.
2470
2484
  Options:
2471
2485
  --branch <s> Branch name (default: main)
2472
2486
  -bd, --biz-domain Business domain (default: bd_public)
2473
- --pretty Pretty-print JSON output`;
2487
+ --pretty Pretty-print JSON output
2488
+ --detect-encoding Detect .bkn encoding and normalize to UTF-8 (default: on)
2489
+ --no-detect-encoding Do not detect; require UTF-8 .bkn files
2490
+ --source-encoding <name> Decode all .bkn files with this encoding (e.g. gb18030); overrides detection`;
2474
2491
  const KN_PULL_HELP = `kweaver bkn pull <kn-id> [<directory>] [options]
2475
2492
 
2476
2493
  Download a BKN tar from a knowledge network and extract to a local directory.
@@ -2481,12 +2498,28 @@ Options:
2481
2498
  -bd, --biz-domain Business domain (default: bd_public)`;
2482
2499
  async function runKnValidateCommand(args) {
2483
2500
  if (args.includes("--help") || args.includes("-h")) {
2484
- console.log("Usage: kweaver bkn validate <directory>\n\nValidate a local BKN directory without uploading.");
2501
+ console.log("Usage: kweaver bkn validate <directory> [options]\n\n" +
2502
+ "Validate a local BKN directory without uploading.\n\n" +
2503
+ "Options:\n" +
2504
+ " --detect-encoding Detect .bkn encoding and normalize to UTF-8 (default: on)\n" +
2505
+ " --no-detect-encoding Require UTF-8 .bkn files\n" +
2506
+ " --source-encoding <n> Decode all .bkn with this encoding (e.g. gb18030)");
2485
2507
  return 0;
2486
2508
  }
2487
- const directory = args.find((a) => !a.startsWith("-"));
2509
+ let encodingOptions;
2510
+ let restArgs;
2511
+ try {
2512
+ const stripped = stripBknEncodingCliArgs(args);
2513
+ encodingOptions = stripped.options;
2514
+ restArgs = stripped.rest;
2515
+ }
2516
+ catch (e) {
2517
+ console.error(e instanceof Error ? e.message : String(e));
2518
+ return 1;
2519
+ }
2520
+ const directory = restArgs.find((a) => !a.startsWith("-"));
2488
2521
  if (!directory) {
2489
- console.error("Missing directory. Usage: kweaver bkn validate <directory>");
2522
+ console.error("Missing directory. Usage: kweaver bkn validate <directory> [options]");
2490
2523
  return 1;
2491
2524
  }
2492
2525
  const absDir = resolve(directory);
@@ -2504,8 +2537,9 @@ async function runKnValidateCommand(args) {
2504
2537
  }
2505
2538
  throw err;
2506
2539
  }
2540
+ const prepared = prepareBknDirectoryForImport(absDir, encodingOptions);
2507
2541
  try {
2508
- const network = await loadNetwork(absDir);
2542
+ const network = await loadNetwork(prepared.dir);
2509
2543
  const result = validateNetwork(network);
2510
2544
  if (!result.ok) {
2511
2545
  for (const e of result.errors)
@@ -2523,6 +2557,9 @@ async function runKnValidateCommand(args) {
2523
2557
  console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2524
2558
  return 1;
2525
2559
  }
2560
+ finally {
2561
+ prepared.cleanup();
2562
+ }
2526
2563
  }
2527
2564
  async function runKnPushCommand(args) {
2528
2565
  let options;
@@ -2552,41 +2589,48 @@ async function runKnPushCommand(args) {
2552
2589
  }
2553
2590
  throw err;
2554
2591
  }
2592
+ const prepared = prepareBknDirectoryForImport(absDir, options.encodingOptions);
2593
+ const workDir = prepared.dir;
2555
2594
  try {
2556
- const network = await loadNetwork(absDir);
2557
- const objs = allObjects(network);
2558
- const rels = allRelations(network);
2559
- const acts = allActions(network);
2560
- console.error(`Validated: ${objs.length} object types, ${rels.length} relation types, ${acts.length} action types`);
2561
- }
2562
- catch (error) {
2563
- console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2564
- return 1;
2565
- }
2566
- try {
2567
- await generateChecksum(absDir);
2568
- console.error("Checksum generated");
2569
- }
2570
- catch (error) {
2571
- console.error(`Checksum generation failed: ${error instanceof Error ? error.message : String(error)}`);
2572
- return 1;
2573
- }
2574
- try {
2575
- const tarBuffer = packDirectoryToTar(absDir);
2576
- const token = await ensureValidToken();
2577
- const body = await uploadBkn({
2578
- baseUrl: token.baseUrl,
2579
- accessToken: token.accessToken,
2580
- tarBuffer,
2581
- businessDomain: options.businessDomain,
2582
- branch: options.branch,
2583
- });
2584
- console.log(formatCallOutput(body, options.pretty));
2585
- return 0;
2595
+ try {
2596
+ const network = await loadNetwork(workDir);
2597
+ const objs = allObjects(network);
2598
+ const rels = allRelations(network);
2599
+ const acts = allActions(network);
2600
+ console.error(`Validated: ${objs.length} object types, ${rels.length} relation types, ${acts.length} action types`);
2601
+ }
2602
+ catch (error) {
2603
+ console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2604
+ return 1;
2605
+ }
2606
+ try {
2607
+ await generateChecksum(workDir);
2608
+ console.error("Checksum generated");
2609
+ }
2610
+ catch (error) {
2611
+ console.error(`Checksum generation failed: ${error instanceof Error ? error.message : String(error)}`);
2612
+ return 1;
2613
+ }
2614
+ try {
2615
+ const tarBuffer = packDirectoryToTar(workDir);
2616
+ const token = await ensureValidToken();
2617
+ const body = await uploadBkn({
2618
+ baseUrl: token.baseUrl,
2619
+ accessToken: token.accessToken,
2620
+ tarBuffer,
2621
+ businessDomain: options.businessDomain,
2622
+ branch: options.branch,
2623
+ });
2624
+ console.log(formatCallOutput(body, options.pretty));
2625
+ return 0;
2626
+ }
2627
+ catch (error) {
2628
+ console.error(formatHttpError(error));
2629
+ return 1;
2630
+ }
2586
2631
  }
2587
- catch (error) {
2588
- console.error(formatHttpError(error));
2589
- return 1;
2632
+ finally {
2633
+ prepared.cleanup();
2590
2634
  }
2591
2635
  }
2592
2636
  async function runKnPullCommand(args) {
@@ -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.8",
3
+ "version": "0.4.10",
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",
@@ -29,6 +29,7 @@
29
29
  "lint": "tsc --noEmit -p tsconfig.json",
30
30
  "test": "node --import tsx --test test/*.test.ts",
31
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",