@kweaver-ai/kweaver-sdk 0.4.8 → 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.
@@ -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
@@ -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);
@@ -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 {
@@ -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 = "";
@@ -2443,8 +2445,13 @@ export function packDirectoryToTar(dirPath) {
2443
2445
  encoding: "buffer",
2444
2446
  env: { ...process.env, COPYFILE_DISABLE: "1" },
2445
2447
  });
2446
- 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
+ }
2447
2453
  throw result.error;
2454
+ }
2448
2455
  if (result.status !== 0) {
2449
2456
  throw new Error(`tar pack failed: ${result.stderr?.toString() ?? result.status}`);
2450
2457
  }
@@ -2457,6 +2464,10 @@ export function extractTarToDirectory(tarBuffer, dirPath) {
2457
2464
  input: tarBuffer,
2458
2465
  });
2459
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
+ }
2460
2471
  throw result.error;
2461
2472
  }
2462
2473
  if (result.status !== 0) {
@@ -2470,7 +2481,10 @@ Pack a BKN directory into a tar and upload to import as a knowledge network.
2470
2481
  Options:
2471
2482
  --branch <s> Branch name (default: main)
2472
2483
  -bd, --biz-domain Business domain (default: bd_public)
2473
- --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`;
2474
2488
  const KN_PULL_HELP = `kweaver bkn pull <kn-id> [<directory>] [options]
2475
2489
 
2476
2490
  Download a BKN tar from a knowledge network and extract to a local directory.
@@ -2481,12 +2495,28 @@ Options:
2481
2495
  -bd, --biz-domain Business domain (default: bd_public)`;
2482
2496
  async function runKnValidateCommand(args) {
2483
2497
  if (args.includes("--help") || args.includes("-h")) {
2484
- 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)");
2485
2504
  return 0;
2486
2505
  }
2487
- 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("-"));
2488
2518
  if (!directory) {
2489
- console.error("Missing directory. Usage: kweaver bkn validate <directory>");
2519
+ console.error("Missing directory. Usage: kweaver bkn validate <directory> [options]");
2490
2520
  return 1;
2491
2521
  }
2492
2522
  const absDir = resolve(directory);
@@ -2504,8 +2534,9 @@ async function runKnValidateCommand(args) {
2504
2534
  }
2505
2535
  throw err;
2506
2536
  }
2537
+ const prepared = prepareBknDirectoryForImport(absDir, encodingOptions);
2507
2538
  try {
2508
- const network = await loadNetwork(absDir);
2539
+ const network = await loadNetwork(prepared.dir);
2509
2540
  const result = validateNetwork(network);
2510
2541
  if (!result.ok) {
2511
2542
  for (const e of result.errors)
@@ -2523,6 +2554,9 @@ async function runKnValidateCommand(args) {
2523
2554
  console.error(`BKN validation failed: ${error instanceof Error ? error.message : String(error)}`);
2524
2555
  return 1;
2525
2556
  }
2557
+ finally {
2558
+ prepared.cleanup();
2559
+ }
2526
2560
  }
2527
2561
  async function runKnPushCommand(args) {
2528
2562
  let options;
@@ -2552,41 +2586,48 @@ async function runKnPushCommand(args) {
2552
2586
  }
2553
2587
  throw err;
2554
2588
  }
2589
+ const prepared = prepareBknDirectoryForImport(absDir, options.encodingOptions);
2590
+ const workDir = prepared.dir;
2555
2591
  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;
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
+ }
2586
2628
  }
2587
- catch (error) {
2588
- console.error(formatHttpError(error));
2589
- return 1;
2629
+ finally {
2630
+ prepared.cleanup();
2590
2631
  }
2591
2632
  }
2592
2633
  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.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",
@@ -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",