@kweaver-ai/kweaver-sdk 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/README.zh.md +5 -0
- package/dist/api/agent-chat.d.ts +1 -1
- package/dist/api/agent-chat.js +4 -4
- package/dist/api/agent-list.d.ts +35 -0
- package/dist/api/agent-list.js +86 -12
- package/dist/api/bkn-backend.d.ts +60 -0
- package/dist/api/bkn-backend.js +103 -10
- package/dist/api/conversations.d.ts +6 -3
- package/dist/api/conversations.js +26 -27
- package/dist/api/dataflow.js +1 -10
- package/dist/api/datasources.js +1 -10
- package/dist/api/dataviews.js +1 -10
- package/dist/api/headers.d.ts +9 -0
- package/dist/api/headers.js +25 -0
- package/dist/api/knowledge-networks.d.ts +41 -0
- package/dist/api/knowledge-networks.js +69 -22
- package/dist/api/ontology-query.d.ts +14 -1
- package/dist/api/ontology-query.js +63 -49
- package/dist/api/semantic-search.js +2 -12
- package/dist/api/skills.d.ts +141 -0
- package/dist/api/skills.js +216 -0
- package/dist/api/vega.d.ts +63 -0
- package/dist/api/vega.js +130 -10
- package/dist/auth/oauth.d.ts +5 -1
- package/dist/auth/oauth.js +293 -94
- package/dist/cli.js +28 -4
- package/dist/client.d.ts +3 -0
- package/dist/client.js +4 -0
- package/dist/commands/agent.d.ts +33 -1
- package/dist/commands/agent.js +721 -49
- package/dist/commands/auth.js +156 -33
- package/dist/commands/bkn-ops.d.ts +77 -0
- package/dist/commands/bkn-ops.js +1056 -0
- package/dist/commands/bkn-query.d.ts +14 -0
- package/dist/commands/bkn-query.js +370 -0
- package/dist/commands/bkn-schema.d.ts +135 -0
- package/dist/commands/bkn-schema.js +1461 -0
- package/dist/commands/bkn-utils.d.ts +36 -0
- package/dist/commands/bkn-utils.js +102 -0
- package/dist/commands/bkn.d.ts +7 -113
- package/dist/commands/bkn.js +175 -2429
- package/dist/commands/dataview.d.ts +7 -0
- package/dist/commands/dataview.js +38 -2
- package/dist/commands/ds.d.ts +1 -0
- package/dist/commands/ds.js +8 -1
- package/dist/commands/import-csv.d.ts +2 -0
- package/dist/commands/import-csv.js +3 -2
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.js +524 -0
- package/dist/commands/vega.js +371 -14
- package/dist/config/jwt.d.ts +6 -0
- package/dist/config/jwt.js +21 -0
- package/dist/config/store.d.ts +37 -5
- package/dist/config/store.js +363 -30
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/resources/bkn.d.ts +4 -0
- package/dist/resources/bkn.js +4 -0
- package/dist/resources/conversations.d.ts +5 -2
- package/dist/resources/conversations.js +17 -3
- package/dist/resources/skills.d.ts +47 -0
- package/dist/resources/skills.js +47 -0
- package/dist/resources/vega.d.ts +11 -0
- package/dist/resources/vega.js +37 -1
- package/package.json +1 -1
package/dist/auth/oauth.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
import { getCurrentPlatform, loadClientConfig, loadTokenConfig, saveClientConfig, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
|
|
1
|
+
import { deleteClientConfig, getCurrentPlatform, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveUserId, saveClientConfig, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
|
|
2
2
|
import { HttpError, NetworkRequestError } from "../utils/http.js";
|
|
3
3
|
const TOKEN_TTL_SECONDS = 3600;
|
|
4
4
|
/** Seconds before access token expiry to trigger refresh (matches Python ConfigAuth). */
|
|
5
5
|
const REFRESH_THRESHOLD_SEC = 60;
|
|
6
6
|
const DEFAULT_REDIRECT_PORT = 9010;
|
|
7
7
|
const DEFAULT_SCOPE = "openid offline all";
|
|
8
|
+
/** Best-effort fetch of display name via EACP userinfo (ShareServer). */
|
|
9
|
+
async function fetchDisplayName(baseUrl, accessToken, tlsInsecure) {
|
|
10
|
+
try {
|
|
11
|
+
const res = await runWithTlsInsecure(tlsInsecure, () => fetch(`${baseUrl}/api/eacp/v1/user/get`, {
|
|
12
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
|
|
13
|
+
}));
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
return null;
|
|
16
|
+
const info = (await res.json());
|
|
17
|
+
if (typeof info.account === "string")
|
|
18
|
+
return info.account;
|
|
19
|
+
if (typeof info.name === "string")
|
|
20
|
+
return info.name;
|
|
21
|
+
if (typeof info.mail === "string")
|
|
22
|
+
return info.mail;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* Non-critical — displayName will be absent. */
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
8
29
|
/** POSIX shell single-quote escaping for copy-paste commands. */
|
|
9
30
|
export function shellQuoteForShell(value) {
|
|
10
31
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -118,11 +139,132 @@ async function generatePkce() {
|
|
|
118
139
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
119
140
|
return { verifier, challenge };
|
|
120
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Pre-flight check: verify that a cached OAuth2 client is still recognised
|
|
144
|
+
* by the server. Fetches the authorization endpoint with `redirect: "manual"`
|
|
145
|
+
* and inspects the Location header. Returns false when Hydra redirects to an
|
|
146
|
+
* error page containing `invalid_client` or similar indicators.
|
|
147
|
+
*/
|
|
148
|
+
async function isClientStillValid(baseUrl, clientId, redirectUri) {
|
|
149
|
+
try {
|
|
150
|
+
const params = new URLSearchParams({
|
|
151
|
+
client_id: clientId,
|
|
152
|
+
response_type: "code",
|
|
153
|
+
scope: "openid",
|
|
154
|
+
redirect_uri: redirectUri,
|
|
155
|
+
state: "preflight",
|
|
156
|
+
});
|
|
157
|
+
const resp = await fetch(`${baseUrl}/oauth2/auth?${params}`, {
|
|
158
|
+
redirect: "manual",
|
|
159
|
+
});
|
|
160
|
+
if (resp.status === 302) {
|
|
161
|
+
const location = resp.headers.get("location") ?? "";
|
|
162
|
+
if (location.includes("error=invalid_client") ||
|
|
163
|
+
location.includes("error=error")) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Non-redirect — Hydra might serve an error page directly
|
|
169
|
+
if (resp.status >= 400)
|
|
170
|
+
return false;
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Network error — cannot pre-validate; let the real flow proceed
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resolve a cached client or register a new one. When a cached client fails
|
|
180
|
+
* pre-flight validation (stale registration after server reset), the local
|
|
181
|
+
* client.json is deleted and a fresh registration is performed.
|
|
182
|
+
*/
|
|
183
|
+
async function resolveOrRegisterClient(baseUrl, redirectUri, scope, options) {
|
|
184
|
+
if (options?.clientId) {
|
|
185
|
+
const client = {
|
|
186
|
+
baseUrl,
|
|
187
|
+
clientId: options.clientId,
|
|
188
|
+
clientSecret: options.clientSecret ?? "",
|
|
189
|
+
redirectUri,
|
|
190
|
+
logoutRedirectUri: redirectUri.replace("/callback", "/successful-logout"),
|
|
191
|
+
scope,
|
|
192
|
+
lang: "zh-cn",
|
|
193
|
+
product: "adp",
|
|
194
|
+
xForwardedPrefix: "",
|
|
195
|
+
};
|
|
196
|
+
saveClientConfig(baseUrl, client);
|
|
197
|
+
return client;
|
|
198
|
+
}
|
|
199
|
+
let client = loadClientConfig(baseUrl);
|
|
200
|
+
if (client?.clientId) {
|
|
201
|
+
const valid = await isClientStillValid(baseUrl, client.clientId, redirectUri);
|
|
202
|
+
if (valid)
|
|
203
|
+
return client;
|
|
204
|
+
process.stderr.write("Cached OAuth2 client is no longer valid on the server. Re-registering…\n");
|
|
205
|
+
deleteClientConfig(baseUrl);
|
|
206
|
+
client = null;
|
|
207
|
+
}
|
|
208
|
+
const registered = await registerOAuth2Client(baseUrl, redirectUri, scope);
|
|
209
|
+
saveClientConfig(baseUrl, registered);
|
|
210
|
+
return registered;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Parse a redirect URI to extract host, port, and pathname.
|
|
214
|
+
* Returns null if the URI is not a valid HTTP(S) URL.
|
|
215
|
+
*/
|
|
216
|
+
function parseRedirectUri(uri) {
|
|
217
|
+
try {
|
|
218
|
+
const parsed = new URL(uri);
|
|
219
|
+
const host = parsed.hostname;
|
|
220
|
+
const port = parsed.port ? Number(parsed.port) : (parsed.protocol === "https:" ? 443 : 80);
|
|
221
|
+
const isLocalhost = host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
222
|
+
return { host, port, pathname: parsed.pathname, isLocalhost };
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Manual code flow for non-localhost redirect URIs.
|
|
230
|
+
* Prints the auth URL, then reads the full callback URL from stdin
|
|
231
|
+
* to extract the authorization code.
|
|
232
|
+
*/
|
|
233
|
+
async function waitForManualCode(authUrl, state) {
|
|
234
|
+
const { createInterface } = await import("node:readline");
|
|
235
|
+
process.stderr.write("\nSince the redirect URI is not localhost, you need to complete login manually.\n" +
|
|
236
|
+
"1. Open this URL in your browser:\n\n" +
|
|
237
|
+
` ${authUrl}\n\n` +
|
|
238
|
+
"2. After login, the browser will redirect to your callback URL.\n" +
|
|
239
|
+
"3. Copy the full callback URL and paste it here:\n\n");
|
|
240
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
241
|
+
const callbackUrl = await new Promise((resolve) => {
|
|
242
|
+
rl.question("Callback URL> ", (answer) => {
|
|
243
|
+
rl.close();
|
|
244
|
+
resolve(answer.trim());
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
const parsed = new URL(callbackUrl);
|
|
248
|
+
const receivedState = parsed.searchParams.get("state");
|
|
249
|
+
if (receivedState !== state) {
|
|
250
|
+
throw new Error("OAuth2 state mismatch — possible CSRF attack.");
|
|
251
|
+
}
|
|
252
|
+
const error = parsed.searchParams.get("error");
|
|
253
|
+
if (error) {
|
|
254
|
+
const desc = parsed.searchParams.get("error_description") ?? "";
|
|
255
|
+
throw new Error(desc ? `Authorization failed: ${error} — ${desc}` : `Authorization failed: ${error}`);
|
|
256
|
+
}
|
|
257
|
+
const code = parsed.searchParams.get("code");
|
|
258
|
+
if (!code) {
|
|
259
|
+
throw new Error("No authorization code found in the callback URL.");
|
|
260
|
+
}
|
|
261
|
+
return code;
|
|
262
|
+
}
|
|
121
263
|
/**
|
|
122
264
|
* OAuth2 Authorization Code login flow.
|
|
123
265
|
* 1. Register client (if not already registered), OR use a provided client ID
|
|
124
266
|
* 2. Open browser to /oauth2/auth
|
|
125
|
-
* 3. Receive authorization code via local HTTP callback
|
|
267
|
+
* 3. Receive authorization code via local HTTP callback (or manual paste for non-localhost)
|
|
126
268
|
* 4. Exchange code for access_token + refresh_token
|
|
127
269
|
* 5. Save token.json + client.json to ~/.kweaver/
|
|
128
270
|
*/
|
|
@@ -133,29 +275,14 @@ export async function oauth2Login(baseUrl, options) {
|
|
|
133
275
|
const base = normalizeBaseUrl(baseUrl);
|
|
134
276
|
const port = options?.port ?? DEFAULT_REDIRECT_PORT;
|
|
135
277
|
const scope = options?.scope ?? DEFAULT_SCOPE;
|
|
136
|
-
|
|
278
|
+
// Determine redirect URI: explicit option > port-based default
|
|
279
|
+
const redirectUri = options?.redirectUri ?? `http://127.0.0.1:${port}/callback`;
|
|
280
|
+
const parsedRedirect = parseRedirectUri(redirectUri);
|
|
281
|
+
const isLocalRedirect = parsedRedirect?.isLocalhost ?? true;
|
|
282
|
+
const listenPort = parsedRedirect?.port ?? port;
|
|
283
|
+
const callbackPathname = parsedRedirect?.pathname ?? "/callback";
|
|
137
284
|
// Step 1: Determine client — use provided client ID or fall back to dynamic registration
|
|
138
|
-
let client =
|
|
139
|
-
if (options?.clientId) {
|
|
140
|
-
// Use the platform's existing client (e.g. the web app client).
|
|
141
|
-
// Persist it so future logins reuse it without re-registering.
|
|
142
|
-
client = {
|
|
143
|
-
baseUrl: base,
|
|
144
|
-
clientId: options.clientId,
|
|
145
|
-
clientSecret: options.clientSecret ?? "",
|
|
146
|
-
redirectUri,
|
|
147
|
-
logoutRedirectUri: redirectUri.replace("/callback", "/successful-logout"),
|
|
148
|
-
scope,
|
|
149
|
-
lang: "zh-cn",
|
|
150
|
-
product: "adp",
|
|
151
|
-
xForwardedPrefix: "",
|
|
152
|
-
};
|
|
153
|
-
saveClientConfig(base, client);
|
|
154
|
-
}
|
|
155
|
-
else if (!client?.clientId) {
|
|
156
|
-
client = await registerOAuth2Client(base, redirectUri, scope);
|
|
157
|
-
saveClientConfig(base, client);
|
|
158
|
-
}
|
|
285
|
+
let client = await resolveOrRegisterClient(base, redirectUri, scope, options);
|
|
159
286
|
// Use PKCE when no client secret is available (public client / platform client).
|
|
160
287
|
const usePkce = !client.clientSecret;
|
|
161
288
|
const pkce = usePkce ? await generatePkce() : null;
|
|
@@ -177,71 +304,91 @@ export async function oauth2Login(baseUrl, options) {
|
|
|
177
304
|
authParams.set("code_challenge_method", "S256");
|
|
178
305
|
}
|
|
179
306
|
const authUrl = `${base}/oauth2/auth?${authParams.toString()}`;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
server
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
307
|
+
let token;
|
|
308
|
+
if (isLocalRedirect) {
|
|
309
|
+
// Step 4a: Local callback — start HTTP server to receive the authorization code
|
|
310
|
+
token = await new Promise((resolve, reject) => {
|
|
311
|
+
let server;
|
|
312
|
+
const timeoutId = setTimeout(() => {
|
|
313
|
+
server?.close();
|
|
314
|
+
reject(new Error("OAuth2 login timed out (120s). No authorization code received."));
|
|
315
|
+
}, 120_000);
|
|
316
|
+
server = createServer((req, res) => {
|
|
317
|
+
void (async () => {
|
|
318
|
+
try {
|
|
319
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${listenPort}`);
|
|
320
|
+
if (url.pathname !== callbackPathname) {
|
|
321
|
+
res.writeHead(404);
|
|
322
|
+
res.end();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const receivedState = url.searchParams.get("state");
|
|
326
|
+
const code = url.searchParams.get("code");
|
|
327
|
+
const callbackError = url.searchParams.get("error");
|
|
328
|
+
const callbackErrorDesc = url.searchParams.get("error_description");
|
|
329
|
+
if (receivedState !== state) {
|
|
330
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
331
|
+
res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
|
|
332
|
+
clearTimeout(timeoutId);
|
|
333
|
+
server.close();
|
|
334
|
+
reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (callbackError) {
|
|
338
|
+
const msg = callbackErrorDesc
|
|
339
|
+
? `Authorization failed: ${callbackError} — ${callbackErrorDesc}`
|
|
340
|
+
: `Authorization failed: ${callbackError}`;
|
|
341
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
342
|
+
res.end(buildCallbackExchangeErrorHtml(msg));
|
|
343
|
+
clearTimeout(timeoutId);
|
|
344
|
+
server.close();
|
|
345
|
+
reject(new Error(msg));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!code) {
|
|
349
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
350
|
+
res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
|
|
351
|
+
clearTimeout(timeoutId);
|
|
352
|
+
server.close();
|
|
353
|
+
reject(new Error("No authorization code received in callback."));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const exchanged = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
|
|
357
|
+
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
|
|
358
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
359
|
+
res.end(buildCallbackHtml(copyCommand));
|
|
201
360
|
clearTimeout(timeoutId);
|
|
202
361
|
server.close();
|
|
203
|
-
|
|
204
|
-
return;
|
|
362
|
+
resolve(exchanged);
|
|
205
363
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
364
|
+
catch (err) {
|
|
365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
+
try {
|
|
367
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
368
|
+
res.end(buildCallbackExchangeErrorHtml(message));
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
/* response may already be sent */
|
|
372
|
+
}
|
|
209
373
|
clearTimeout(timeoutId);
|
|
210
374
|
server.close();
|
|
211
|
-
reject(
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const exchanged = await exchangeCodeForToken(base, receivedCode, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
|
|
215
|
-
const copyCommand = buildCopyCommand(base, client.clientId, client.clientSecret, exchanged.refreshToken, options?.tlsInsecure);
|
|
216
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
217
|
-
res.end(buildCallbackHtml(copyCommand));
|
|
218
|
-
clearTimeout(timeoutId);
|
|
219
|
-
server.close();
|
|
220
|
-
resolve(exchanged);
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
-
try {
|
|
225
|
-
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
226
|
-
res.end(buildCallbackExchangeErrorHtml(message));
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
/* response may already be sent */
|
|
375
|
+
reject(err instanceof Error ? err : new Error(message));
|
|
230
376
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Step 5: Open browser (uses spawn with proper Windows quoting)
|
|
239
|
-
import("../utils/browser.js").then(({ openBrowser }) => {
|
|
240
|
-
openBrowser(authUrl);
|
|
377
|
+
})();
|
|
378
|
+
});
|
|
379
|
+
server.listen(listenPort, "127.0.0.1", () => {
|
|
380
|
+
import("../utils/browser.js").then(({ openBrowser }) => {
|
|
381
|
+
openBrowser(authUrl);
|
|
382
|
+
});
|
|
383
|
+
process.stderr.write(`If the wrong browser opens, copy this URL to your correct browser:\n ${authUrl}\n`);
|
|
241
384
|
});
|
|
242
|
-
process.stderr.write(`If the wrong browser opens, copy this URL to your correct browser:\n ${authUrl}\n`);
|
|
243
385
|
});
|
|
244
|
-
}
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// Step 4b: Non-localhost redirect — manual code entry flow
|
|
389
|
+
const code = await waitForManualCode(authUrl, state);
|
|
390
|
+
token = await exchangeCodeForToken(base, code, client.clientId, client.clientSecret, redirectUri, pkce?.verifier, options?.tlsInsecure);
|
|
391
|
+
}
|
|
245
392
|
setCurrentPlatform(base);
|
|
246
393
|
return token;
|
|
247
394
|
});
|
|
@@ -329,6 +476,9 @@ async function exchangeCodeForToken(baseUrl, code, clientId, clientSecret, redir
|
|
|
329
476
|
obtainedAt: now.toISOString(),
|
|
330
477
|
...(tlsInsecure ? { tlsInsecure: true } : {}),
|
|
331
478
|
};
|
|
479
|
+
const displayName = await fetchDisplayName(baseUrl, data.access_token, tlsInsecure);
|
|
480
|
+
if (displayName)
|
|
481
|
+
token.displayName = displayName;
|
|
332
482
|
saveTokenConfig(token);
|
|
333
483
|
return token;
|
|
334
484
|
}
|
|
@@ -359,14 +509,13 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
359
509
|
const base = normalizeBaseUrl(baseUrl);
|
|
360
510
|
const port = options?.port ?? DEFAULT_REDIRECT_PORT;
|
|
361
511
|
const scope = options?.scope ?? DEFAULT_SCOPE;
|
|
362
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
512
|
+
const redirectUri = options?.redirectUri ?? `http://127.0.0.1:${port}/callback`;
|
|
513
|
+
const parsedRedirect = parseRedirectUri(redirectUri);
|
|
514
|
+
const listenPort = parsedRedirect?.port ?? port;
|
|
515
|
+
const callbackPathname = parsedRedirect?.pathname ?? "/callback";
|
|
363
516
|
const hasCredentials = !!(options?.username && options?.password);
|
|
364
|
-
// Step 1: Ensure registered OAuth2 client
|
|
365
|
-
let client =
|
|
366
|
-
if (!client?.clientId) {
|
|
367
|
-
client = await registerOAuth2Client(base, redirectUri, scope);
|
|
368
|
-
saveClientConfig(base, client);
|
|
369
|
-
}
|
|
517
|
+
// Step 1: Ensure registered OAuth2 client (with stale-client auto-recovery)
|
|
518
|
+
let client = await resolveOrRegisterClient(base, redirectUri, scope);
|
|
370
519
|
// Step 2: Generate CSRF state
|
|
371
520
|
const state = randomBytes(12).toString("hex");
|
|
372
521
|
// Step 3: Build authorization URL
|
|
@@ -394,14 +543,16 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
394
543
|
server = createServer((req, res) => {
|
|
395
544
|
void (async () => {
|
|
396
545
|
try {
|
|
397
|
-
const url = new URL(req.url ?? "/", `http://127.0.0.1:${
|
|
398
|
-
if (url.pathname !==
|
|
546
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${listenPort}`);
|
|
547
|
+
if (url.pathname !== callbackPathname) {
|
|
399
548
|
res.writeHead(404);
|
|
400
549
|
res.end();
|
|
401
550
|
return;
|
|
402
551
|
}
|
|
403
552
|
const receivedState = url.searchParams.get("state");
|
|
404
553
|
const receivedCode = url.searchParams.get("code");
|
|
554
|
+
const callbackError = url.searchParams.get("error");
|
|
555
|
+
const callbackErrorDesc = url.searchParams.get("error_description");
|
|
405
556
|
if (receivedState !== state) {
|
|
406
557
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
407
558
|
res.end(buildCallbackExchangeErrorHtml("OAuth2 state mismatch — possible CSRF attack."));
|
|
@@ -411,6 +562,18 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
411
562
|
reject(new Error("OAuth2 state mismatch — possible CSRF attack."));
|
|
412
563
|
return;
|
|
413
564
|
}
|
|
565
|
+
if (callbackError) {
|
|
566
|
+
const msg = callbackErrorDesc
|
|
567
|
+
? `Authorization failed: ${callbackError} — ${callbackErrorDesc}`
|
|
568
|
+
: `Authorization failed: ${callbackError}`;
|
|
569
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
570
|
+
res.end(buildCallbackExchangeErrorHtml(msg));
|
|
571
|
+
clearTimeout(timeoutId);
|
|
572
|
+
server.close();
|
|
573
|
+
browser?.close();
|
|
574
|
+
reject(new Error(msg));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
414
577
|
if (!receivedCode) {
|
|
415
578
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
416
579
|
res.end(buildCallbackExchangeErrorHtml("No authorization code received in callback."));
|
|
@@ -445,7 +608,7 @@ export async function playwrightLogin(baseUrl, options) {
|
|
|
445
608
|
}
|
|
446
609
|
})();
|
|
447
610
|
});
|
|
448
|
-
server.listen(
|
|
611
|
+
server.listen(listenPort, "127.0.0.1", async () => {
|
|
449
612
|
try {
|
|
450
613
|
browser = await chromium.launch({ headless: hasCredentials });
|
|
451
614
|
const context = await browser.newContext({ ignoreHTTPSErrors: !!options?.tlsInsecure });
|
|
@@ -577,6 +740,10 @@ export async function refreshAccessToken(token) {
|
|
|
577
740
|
}
|
|
578
741
|
const now = new Date();
|
|
579
742
|
const expiresIn = typeof data.expires_in === "number" ? data.expires_in : 3600;
|
|
743
|
+
let displayName = token.displayName;
|
|
744
|
+
if (!displayName) {
|
|
745
|
+
displayName = (await fetchDisplayName(baseUrl, data.access_token, token.tlsInsecure)) ?? undefined;
|
|
746
|
+
}
|
|
580
747
|
const newToken = {
|
|
581
748
|
baseUrl,
|
|
582
749
|
accessToken: data.access_token,
|
|
@@ -588,6 +755,7 @@ export async function refreshAccessToken(token) {
|
|
|
588
755
|
idToken: data.id_token ?? token.idToken ?? "",
|
|
589
756
|
obtainedAt: now.toISOString(),
|
|
590
757
|
...(token.tlsInsecure ? { tlsInsecure: true } : {}),
|
|
758
|
+
...(displayName ? { displayName } : {}),
|
|
591
759
|
};
|
|
592
760
|
saveTokenConfig(newToken);
|
|
593
761
|
return newToken;
|
|
@@ -618,7 +786,20 @@ export async function ensureValidToken(opts) {
|
|
|
618
786
|
if (!currentPlatform) {
|
|
619
787
|
throw new Error("No active platform selected. Run `kweaver auth login <platform-url>` first.");
|
|
620
788
|
}
|
|
621
|
-
|
|
789
|
+
// KWEAVER_USER: load a specific user's token without switching active user
|
|
790
|
+
const envUser = process.env.KWEAVER_USER;
|
|
791
|
+
let token;
|
|
792
|
+
if (envUser) {
|
|
793
|
+
const userId = resolveUserId(currentPlatform, envUser);
|
|
794
|
+
if (!userId) {
|
|
795
|
+
throw new Error(`User '${envUser}' not found for ${currentPlatform}. ` +
|
|
796
|
+
"Run `kweaver auth users` to see available users.");
|
|
797
|
+
}
|
|
798
|
+
token = loadUserTokenConfig(currentPlatform, userId);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
token = loadTokenConfig(currentPlatform);
|
|
802
|
+
}
|
|
622
803
|
if (!token) {
|
|
623
804
|
throw new Error(`No saved token for ${currentPlatform}. Run \`kweaver auth login ${currentPlatform}\` first.`);
|
|
624
805
|
}
|
|
@@ -652,7 +833,15 @@ export async function with401RefreshRetry(fn) {
|
|
|
652
833
|
throw error;
|
|
653
834
|
}
|
|
654
835
|
const platformUrl = normalizeBaseUrl(currentPlatform);
|
|
655
|
-
const
|
|
836
|
+
const envUser = process.env.KWEAVER_USER;
|
|
837
|
+
let latest;
|
|
838
|
+
if (envUser) {
|
|
839
|
+
const userId = resolveUserId(platformUrl, envUser);
|
|
840
|
+
latest = userId ? loadUserTokenConfig(platformUrl, userId) : null;
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
latest = loadTokenConfig(platformUrl);
|
|
844
|
+
}
|
|
656
845
|
if (!latest) {
|
|
657
846
|
throw error;
|
|
658
847
|
}
|
|
@@ -681,7 +870,17 @@ export async function withTokenRetry(fn) {
|
|
|
681
870
|
catch (error) {
|
|
682
871
|
if (error instanceof HttpError && error.status === 401) {
|
|
683
872
|
const platformUrl = normalizeBaseUrl(token.baseUrl);
|
|
684
|
-
const
|
|
873
|
+
const envUser = process.env.KWEAVER_USER;
|
|
874
|
+
let latest;
|
|
875
|
+
if (envUser) {
|
|
876
|
+
const userId = resolveUserId(platformUrl, envUser);
|
|
877
|
+
latest = userId ? loadUserTokenConfig(platformUrl, userId) : null;
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
latest = loadTokenConfig(platformUrl);
|
|
881
|
+
}
|
|
882
|
+
if (!latest)
|
|
883
|
+
latest = token;
|
|
685
884
|
try {
|
|
686
885
|
const refreshed = await refreshAccessToken(latest);
|
|
687
886
|
return await fn(refreshed);
|
package/dist/cli.js
CHANGED
|
@@ -7,12 +7,14 @@ import { runConfigCommand } from "./commands/config.js";
|
|
|
7
7
|
import { runContextLoaderCommand } from "./commands/context-loader.js";
|
|
8
8
|
import { runDsCommand } from "./commands/ds.js";
|
|
9
9
|
import { runDataviewCommand } from "./commands/dataview.js";
|
|
10
|
+
import { runSkillCommand } from "./commands/skill.js";
|
|
10
11
|
import { runTokenCommand } from "./commands/token.js";
|
|
11
12
|
import { runVegaCommand } from "./commands/vega.js";
|
|
12
13
|
function printHelp() {
|
|
13
14
|
console.log(`kweaver
|
|
14
15
|
|
|
15
16
|
Usage:
|
|
17
|
+
kweaver [--user <userId|username>] <command> [options]
|
|
16
18
|
kweaver --version | -V
|
|
17
19
|
kweaver --help | -h
|
|
18
20
|
|
|
@@ -24,8 +26,10 @@ Usage:
|
|
|
24
26
|
kweaver auth status [platform-url|alias]
|
|
25
27
|
kweaver auth list
|
|
26
28
|
kweaver auth use <platform-url|alias>
|
|
27
|
-
kweaver auth
|
|
28
|
-
kweaver auth
|
|
29
|
+
kweaver auth users [platform-url|alias]
|
|
30
|
+
kweaver auth switch [platform-url|alias] --user <userId|username>
|
|
31
|
+
kweaver auth logout [platform-url|alias] [--user <userId>]
|
|
32
|
+
kweaver auth delete <platform-url|alias> [--user <userId>]
|
|
29
33
|
kweaver token
|
|
30
34
|
|
|
31
35
|
kweaver call <url> [-X METHOD] [-H "Name: value"] [-d BODY] [--data-raw BODY]
|
|
@@ -54,7 +58,7 @@ Usage:
|
|
|
54
58
|
kweaver dataview list [--datasource-id id] [--type atomic|custom] [--limit n] [-bd value] [--pretty]
|
|
55
59
|
kweaver dataview find --name <name> [--exact] [--datasource-id id] [--wait] [--timeout ms] [-bd value] [--pretty]
|
|
56
60
|
kweaver dataview get <id> [-bd value] [--pretty]
|
|
57
|
-
kweaver dataview query <id> [--sql sql] [--limit n] [--offset n] [--need-total] [-bd value] [--pretty]
|
|
61
|
+
kweaver dataview query <id> [--sql sql] [--limit n] [--offset n] [--need-total] [--raw-sql] [-bd value] [--pretty]
|
|
58
62
|
kweaver dataview delete <id> [-y] [-bd value]
|
|
59
63
|
|
|
60
64
|
kweaver bkn list [options]
|
|
@@ -81,6 +85,12 @@ Usage:
|
|
|
81
85
|
kweaver config list-bd
|
|
82
86
|
kweaver config show
|
|
83
87
|
|
|
88
|
+
kweaver skill list|get|register|status|delete [options]
|
|
89
|
+
kweaver skill market [options]
|
|
90
|
+
kweaver skill content <skill-id> [--raw] [--output file]
|
|
91
|
+
kweaver skill read-file <skill-id> <rel-path> [--raw] [--output file]
|
|
92
|
+
kweaver skill download|install <skill-id> [path] [options]
|
|
93
|
+
|
|
84
94
|
kweaver vega health|stats|inspect
|
|
85
95
|
kweaver vega catalog list|get|health|test-connection|discover|resources [options]
|
|
86
96
|
kweaver vega resource list|get|query [options]
|
|
@@ -95,6 +105,9 @@ Usage:
|
|
|
95
105
|
kweaver context-loader query-object-instance|query-instance-subgraph|get-logic-properties|get-action-info ...
|
|
96
106
|
(alias: kweaver context ...)
|
|
97
107
|
|
|
108
|
+
Global options:
|
|
109
|
+
--user <id|name> Use a specific user's credentials for this command (env: KWEAVER_USER)
|
|
110
|
+
|
|
98
111
|
Commands:
|
|
99
112
|
auth Login, list, inspect, and switch saved platform auth profiles
|
|
100
113
|
token Print the current access token, refreshing it first if needed
|
|
@@ -105,13 +118,21 @@ Commands:
|
|
|
105
118
|
bkn Knowledge network (CRUD, build, validate, export, stats, push/pull,
|
|
106
119
|
object-type, relation-type, subgraph, action-type, action-execution, action-log)
|
|
107
120
|
config Per-platform configuration (business domain)
|
|
121
|
+
skill Skill registry and market (register, search, progressive read, download/install)
|
|
108
122
|
vega Vega observability (catalog, resource, connector-type, health/stats/inspect)
|
|
109
123
|
context-loader Context-loader MCP (config, tools, resources, prompts, kn-search, query-*, etc.)
|
|
110
124
|
help Show this message`);
|
|
111
125
|
}
|
|
112
126
|
export async function run(argv) {
|
|
113
127
|
applyTlsEnvFromSavedTokens();
|
|
114
|
-
|
|
128
|
+
// Global --user flag: override active user for this invocation
|
|
129
|
+
const userIdx = argv.indexOf("--user");
|
|
130
|
+
let filteredArgv = argv;
|
|
131
|
+
if (userIdx !== -1 && userIdx + 1 < argv.length) {
|
|
132
|
+
process.env.KWEAVER_USER = argv[userIdx + 1];
|
|
133
|
+
filteredArgv = [...argv.slice(0, userIdx), ...argv.slice(userIdx + 2)];
|
|
134
|
+
}
|
|
135
|
+
const [command, ...rest] = filteredArgv;
|
|
115
136
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
116
137
|
const { createRequire } = await import("node:module");
|
|
117
138
|
const require = createRequire(import.meta.url);
|
|
@@ -150,6 +171,9 @@ export async function run(argv) {
|
|
|
150
171
|
if (command === "config") {
|
|
151
172
|
return runConfigCommand(rest);
|
|
152
173
|
}
|
|
174
|
+
if (command === "skill") {
|
|
175
|
+
return runSkillCommand(rest);
|
|
176
|
+
}
|
|
153
177
|
if (command === "context-loader" || command === "context") {
|
|
154
178
|
return runContextLoaderCommand(rest);
|
|
155
179
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { DataSourcesResource } from "./resources/datasources.js";
|
|
|
6
6
|
import { DataViewsResource } from "./resources/dataviews.js";
|
|
7
7
|
import { KnowledgeNetworksResource } from "./resources/knowledge-networks.js";
|
|
8
8
|
import { BknResource } from "./resources/bkn.js";
|
|
9
|
+
import { SkillsResource } from "./resources/skills.js";
|
|
9
10
|
import { VegaResource } from "./resources/vega.js";
|
|
10
11
|
/**
|
|
11
12
|
* Shared credentials passed to every resource method.
|
|
@@ -90,6 +91,8 @@ export declare class KWeaverClient implements ClientContext {
|
|
|
90
91
|
readonly dataviews: DataViewsResource;
|
|
91
92
|
/** Vega observability platform (catalogs, resources, connector types). */
|
|
92
93
|
readonly vega: VegaResource;
|
|
94
|
+
/** ADP/KWeaver skill registry, market, progressive read, and install helpers. */
|
|
95
|
+
readonly skills: SkillsResource;
|
|
93
96
|
constructor(opts?: KWeaverClientOptions);
|
|
94
97
|
/**
|
|
95
98
|
* Async factory that auto-refreshes expired or revoked tokens.
|
package/dist/client.js
CHANGED
|
@@ -9,6 +9,7 @@ import { DataSourcesResource } from "./resources/datasources.js";
|
|
|
9
9
|
import { DataViewsResource } from "./resources/dataviews.js";
|
|
10
10
|
import { KnowledgeNetworksResource } from "./resources/knowledge-networks.js";
|
|
11
11
|
import { BknResource } from "./resources/bkn.js";
|
|
12
|
+
import { SkillsResource } from "./resources/skills.js";
|
|
12
13
|
import { VegaResource } from "./resources/vega.js";
|
|
13
14
|
// ── KWeaverClient ─────────────────────────────────────────────────────────────
|
|
14
15
|
/**
|
|
@@ -59,6 +60,8 @@ export class KWeaverClient {
|
|
|
59
60
|
dataviews;
|
|
60
61
|
/** Vega observability platform (catalogs, resources, connector types). */
|
|
61
62
|
vega;
|
|
63
|
+
/** ADP/KWeaver skill registry, market, progressive read, and install helpers. */
|
|
64
|
+
skills;
|
|
62
65
|
constructor(opts = {}) {
|
|
63
66
|
const envDomain = process.env.KWEAVER_BUSINESS_DOMAIN;
|
|
64
67
|
let baseUrl;
|
|
@@ -114,6 +117,7 @@ export class KWeaverClient {
|
|
|
114
117
|
this.datasources = new DataSourcesResource(this);
|
|
115
118
|
this.dataviews = new DataViewsResource(this);
|
|
116
119
|
this.vega = new VegaResource(this);
|
|
120
|
+
this.skills = new SkillsResource(this);
|
|
117
121
|
}
|
|
118
122
|
/**
|
|
119
123
|
* Async factory that auto-refreshes expired or revoked tokens.
|