@kweaver-ai/kweaver-sdk 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/kweaver.js CHANGED
@@ -1,9 +1,24 @@
1
1
  #!/usr/bin/env node
2
+
3
+ function exit(code) {
4
+ if (process.stdout.writableNeedDrain || process.stderr.writableNeedDrain) {
5
+ const done = () => {
6
+ if (!process.stdout.writableNeedDrain && !process.stderr.writableNeedDrain) {
7
+ process.exit(code);
8
+ }
9
+ };
10
+ process.stdout.once("drain", done);
11
+ process.stderr.once("drain", done);
12
+ } else {
13
+ process.exit(code);
14
+ }
15
+ }
16
+
2
17
  import("../dist/cli.js").then(({ run }) => {
3
18
  run(process.argv.slice(2))
4
- .then((code) => process.exit(code))
19
+ .then((code) => exit(code))
5
20
  .catch((err) => {
6
21
  console.error(err instanceof Error ? err.message : String(err));
7
- process.exit(1);
22
+ exit(1);
8
23
  });
9
24
  });
@@ -13,12 +13,21 @@ export declare function oauth2Login(baseUrl: string, options?: {
13
13
  scope?: string;
14
14
  }): Promise<TokenConfig>;
15
15
  /**
16
- * Playwright cookie login (legacy fallback).
17
- * Does NOT produce a refresh_token — token expires in 1 hour with no auto-refresh.
16
+ * Playwright-automated OAuth2 login.
17
+ *
18
+ * Uses the full OAuth2 authorization code flow (same as `oauth2Login`) but
19
+ * automates the browser interaction with Playwright. This produces a
20
+ * refresh_token so the CLI can auto-refresh without re-login.
21
+ *
22
+ * When `username` and `password` are provided the browser runs headless and
23
+ * fills the login form automatically. Otherwise it opens a visible browser
24
+ * window for manual login (same UX as the old cookie-based flow).
18
25
  */
19
26
  export declare function playwrightLogin(baseUrl: string, options?: {
20
27
  username?: string;
21
28
  password?: string;
29
+ port?: number;
30
+ scope?: string;
22
31
  }): Promise<TokenConfig>;
23
32
  /**
24
33
  * Exchange refresh_token for a new access token (OAuth2 password grant style, same as Python ConfigAuth).
@@ -161,10 +161,19 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
161
161
  return token;
162
162
  }
163
163
  /**
164
- * Playwright cookie login (legacy fallback).
165
- * Does NOT produce a refresh_token — token expires in 1 hour with no auto-refresh.
164
+ * Playwright-automated OAuth2 login.
165
+ *
166
+ * Uses the full OAuth2 authorization code flow (same as `oauth2Login`) but
167
+ * automates the browser interaction with Playwright. This produces a
168
+ * refresh_token so the CLI can auto-refresh without re-login.
169
+ *
170
+ * When `username` and `password` are provided the browser runs headless and
171
+ * fills the login form automatically. Otherwise it opens a visible browser
172
+ * window for manual login (same UX as the old cookie-based flow).
166
173
  */
