@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.
- package/dist/auth/oauth.d.ts +11 -2
- package/dist/auth/oauth.js +98 -67
- package/dist/cli.js +80 -25
- package/dist/commands/bkn.d.ts +38 -0
- package/dist/commands/bkn.js +277 -57
- package/dist/commands/call.js +7 -1
- package/dist/config/store.js +5 -3
- package/dist/utils/bkn-encoding.d.ts +32 -0
- package/dist/utils/bkn-encoding.js +152 -0
- package/dist/utils/browser.js +14 -11
- package/package.json +7 -2
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -13,12 +13,21 @@ export declare function oauth2Login(baseUrl: string, options?: {
|
|
|
13
13
|
scope?: string;
|
|
14
14
|
}): Promise<TokenConfig>;
|
|
15
15
|
/**
|
|
16
|
-
* Playwright
|
|
17
|
-
*
|
|
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).
|
package/dist/auth/oauth.js
CHANGED
|
@@ -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("
|
|
79
|
-
|
|
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
|
|
165
|
-
*
|
|
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
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
throw e;
|
|
234
|
+
else {
|
|
235
|
+
resolve(receivedCode);
|
|
219
236
|
}
|
|
220
237
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
15
|
-
kweaver
|
|
16
|
-
|
|
17
|
-
kweaver auth
|
|
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
|
-
|
|
24
|
-
kweaver
|
|
25
|
-
|
|
26
|
-
kweaver
|
|
27
|
-
|
|
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
|
|
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> [
|
|
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
|
|
37
|
-
kweaver
|
|
38
|
-
kweaver
|
|
39
|
-
kweaver
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
50
|
-
context-loader
|
|
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
|
-
|
|
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);
|
package/dist/commands/bkn.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/bkn.js
CHANGED
|
@@ -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 <
|
|
362
|
-
const arg =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
-
|
|
2409
|
-
|
|
2410
|
-
return 1;
|
|
2629
|
+
finally {
|
|
2630
|
+
prepared.cleanup();
|
|
2411
2631
|
}
|
|
2412
2632
|
}
|
|
2413
2633
|
async function runKnPullCommand(args) {
|
package/dist/commands/call.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/config/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/utils/browser.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
export function openBrowser(url) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
17
|
-
child.
|
|
18
|
-
|
|
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.
|
|
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",
|