167
174
  export async function playwrightLogin(baseUrl, options) {
175
+ const { createServer } = await import("node:http");
176
+ const { randomBytes } = await import("node:crypto");
168
177
  let chromium;
169
178
  try {
170
179
  const modName = "playwright";
@@ -174,71 +183,94 @@ export async function playwrightLogin(baseUrl, options) {
174
183
  catch {
175
184
  throw new Error("Playwright is not installed. Run:\n npm install playwright && npx playwright install chromium");
176
185
  }
177
- const hasCredentials = options?.username && options?.password;
178
- const browser = await chromium.launch({ headless: hasCredentials ? true : false });
179
- try {
180
- const context = await browser.newContext();
181
- const page = await context.newPage();
182
- await page.goto(`${baseUrl}/api/dip-hub/v1/login`, {
183
- waitUntil: "networkidle",
184
- timeout: 30_000,
185
- });
186
- if (hasCredentials) {
187
- // Headless mode: auto-fill credentials
188
- await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
189
- await page.fill('input[name="account"]', options.username);
190
- await page.fill('input[name="password"]', options.password);
191
- await page.click("button.ant-btn-primary");
192
- }
193
- // else: headed mode — user logs in manually in the browser window
194
- const TIMEOUT_SECONDS = hasCredentials ? 30 : 120;
195
- let accessToken = null;
196
- for (let i = 0; i < TIMEOUT_SECONDS; i++) {
197
- await new Promise((r) => setTimeout(r, 1000));
198
- // Check cookies (works even after navigation)
199
- for (const cookie of await context.cookies()) {
200
- if (cookie.name === "dip.oauth2_token") {
201
- accessToken = decodeURIComponent(cookie.value);
202
- break;
186
+ const base = normalizeBaseUrl(baseUrl);
187
+ const port = options?.port ?? DEFAULT_REDIRECT_PORT;
188
+ const scope = options?.scope ?? DEFAULT_SCOPE;
189
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
190
+ const hasCredentials = !!(options?.username && options?.password);
191
+ // Step 1: Ensure registered OAuth2 client
192
+ let client = loadClientConfig(base);
193
+ if (!client?.clientId) {
194
+ client = await registerOAuth2Client(base, redirectUri, scope);
195
+ saveClientConfig(base, client);
196
+ }
197
+ // Step 2: Generate CSRF state
198
+ const state = randomBytes(12).toString("hex");
199
+ // Step 3: Build authorization URL
200
+ const authParams = new URLSearchParams({
201
+ redirect_uri: redirectUri,
202
+ "x-forwarded-prefix": "",
203
+ client_id: client.clientId,
204
+ scope,
205
+ response_type: "code",
206
+ state,
207
+ lang: "zh-cn",
208
+ product: "adp",
209
+ });
210
+ const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
211
+ // Step 4: Start local callback server to capture the authorization code
212
+ const code = await new Promise((resolve, reject) => {
213
+ const TIMEOUT_MS = hasCredentials ? 30_000 : 120_000;
214
+ const timeoutId = setTimeout(() => {
215
+ server.close();
216
+ browser?.close();
217
+ reject(new Error(`OAuth2 login timed out (${TIMEOUT_MS / 1000}s). No authorization code received.`));
218
+ }, TIMEOUT_MS);
219
+ const server = createServer((req, res) => {
220
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
221
+ if (url.pathname === "/callback") {
222
+ const receivedState = url.searchParams.get("state");
223
+ const receivedCode = url.searchParams.get("code");
224
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
225
+ res.end("<html><body><h2>Login successful. You can close this tab.</h2></body></html>");
226
+ clearTimeout(timeoutId);
227
+ server.close();
228
+ browser?.close();
229
+ if (receivedState !== state) {
230
+ reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
203
231
  }
204
- }
205
- if (accessToken)
206
- break;
207
- // In headless mode, check for login error messages
208
- if (hasCredentials) {
209
- try {
210
- const errorEl = await page.$(".ant-message-error, .ant-alert-error");
211
- if (errorEl) {
212
- const errorText = await errorEl.textContent();
213
- throw new Error(`Login failed: ${errorText?.trim() || "unknown error"}`);
214
- }
232
+ else if (!receivedCode) {
233
+ reject(new Error("No authorization code received in callback."));
215
234
  }
216
- catch (e) {
217
- if (e instanceof Error && e.message.startsWith("Login failed:"))
218
- throw e;
235
+ else {
236
+ resolve(receivedCode);
219
237
  }
220
238
  }
221
- }
222
- if (!accessToken) {
223
- throw new Error(`Login timed out: dip.oauth2_token cookie not received within ${TIMEOUT_SECONDS} seconds.`);
224
- }
225
- const now = new Date();
226
- const tokenConfig = {
227
- baseUrl,
228
- accessToken,
229
- tokenType: "bearer",
230
- scope: "",
231
- expiresIn: TOKEN_TTL_SECONDS,
232
- expiresAt: new Date(now.getTime() + TOKEN_TTL_SECONDS * 1000).toISOString(),
233
- obtainedAt: now.toISOString(),
234
- };
235
- saveTokenConfig(tokenConfig);
236
- setCurrentPlatform(baseUrl);
237
- return tokenConfig;
238
- }
239
- finally {
240
- await browser.close();
241
- }
239
+ else {
240
+ res.writeHead(404);
241
+ res.end();
242
+ }
243
+ });
244
+ let browser;
245
+ server.listen(port, "127.0.0.1", async () => {
246
+ try {
247
+ browser = await chromium.launch({ headless: hasCredentials });
248
+ const context = await browser.newContext();
249
+ const page = await context.newPage();
250
+ // Navigate to OAuth2 auth URL — redirects to signin page
251
+ await page.goto(authUrl, { waitUntil: "networkidle", timeout: 30_000 });
252
+ if (hasCredentials) {
253
+ // Auto-fill credentials
254
+ await page.waitForSelector('input[name="account"]', { timeout: 10_000 });
255
+ await page.fill('input[name="account"]', options.username);
256
+ await page.fill('input[name="password"]', options.password);
257
+ await page.click("button.ant-btn-primary");
258
+ }
259
+ // else: visible browser — user logs in manually
260
+ // The OAuth2 callback will fire when login completes, resolving the promise above
261
+ }
262
+ catch (err) {
263
+ clearTimeout(timeoutId);
264
+ server.close();
265
+ browser?.close();
266
+ reject(err);
267
+ }
268
+ });
269
+ });
270
+ // Step 5: Exchange authorization code for tokens (includes refresh_token)
271
+ const token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri);
272
+ setCurrentPlatform(base);
273
+ return token;
242
274
  }
243
275
  function tokenNeedsRefresh(token) {
244
276
  if (!token.expiresAt) {
package/dist/cli.js CHANGED
@@ -11,47 +11,100 @@ function printHelp() {
11
11
  console.log(`kweaver
12
12
 
13
13
  Usage:
14
- kweaver auth <platform-url>
15
- kweaver auth login <platform-url>
16
- kweaver auth <platform-url> [--alias name] [--no-open] [--host host] [--redirect-uri uri]
17
- kweaver auth status [platform-url]
14
+ kweaver --version | -V
15
+ kweaver --help | -h
16
+
17
+ kweaver auth <platform-url> [--alias name] [-u user] [-p pass] [--playwright]
18
+ kweaver auth login <platform-url> (alias for auth <url>)
19
+ kweaver auth status [platform-url|alias]
18
20
  kweaver auth list
19
- kweaver auth use <platform-url>
20
- kweaver auth logout [platform-url]
21
- kweaver auth delete <platform-url>
21
+ kweaver auth use <platform-url|alias>
22
+ kweaver auth logout [platform-url|alias]
23
+ kweaver auth delete <platform-url|alias>
22
24
  kweaver token
23
- kweaver call <url> [-X METHOD] [-H "Name: value"] [-d BODY] [--pretty] [--verbose] [-bd value]
24
- kweaver agent chat <agent_id> [-m "message"] [--version value] [--conversation-id id] [--stream] [--no-stream] [--verbose] [-bd value]
25
- kweaver agent list [options]
26
- kweaver agent get <agent_id> [options]
27
- kweaver ds list [options]
25
+
26
+ kweaver call <url> [-X METHOD] [-H "Name: value"] [-d BODY] [--data-raw BODY]
27
+ [--url URL] [--pretty] [--verbose] [-bd value]
28
+ (alias: kweaver curl ...)
29
+
30
+ kweaver agent chat <agent_id> [-m "message"] [--version value] [--conversation-id id]
31
+ [--stream] [--no-stream] [--verbose] [-bd value]
32
+ kweaver agent list [--name X] [--limit N] [--offset N] [-bd value] [--pretty]
33
+ kweaver agent get <agent_id> [-bd value] [--pretty]
34
+ kweaver agent get-by-key <key> [-bd value] [--pretty]
35
+ kweaver agent sessions <agent_id> [-bd value] [--limit N] [--pretty]
36
+ kweaver agent history <agent_id> <session_id> [-bd value] [--limit N] [--pretty]
37
+ kweaver agent create [options]
38
+ kweaver agent update <agent_id> [options]
39
+ kweaver agent delete <agent_id> [-bd value]
40
+ kweaver agent publish <agent_id> [-bd value]
41
+ kweaver agent unpublish <agent_id> [-bd value]
42
+
43
+ kweaver ds list [--keyword X] [--type T] [-bd value] [--pretty]
28
44
  kweaver ds get <id>
29
- kweaver ds connect <db_type> <host> <port> <database> --account X --password Y
45
+ kweaver ds delete <id> [-y]
46
+ kweaver ds tables <id> [--keyword X] [--pretty]
47
+ kweaver ds connect <db_type> <host> <port> <database> --account X --password Y [--schema S] [--name N]
48
+
30
49
  kweaver bkn list [options]
31
50
  kweaver bkn get <kn-id> [options]
32
- kweaver bkn search <kn-id> <query> [options]
51
+ kweaver bkn search <kn-id> <query> [--max-concepts N] [--mode M] [--pretty] [-bd value]
33
52
  kweaver bkn create [options]
53
+ kweaver bkn create-from-ds [options]
34
54
  kweaver bkn update <kn-id> [options]
35
- kweaver bkn delete <kn-id>
36
- kweaver config [set-bd|show]
37
- kweaver vega [health|stats|inspect|catalog|resource|connector-type]
38
- kweaver context-loader [config|kn-search|...]
39
- kweaver --help
55
+ kweaver bkn delete <kn-id> [-y]
56
+ kweaver bkn build <kn-id> [--wait] [--no-wait] [--timeout N]
57
+ kweaver bkn validate <kn-id>
58
+ kweaver bkn export <kn-id>
59
+ kweaver bkn stats <kn-id>
60
+ kweaver bkn push <directory> [--branch main] [-bd value]
61
+ kweaver bkn pull <kn-id> [directory] [--branch main] [-bd value]
62
+ kweaver bkn object-type list|get|create|update|delete|query|properties <kn-id> ...
63
+ kweaver bkn relation-type list|get|create|update|delete <kn-id> ...
64
+ kweaver bkn subgraph <kn-id> <body-json>
65
+ kweaver bkn action-type list|query|execute <kn-id> ... [--wait] [--no-wait] [--timeout N]
66
+ kweaver bkn action-execution get <kn-id> <execution-id>
67
+ kweaver bkn action-log list|get|cancel <kn-id> ...
68
+
69
+ kweaver config set-bd <value>
70
+ kweaver config show
71
+
72
+ kweaver vega health|stats|inspect
73
+ kweaver vega catalog list|get|health|test-connection|discover|resources [options]
74
+ kweaver vega resource list|get|query|preview [options]
75
+ kweaver vega connector-type list|get [options]
76
+
77
+ kweaver context-loader config set|use|list|remove|show [options]
78
+ kweaver context-loader tools|resources|templates|prompts [--cursor]
79
+ kweaver context-loader resource <uri>
80
+ kweaver context-loader prompt <name> [--args json]
81
+ kweaver context-loader kn-search <query> [--only-schema]
82
+ kweaver context-loader kn-schema-search <query> [--max N]
83
+ kweaver context-loader query-object-instance|query-instance-subgraph|get-logic-properties|get-action-info ...
84
+ (alias: kweaver context ...)
40
85
 
41
86
  Commands:
42
87
  auth Login, list, inspect, and switch saved platform auth profiles
43
88
  token Print the current access token, refreshing it first if needed
44
- call Call an API with curl-style flags and auto-injected token headers
89
+ call (curl) Call an API with curl-style flags and auto-injected token headers
90
+ agent Agent CRUD, chat, sessions, history, publish/unpublish
45
91
  ds Manage datasources (list, get, delete, tables, connect)
46
- agent Chat with a KWeaver agent (agent chat <id>), list published agents (agent list)
47
- bkn Business knowledge network (list/get/create/update/delete/export/stats; object-type, subgraph, action-type, action-log)
92
+ bkn Knowledge network (CRUD, build, validate, export, stats, push/pull,
93
+ object-type, relation-type, subgraph, action-type, action-execution, action-log)
48
94
  config Per-platform configuration (business domain)
49
- vega Vega observability platform (catalogs, resources, connector-types, health)
50
- context-loader Call context-loader MCP (tools, resources, prompts; kn-search, query-*, etc.)
95
+ vega Vega observability (catalog, resource, connector-type, health/stats/inspect)
96
+ context-loader Context-loader MCP (config, tools, resources, prompts, kn-search, query-*, etc.)
51
97
  help Show this message`);
52
98
  }
53
99
  export async function run(argv) {
54
100
  const [command, ...rest] = argv;
101
+ if (command === "--version" || command === "-V" || command === "version") {
102
+ const { createRequire } = await import("node:module");
103
+ const require = createRequire(import.meta.url);
104
+ const pkg = require("../package.json");
105
+ console.log(pkg.version);
106
+ return 0;
107
+ }
55
108
  if (argv.length === 0 || !command || command === "--help" || command === "-h" || command === "help") {
56
109
  printHelp();
57
110
  return 0;
@@ -87,14 +140,28 @@ export async function run(argv) {
87
140
  printHelp();
88
141
  return 1;
89
142
  }
143
+ function safeExit(code) {
144
+ if (process.stdout.writableNeedDrain || process.stderr.writableNeedDrain) {
145
+ const done = () => {
146
+ if (!process.stdout.writableNeedDrain && !process.stderr.writableNeedDrain) {
147
+ process.exit(code);
148
+ }
149
+ };
150
+ process.stdout.once("drain", done);
151
+ process.stderr.once("drain", done);
152
+ }
153
+ else {
154
+ process.exit(code);
155
+ }
156
+ }
90
157
  if (import.meta.url === `file://${process.argv[1]}`) {
91
158
  run(process.argv.slice(2))
92
159
  .then((code) => {
93
- process.exit(code);
160
+ safeExit(code);
94
161
  })
95
162
  .catch((error) => {
96
163
  const message = error instanceof Error ? error.message : String(error);
97
164
  console.error(message);
98
- process.exit(1);
165
+ safeExit(1);
99
166
  });
100
167
  }
@@ -64,6 +64,42 @@ export interface KnObjectTypeQueryOptions {
64
64
  }
65
65
  export declare function parseKnObjectTypeQueryArgs(args: string[]): KnObjectTypeQueryOptions;
66
66
  export declare function runKnCommand(args: string[]): Promise<number>;
67
+ /** Fields merged via GET → modify → PUT (not raw body mode). */
68
+ export interface ObjectTypeMergeFields {
69
+ name?: string;
70
+ displayKey?: string;
71
+ addProperties: Record<string, unknown>[];
72
+ removeProperties: string[];
73
+ tags?: string[];
74
+ comment?: string;
75
+ icon?: string;
76
+ color?: string;
77
+ }
78
+ export type ObjectTypeUpdateParsed = {
79
+ mode: "body";
80
+ knId: string;
81
+ otId: string;
82
+ body: string;
83
+ businessDomain: string;
84
+ pretty: boolean;
85
+ } | {
86
+ mode: "merge";
87
+ knId: string;
88
+ otId: string;
89
+ merge: ObjectTypeMergeFields;
90
+ businessDomain: string;
91
+ pretty: boolean;
92
+ branch: string;
93
+ };
94
+ /** Prepare a GET response entry for PUT (drop read-only fields). */
95
+ export declare function stripObjectTypeForPut(entry: Record<string, unknown>): Record<string, unknown>;
96
+ /**
97
+ * Apply merge flags onto a stripped object-type object (mutates copy).
98
+ * - Add: property `name` not in list → append.
99
+ * - Update: property `name` exists → replace entry (same as add; CLI also accepts `--update-property`).
100
+ * - Delete: `--remove-property` removes by `name` before adds are applied.
101
+ */
102
+ export declare function applyObjectTypeMerge(target: Record<string, unknown>, merge: ObjectTypeMergeFields): Record<string, unknown>;
67
103
  export interface KnActionTypeExecuteOptions {
68
104
  knId: string;
69
105
  atId: string;
@@ -456,6 +456,57 @@ function parseJsonObject(text, errorMessage) {
456
456
  }
457
457
  return parsed;
458
458
  }
459
+ const MAX_OUTPUT_BYTES = 100_000;
460
+ /**
461
+ * If a query response exceeds MAX_OUTPUT_BYTES, trim the datas array
462
+ * to fit, preserving valid JSON and the search_after cursor for pagination.
463
+ */
464
+ function truncateQueryResult(raw) {
465
+ if (raw.length <= MAX_OUTPUT_BYTES) {
466
+ return raw;
467
+ }
468
+ let parsed;
469
+ try {
470
+ parsed = JSON.parse(raw);
471
+ }
472
+ catch {
473
+ return raw;
474
+ }
475
+ const datas = parsed.datas;
476
+ if (!Array.isArray(datas) || datas.length === 0) {
477
+ return raw;
478
+ }
479
+ const originalCount = datas.length;
480
+ while (datas.length > 1) {
481
+ datas.pop();
482
+ const candidate = JSON.stringify(parsed);
483
+ if (candidate.length <= MAX_OUTPUT_BYTES) {
484
+ const remaining = originalCount - datas.length;
485
+ const sa = parsed.search_after;
486
+ parsed._truncated = {
487
+ returned: datas.length,
488
+ total_fetched: originalCount,
489
+ remaining,
490
+ next_search_after: sa ?? null,
491
+ hint: sa
492
+ ? `Pass --search-after '${JSON.stringify(sa)}' --limit ${datas.length} to fetch the next page.`
493
+ : `Reduce --limit to ${datas.length} or less to avoid truncation.`,
494
+ };
495
+ console.error(`[warn] Truncated ${originalCount} → ${datas.length} records (output exceeded ${Math.round(MAX_OUTPUT_BYTES / 1024)}KB). ${parsed._truncated.hint}`);
496
+ return JSON.stringify(parsed);
497
+ }
498
+ }
499
+ const sa = parsed.search_after;
500
+ parsed._truncated = {
501
+ returned: 1,
502
+ total_fetched: originalCount,
503
+ remaining: originalCount - 1,
504
+ next_search_after: sa ?? null,
505
+ hint: `Single record is very large. Use --limit 1 and --search-after to iterate.`,
506
+ };
507
+ console.error(`[warn] Truncated ${originalCount} → 1 record. Single record is very large. Use --limit 1 and --search-after to iterate.`);
508
+ return JSON.stringify(parsed);
509
+ }
459
510
  function parseSearchAfterArray(text) {
460
511
  let parsed;
461
512
  try {
@@ -716,12 +767,84 @@ function parseObjectTypeCreateArgs(args) {
716
767
  businessDomain = resolveBusinessDomain();
717
768
  return { knId, body, businessDomain, branch, pretty };
718
769
  }
719
- /** Parse object-type update args: --name X [--display-key Y] */
770
+ const OBJECT_TYPE_PUT_STRIP_KEYS = new Set([
771
+ "status",
772
+ "creator",
773
+ "updater",
774
+ "create_time",
775
+ "update_time",
776
+ "module_type",
777
+ "kn_id",
778
+ ]);
779
+ /** Prepare a GET response entry for PUT (drop read-only fields). */
780
+ export function stripObjectTypeForPut(entry) {
781
+ const out = { ...entry };
782
+ for (const k of OBJECT_TYPE_PUT_STRIP_KEYS) {
783
+ delete out[k];
784
+ }
785
+ return out;
786
+ }
787
+ /**
788
+ * Apply merge flags onto a stripped object-type object (mutates copy).
789
+ * - Add: property `name` not in list → append.
790
+ * - Update: property `name` exists → replace entry (same as add; CLI also accepts `--update-property`).
791
+ * - Delete: `--remove-property` removes by `name` before adds are applied.
792
+ */
793
+ export function applyObjectTypeMerge(target, merge) {
794
+ if (merge.name !== undefined)
795
+ target.name = merge.name;
796
+ if (merge.displayKey !== undefined)
797
+ target.display_key = merge.displayKey;
798
+ if (merge.comment !== undefined)
799
+ target.comment = merge.comment;
800
+ if (merge.icon !== undefined)
801
+ target.icon = merge.icon;
802
+ if (merge.color !== undefined)
803
+ target.color = merge.color;
804
+ if (merge.tags !== undefined)
805
+ target.tags = merge.tags;
806
+ let props = target.data_properties;
807
+ if (!Array.isArray(props)) {
808
+ props = [];
809
+ }
810
+ else {
811
+ props = props.map((p) => p && typeof p === "object" && !Array.isArray(p) ? { ...p } : p);
812
+ }
813
+ const list = props;
814
+ for (const rm of merge.removeProperties) {
815
+ for (let j = list.length - 1; j >= 0; j -= 1) {
816
+ const n = list[j]?.name;
817
+ if (typeof n === "string" && n === rm)
818
+ list.splice(j, 1);
819
+ }
820
+ }
821
+ for (const add of merge.addProperties) {
822
+ const nm = add.name;
823
+ if (typeof nm !== "string" || !nm) {
824
+ throw new Error("--add-property / --update-property JSON must include a non-empty string \"name\" field.");
825
+ }
826
+ const idx = list.findIndex((p) => p?.name === nm);
827
+ if (idx >= 0)
828
+ list[idx] = add;
829
+ else
830
+ list.push(add);
831
+ }
832
+ target.data_properties = list;
833
+ return target;
834
+ }
835
+ /** Parse object-type update: raw JSON body OR merge flags (GET-merge-PUT). */
720
836
  function parseObjectTypeUpdateArgs(args) {
721
837
  let name;
722
838
  let displayKey;
723
839
  let businessDomain = "";
724
840
  let pretty = true;
841
+ let branch = "main";
842
+ let comment;
843
+ let icon;
844
+ let color;
845
+ let tagsJson;
846
+ const addProperties = [];
847
+ const removeProperties = [];
725
848
  const positional = [];
726
849
  for (let i = 0; i < args.length; i += 1) {
727
850
  const arg = args[i];
@@ -735,6 +858,35 @@ function parseObjectTypeUpdateArgs(args) {
735
858
  displayKey = args[++i];
736
859
  continue;
737
860
  }
861
+ if ((arg === "--add-property" || arg === "--update-property") && args[i + 1]) {
862
+ const raw = args[++i];
863
+ addProperties.push(parseJsonObject(raw, `--add-property / --update-property must be valid JSON object: ${raw}`));
864
+ continue;
865
+ }
866
+ if (arg === "--remove-property" && args[i + 1]) {
867
+ removeProperties.push(args[++i]);
868
+ continue;
869
+ }
870
+ if (arg === "--tags" && args[i + 1]) {
871
+ tagsJson = args[++i];
872
+ continue;
873
+ }
874
+ if (arg === "--comment" && args[i + 1]) {
875
+ comment = args[++i];
876
+ continue;
877
+ }
878
+ if (arg === "--icon" && args[i + 1]) {
879
+ icon = args[++i];
880
+ continue;
881
+ }
882
+ if (arg === "--color" && args[i + 1]) {
883
+ color = args[++i];
884
+ continue;
885
+ }
886
+ if (arg === "--branch" && args[i + 1]) {
887
+ branch = args[++i];
888
+ continue;
889
+ }
738
890
  if ((arg === "-bd" || arg === "--biz-domain") && args[i + 1]) {
739
891
  businessDomain = args[++i];
740
892
  continue;
@@ -746,21 +898,72 @@ function parseObjectTypeUpdateArgs(args) {
746
898
  if (!arg.startsWith("-"))
747
899
  positional.push(arg);
748
900
  }
749
- const [knId, otId] = positional;
901
+ const [knId, otId, maybeBody] = positional;
750
902
  if (!knId || !otId) {
751
- throw new Error("Usage: kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y]");
903
+ 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> ...]");
904
+ }
905
+ const hasMergeFlags = name !== undefined ||
906
+ displayKey !== undefined ||
907
+ addProperties.length > 0 ||
908
+ removeProperties.length > 0 ||
909
+ tagsJson !== undefined ||
910
+ comment !== undefined ||
911
+ icon !== undefined ||
912
+ color !== undefined;
913
+ if (maybeBody !== undefined && maybeBody.trim().startsWith("{")) {
914
+ if (hasMergeFlags) {
915
+ throw new Error("Do not combine a raw JSON body with --name/--add-property/--update-property/--remove-property and other merge flags.");
916
+ }
917
+ if (!businessDomain)
918
+ businessDomain = resolveBusinessDomain();
919
+ return {
920
+ mode: "body",
921
+ knId,
922
+ otId,
923
+ body: maybeBody.trim(),
924
+ businessDomain,
925
+ pretty,
926
+ };
752
927
  }
753
- const payload = {};
754
- if (name !== undefined)
755
- payload.name = name;
756
- if (displayKey !== undefined)
757
- payload.display_key = displayKey;
758
- if (Object.keys(payload).length === 0) {
759
- throw new Error("No update fields. Use --name or --display-key.");
928
+ if (maybeBody !== undefined) {
929
+ throw new Error(`Unexpected third argument "${maybeBody}". For raw PUT body, pass a single JSON object starting with "{".`);
930
+ }
931
+ let tags;
932
+ if (tagsJson !== undefined) {
933
+ try {
934
+ const t = JSON.parse(tagsJson);
935
+ if (!Array.isArray(t) || !t.every((x) => typeof x === "string")) {
936
+ throw new Error("invalid");
937
+ }
938
+ tags = t;
939
+ }
940
+ catch {
941
+ throw new Error(`--tags must be a JSON array of strings, e.g. '["足球","球员"]'`);
942
+ }
943
+ }
944
+ const merge = {
945
+ addProperties,
946
+ removeProperties,
947
+ ...(name !== undefined ? { name } : {}),
948
+ ...(displayKey !== undefined ? { displayKey } : {}),
949
+ ...(tags !== undefined ? { tags } : {}),
950
+ ...(comment !== undefined ? { comment } : {}),
951
+ ...(icon !== undefined ? { icon } : {}),
952
+ ...(color !== undefined ? { color } : {}),
953
+ };
954
+ if (merge.name === undefined &&
955
+ merge.displayKey === undefined &&
956
+ merge.addProperties.length === 0 &&
957
+ merge.removeProperties.length === 0 &&
958
+ merge.tags === undefined &&
959
+ merge.comment === undefined &&
960
+ merge.icon === undefined &&
961
+ merge.color === undefined) {
962
+ 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.");
760
963
  }
761
964
  if (!businessDomain)
762
965
  businessDomain = resolveBusinessDomain();
763
- return { knId, otId, body: JSON.stringify(payload), businessDomain, pretty };
966
+ return { mode: "merge", knId, otId, merge, businessDomain, pretty, branch };
764
967
  }
765
968
  /** Parse object-type delete args: <kn-id> <ot-ids> [-y] */
766
969
  function parseObjectTypeDeleteArgs(args) {
@@ -908,14 +1111,15 @@ async function runKnObjectTypeCommand(args) {
908
1111
  console.log(`kweaver bkn object-type list <kn-id> [--pretty] [-bd value]
909
1112
  kweaver bkn object-type get <kn-id> <ot-id> [--pretty] [-bd value]
910
1113
  kweaver bkn object-type create <kn-id> --name X --dataview-id Y --primary-key Z --display-key W [--property '<json>' ...]
911
- kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y]
1114
+ 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]
1115
+ kweaver bkn object-type update <kn-id> <ot-id> '<full-json-body>'
912
1116
  kweaver bkn object-type delete <kn-id> <ot-ids> [-y]
913
1117
  kweaver bkn object-type query <kn-id> <ot-id> ['<json>'] [--limit <n>] [--search-after '<json-array>'] [--pretty] [-bd value]
914
1118
  kweaver bkn object-type properties <kn-id> <ot-id> '<json>' [--pretty] [-bd value]
915
1119
 
916
1120
  list: List object types (schema) from ontology-manager.
917
1121
  get: Get single object type details.
918
- create/update/delete: Schema CRUD (create requires dataview-id).
1122
+ 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.
919
1123
  query: Query via ontology-query API. Default limit is 30 if not specified. Use --search-after for pagination.
920
1124
  properties: Query instance properties by primary key.
921
1125
 
@@ -958,12 +1162,37 @@ properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"p
958
1162
  if (action === "update") {
959
1163
  const opts = parseObjectTypeUpdateArgs(rest);
960
1164
  const token = await ensureValidToken();
1165
+ let putBody;
1166
+ if (opts.mode === "body") {
1167
+ putBody = opts.body;
1168
+ }
1169
+ else {
1170
+ const raw = await getObjectType({
1171
+ baseUrl: token.baseUrl,
1172
+ accessToken: token.accessToken,
1173
+ knId: opts.knId,
1174
+ otId: opts.otId,
1175
+ businessDomain: opts.businessDomain,
1176
+ branch: opts.branch,
1177
+ });
1178
+ const parsed = JSON.parse(raw);
1179
+ const entryUnknown = parsed.entries;
1180
+ const entry = Array.isArray(entryUnknown) && entryUnknown.length > 0 && entryUnknown[0] && typeof entryUnknown[0] === "object"
1181
+ ? entryUnknown[0]
1182
+ : parsed;
1183
+ if (!entry || typeof entry !== "object") {
1184
+ throw new Error("Unexpected object-type GET response shape.");
1185
+ }
1186
+ const stripped = stripObjectTypeForPut(entry);
1187
+ applyObjectTypeMerge(stripped, opts.merge);
1188
+ putBody = JSON.stringify(stripped);
1189
+ }
961
1190
  const body = await updateObjectType({
962
1191
  baseUrl: token.baseUrl,
963
1192
  accessToken: token.accessToken,
964
1193
  knId: opts.knId,
965
1194
  otId: opts.otId,
966
- body: opts.body,
1195
+ body: putBody,
967
1196
  businessDomain: opts.businessDomain,
968
1197
  });
969
1198
  console.log(formatCallOutput(body, opts.pretty));
@@ -1017,11 +1246,7 @@ properties JSON format: {"_instance_identities":[{"<primary-key>":"<value>"}],"p
1017
1246
  body: options.body,
1018
1247
  businessDomain: options.businessDomain,
1019
1248
  });
1020
- const OUTPUT_WARN_BYTES = 100_000;
1021
- if (result.length > OUTPUT_WARN_BYTES) {
1022
- console.error(`[warn] Response is ${(result.length / 1024).toFixed(0)}KB. Use a smaller --limit or --search-after to paginate.`);
1023
- }
1024
- console.log(formatCallOutput(result, options.pretty));
1249
+ console.log(formatCallOutput(truncateQueryResult(result), options.pretty));
1025
1250
  return 0;
1026
1251
  }
1027
1252
  if (action === "properties") {
@@ -1050,7 +1275,8 @@ JSON: {"_instance_identities":[{"<primary-key>":"<value>"}],"properties":["prop1
1050
1275
  catch (error) {
1051
1276
  if (error instanceof Error && error.message === "help") {
1052
1277
  console.log(`kweaver bkn object-type create <kn-id> --name X --dataview-id Y --primary-key Z --display-key W [--property '<json>' ...]
1053
- kweaver bkn object-type update <kn-id> <ot-id> [--name X] [--display-key Y]
1278
+ 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]
1279
+ kweaver bkn object-type update <kn-id> <ot-id> '<full-json-body>'
1054
1280
  kweaver bkn object-type delete <kn-id> <ot-ids> [-y]`);
1055
1281
  return 0;
1056
1282
  }
@@ -125,7 +125,7 @@ Options:
125
125
  <url> API path (e.g. /api/ontology-manager/v1/knowledge-networks)
126
126
  -X, --request HTTP method (default: GET)
127
127
  -H, --header Extra header (repeatable)
128
- -d, --data JSON request body
128
+ -d, --data, --data-raw JSON request body (sets Content-Type: application/json if not set)
129
129
  -bd, --biz-domain Override x-business-domain (default: bd_public)
130
130
  -v, --verbose Print request info to stderr
131
131
  --pretty Pretty-print JSON output (default)`);
@@ -147,6 +147,12 @@ Options:
147
147
  : invocation.url;
148
148
  const headers = new Headers(invocation.headers);
149
149
  injectAuthHeaders(headers, token.accessToken, invocation.businessDomain);
150
+ if (invocation.body !== undefined &&
151
+ invocation.body.length > 0 &&
152
+ !headers.has("content-type") &&
153
+ !headers.has("Content-Type")) {
154
+ headers.set("content-type", "application/json");
155
+ }
150
156
  if (invocation.verbose) {
151
157
  for (const line of formatVerboseRequest({ ...invocation, url, headers })) {
152
158
  console.error(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kweaver-ai/kweaver-sdk",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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,7 @@
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
32
  "prepublishOnly": "npm run build"
33
33
  },
34
34
  "keywords": [