@kweaver-ai/kweaver-sdk 0.6.6 → 0.6.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/README.md +7 -2
- package/README.zh.md +6 -2
- package/dist/api/toolboxes.js +4 -4
- package/dist/auth/eacp-modify-password.d.ts +25 -0
- package/dist/auth/eacp-modify-password.js +84 -0
- package/dist/auth/oauth.d.ts +71 -3
- package/dist/auth/oauth.js +140 -18
- package/dist/cli.js +2 -1
- package/dist/commands/agent-members.d.ts +68 -0
- package/dist/commands/agent-members.js +383 -0
- package/dist/commands/agent.js +4 -0
- package/dist/commands/auth.js +298 -47
- package/dist/commands/config.js +80 -28
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,9 @@ export KWEAVER_BASE_URL=https://your-kweaver-instance.com
|
|
|
31
31
|
export KWEAVER_TOKEN=your-token
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
With both set, API commands use that token even if you never ran `auth login`. You can also run **`kweaver auth status`**, **`kweaver auth whoami`** (supports `--json`), and **`kweaver config show`** when there is **no** current platform in `~/.kweaver
|
|
34
|
+
With both set, API commands use that token even if you never ran `auth login`. You can also run **`kweaver auth status`**, **`kweaver auth whoami`** (supports `--json`), and **`kweaver config show`** when there is **no** current platform in `~/.kweaver/`. In env-token mode, `whoami` resolves the bound identity from EACP `/api/eacp/v1/user/get` and prints `Type` (user/app), `User ID`, `Account` and `Name`; this works for both opaque and JWT tokens. If EACP is unreachable, the CLI falls back to local JWT decode and prints a short hint when the token is opaque.
|
|
35
|
+
|
|
36
|
+
`kweaver config list-bd` lists business domains for the current user. App (service) tokens are not bound to an end-user — when the backend rejects the call with `401 invalid user_id`, the CLI re-checks the token type via EACP and, if confirmed `type:"app"`, replaces the cryptic backend body with `This command does not support app accounts.`. Use a user token (interactive `auth login`) for user-bound endpoints.
|
|
35
37
|
|
|
36
38
|
### Business domain (platform)
|
|
37
39
|
|
|
@@ -153,8 +155,11 @@ const skillMd = await client.skills.fetchContent("skill-id");
|
|
|
153
155
|
## CLI Reference
|
|
154
156
|
|
|
155
157
|
```
|
|
156
|
-
kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--insecure|-k]
|
|
158
|
+
kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--new-password <pwd>] [--http-signin] [--insecure|-k]
|
|
157
159
|
# -u/-p (with or without --http-signin): HTTP POST /oauth2/signin (yields refresh_token). Missing -u/-p are prompted from stdin (password hidden when TTY).
|
|
160
|
+
# If the server returns error 401001017 (initial password), TTY users get a prompt to set a new password; non-interactive scripts must pass --new-password <pwd>.
|
|
161
|
+
kweaver auth change-password [<url>] [-u <account>] [-o <old>] [-n <new>] [--insecure|-k]
|
|
162
|
+
# EACP POST /api/eacp/v1/auth1/modifypassword — no OAuth token required. Omit -o/-n on a TTY to be prompted.
|
|
158
163
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (headless login)
|
|
159
164
|
kweaver auth export [url|alias] [--json] (export command to run on a headless host)
|
|
160
165
|
kweaver auth status / whoami [url|alias] [--json] # whoami: --json; with KWEAVER_BASE_URL+KWEAVER_TOKEN when no ~/.kweaver/ platform
|
package/README.zh.md
CHANGED
|
@@ -31,7 +31,9 @@ export KWEAVER_BASE_URL=https://your-kweaver-instance.com
|
|
|
31
31
|
export KWEAVER_TOKEN=your-token
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
两者同时设置时,即使未执行 `auth login`,业务命令也会使用该 token。若 **`~/.kweaver/` 无当前平台**,仍可使用 **`kweaver auth status`**、**`kweaver auth whoami`**(支持 `--json`)、**`kweaver config show
|
|
34
|
+
两者同时设置时,即使未执行 `auth login`,业务命令也会使用该 token。若 **`~/.kweaver/` 无当前平台**,仍可使用 **`kweaver auth status`**、**`kweaver auth whoami`**(支持 `--json`)、**`kweaver config show`**。环境变量模式下,`whoami` 会通过 EACP `/api/eacp/v1/user/get` 在线获取身份并展示 `Type`(user/app)、`User ID`、`Account`、`Name`,对 opaque 与 JWT token 都生效;若 EACP 不可达,则回退到本地 JWT 解码,opaque token 会给出简短提示。
|
|
35
|
+
|
|
36
|
+
`kweaver config list-bd` 用于列出当前用户可访问的业务域。**应用(service)token 没有绑定终端用户**——当后端返回 `401 invalid user_id` 时,CLI 会再向 EACP 复核 token 类型,确认为 `type:"app"` 后将晦涩的后端原文替换为 `This command does not support app accounts.`。需要这个能力时请改用交互式 `auth login` 获得的用户 token。
|
|
35
37
|
|
|
36
38
|
### 业务域(平台配置)
|
|
37
39
|
|
|
@@ -146,8 +148,10 @@ const skillMd = await client.skills.fetchContent("skill-id");
|
|
|
146
148
|
## 命令速查
|
|
147
149
|
|
|
148
150
|
```
|
|
149
|
-
kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--insecure|-k]
|
|
151
|
+
kweaver auth login <url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--new-password <pwd>] [--http-signin] [--insecure|-k]
|
|
150
152
|
# -u/-p(无论是否带 --http-signin):HTTP POST /oauth2/signin(可拿 refresh_token);缺失的用户名/密码会从 stdin 提示输入(TTY 下密码隐藏)
|
|
153
|
+
# 若服务端返回 401001017(初始密码),交互终端会引导修改;非交互请使用 --new-password <pwd>。
|
|
154
|
+
kweaver auth change-password [<url>] [-u <account>] [-o <old>] [-n <new>] [--insecure|-k]
|
|
151
155
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (无浏览器登录)
|
|
152
156
|
kweaver auth export [url|alias] [--json] (导出在无浏览器机器上运行的命令)
|
|
153
157
|
kweaver auth status / whoami [url|alias] [--json] # whoami 支持 --json;无 ~/.kweaver/ 当前平台时可配 KWEAVER_BASE_URL+KWEAVER_TOKEN
|
package/dist/api/toolboxes.js
CHANGED
|
@@ -12,8 +12,8 @@ import { buildHeaders } from "./headers.js";
|
|
|
12
12
|
// POST /tool-box/{id}/tools/status enable/disable (batch)
|
|
13
13
|
//
|
|
14
14
|
// Verified during Task 8 e2e against the live backend (2026-04-18):
|
|
15
|
-
// GET /tool-box?keyword=&limit=&offset= list toolboxes
|
|
16
|
-
// GET /tool-box/{id}/
|
|
15
|
+
// GET /tool-box/list?keyword=&limit=&offset= list toolboxes
|
|
16
|
+
// GET /tool-box/{id}/tools/list list tools
|
|
17
17
|
const PATH = "/api/agent-operator-integration/v1/tool-box";
|
|
18
18
|
function url(base, suffix = "") {
|
|
19
19
|
return `${base.replace(/\/+$/, "")}${PATH}${suffix}`;
|
|
@@ -74,7 +74,7 @@ export async function listToolboxes(opts) {
|
|
|
74
74
|
qp.set("limit", String(opts.limit));
|
|
75
75
|
if (opts.offset !== undefined)
|
|
76
76
|
qp.set("offset", String(opts.offset));
|
|
77
|
-
const suffix = qp.toString() ? `?${qp}` : ""
|
|
77
|
+
const suffix = `/list${qp.toString() ? `?${qp}` : ""}`;
|
|
78
78
|
const { body } = await fetchTextOrThrow(url(opts.baseUrl, suffix), {
|
|
79
79
|
method: "GET",
|
|
80
80
|
headers: buildHeaders(opts.accessToken, opts.businessDomain ?? "bd_public"),
|
|
@@ -82,7 +82,7 @@ export async function listToolboxes(opts) {
|
|
|
82
82
|
return body;
|
|
83
83
|
}
|
|
84
84
|
export async function listTools(opts) {
|
|
85
|
-
const { body } = await fetchTextOrThrow(url(opts.baseUrl, `/${encodeURIComponent(opts.boxId)}/
|
|
85
|
+
const { body } = await fetchTextOrThrow(url(opts.baseUrl, `/${encodeURIComponent(opts.boxId)}/tools/list`), {
|
|
86
86
|
method: "GET",
|
|
87
87
|
headers: buildHeaders(opts.accessToken, opts.businessDomain ?? "bd_public"),
|
|
88
88
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Encrypt a password with EACP modifypassword's RSA public key, base64-encoded. */
|
|
2
|
+
export declare function encryptModifyPwd(plain: string, publicKeyPem?: string): string;
|
|
3
|
+
/** @internal For unit tests: decrypt ciphertext produced by encryptModifyPwd with the embedded key. */
|
|
4
|
+
export declare function decryptModifyPwdForTest(cipherB64: string): string;
|
|
5
|
+
export interface EacpModifyPasswordOptions {
|
|
6
|
+
account: string;
|
|
7
|
+
oldPassword: string;
|
|
8
|
+
newPassword: string;
|
|
9
|
+
/** Override the embedded RSA public key (PEM). */
|
|
10
|
+
publicKeyPem?: string;
|
|
11
|
+
tlsInsecure?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface EacpModifyPasswordResult {
|
|
14
|
+
status: number;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
body: string;
|
|
17
|
+
json?: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Call EACP `POST /api/eacp/v1/auth1/modifypassword` to change a user's password
|
|
21
|
+
* when the old password is known (`isforgetpwd: false`).
|
|
22
|
+
*
|
|
23
|
+
* No bearer token / cookie is required — the endpoint authenticates by old password.
|
|
24
|
+
*/
|
|
25
|
+
export declare function eacpModifyPassword(baseUrl: string, options: EacpModifyPasswordOptions): Promise<EacpModifyPasswordResult>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { constants as cryptoConstants, createPrivateKey, createPublicKey, privateDecrypt, publicEncrypt, } from "node:crypto";
|
|
2
|
+
import { normalizeBaseUrl, runWithTlsInsecure } from "./oauth.js";
|
|
3
|
+
/**
|
|
4
|
+
* 1024-bit RSA private key embedded in ShareServer
|
|
5
|
+
* (`isf/ShareServer/src/eachttpserver/ncEACHttpServerUtil.cpp`, function
|
|
6
|
+
* `ncEACHttpServerUtil::RSADecrypt`). It is the keypair used by the EACP
|
|
7
|
+
* `auth1/modifypassword` endpoint to decrypt `oldpwd` / `newpwd`.
|
|
8
|
+
*
|
|
9
|
+
* Note: this key is intentionally hard-coded in the C++ binary and shipped to
|
|
10
|
+
* every customer; it is not a secret. We embed it here so the CLI can perform
|
|
11
|
+
* the matching `RSA_PKCS1` encryption without contacting the server.
|
|
12
|
+
*/
|
|
13
|
+
const EACP_MODIFYPWD_PRIVATE_KEY_PEM = `-----BEGIN RSA PRIVATE KEY-----
|
|
14
|
+
MIICXgIBAAKBgQDB2fhLla9rMx+6LWTXajnK11Kdp520s1Q+TfPfIXI/7G9+L2YC
|
|
15
|
+
4RA3M5rgRi32s5+UFQ/CVqUFqMqVuzaZ4lw/uEdk1qHcP0g6LB3E9wkl2FclFR0M
|
|
16
|
+
+/HrWmxPoON+0y/tFQxxfNgsUodFzbdh0XY1rIVUIbPLvufUBbLKXHDPpwIDAQAB
|
|
17
|
+
AoGBALCM/H6ajXFs1nCR903aCVicUzoS9qckzI0SIhIOPCfMBp8+PAJTSJl9/ohU
|
|
18
|
+
YnhVj/kmVXwBvboxyJAmOcxdRPWL7iTk5nA1oiVXMer3Wby+tRg/ls91xQbJLVv3
|
|
19
|
+
oGSt7q0CXxJpRH2oYkVVlMMlZUwKz3ovHiLKAnhw+jEsdL2BAkEA9hA97yyeA2eq
|
|
20
|
+
f9dMu/ici99R3WJRRtk4NEI4WShtWPyziDg48d3SOzYmhEJjPuOo3g1ze01os70P
|
|
21
|
+
ApE7d0qcyQJBAMmt+FR8h5MwxPQPAzjh/fTuTttvUfBeMiUDrIycK1I/L96lH+fU
|
|
22
|
+
i4Nu+7TPOzExnPeGO5UJbZxrpIEUB7Zs8O8CQQCLzTCTGiNwxc5eMgH77kVrRudp
|
|
23
|
+
Q7nv6ex/7Hu9VDXEUFbkdyULbj9KuvppPJrMmWZROw04qgNp02mayM8jeLXZAkEA
|
|
24
|
+
o+PM/pMn9TPXiWE9xBbaMhUKXgXLd2KEq1GeAbHS/oY8l1hmYhV1vjwNLbSNrH9d
|
|
25
|
+
yEP73TQJL+jFiONHFTbYXwJAU03Xgum5mLIkX/02LpOrz2QCdfX1IMJk2iKi9osV
|
|
26
|
+
KqfbvHsF0+GvFGg18/FXStG9Kr4TjqLsygQJT76/MnMluw==
|
|
27
|
+
-----END RSA PRIVATE KEY-----`;
|
|
28
|
+
let cachedPubKey;
|
|
29
|
+
function getModifyPwdPublicKey() {
|
|
30
|
+
if (!cachedPubKey) {
|
|
31
|
+
cachedPubKey = createPublicKey(createPrivateKey(EACP_MODIFYPWD_PRIVATE_KEY_PEM));
|
|
32
|
+
}
|
|
33
|
+
return cachedPubKey;
|
|
34
|
+
}
|
|
35
|
+
/** Encrypt a password with EACP modifypassword's RSA public key, base64-encoded. */
|
|
36
|
+
export function encryptModifyPwd(plain, publicKeyPem) {
|
|
37
|
+
const key = publicKeyPem ? createPublicKey(publicKeyPem) : getModifyPwdPublicKey();
|
|
38
|
+
const buf = publicEncrypt({ key, padding: cryptoConstants.RSA_PKCS1_PADDING }, Buffer.from(plain, "utf8"));
|
|
39
|
+
return buf.toString("base64");
|
|
40
|
+
}
|
|
41
|
+
/** @internal For unit tests: decrypt ciphertext produced by encryptModifyPwd with the embedded key. */
|
|
42
|
+
export function decryptModifyPwdForTest(cipherB64) {
|
|
43
|
+
const key = createPrivateKey(EACP_MODIFYPWD_PRIVATE_KEY_PEM);
|
|
44
|
+
const buf = privateDecrypt({ key, padding: cryptoConstants.RSA_PKCS1_PADDING }, Buffer.from(cipherB64, "base64"));
|
|
45
|
+
return buf.toString("utf8");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Call EACP `POST /api/eacp/v1/auth1/modifypassword` to change a user's password
|
|
49
|
+
* when the old password is known (`isforgetpwd: false`).
|
|
50
|
+
*
|
|
51
|
+
* No bearer token / cookie is required — the endpoint authenticates by old password.
|
|
52
|
+
*/
|
|
53
|
+
export async function eacpModifyPassword(baseUrl, options) {
|
|
54
|
+
return runWithTlsInsecure(options.tlsInsecure, async () => {
|
|
55
|
+
const body = {
|
|
56
|
+
account: options.account,
|
|
57
|
+
oldpwd: encryptModifyPwd(options.oldPassword, options.publicKeyPem),
|
|
58
|
+
newpwd: encryptModifyPwd(options.newPassword, options.publicKeyPem),
|
|
59
|
+
vcodeinfo: {
|
|
60
|
+
uuid: "",
|
|
61
|
+
vcode: "",
|
|
62
|
+
},
|
|
63
|
+
isforgetpwd: false,
|
|
64
|
+
};
|
|
65
|
+
const url = `${normalizeBaseUrl(baseUrl)}/api/eacp/v1/auth1/modifypassword`;
|
|
66
|
+
const resp = await fetch(url, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
Accept: "application/json, text/plain, */*",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
});
|
|
74
|
+
const text = await resp.text();
|
|
75
|
+
let json;
|
|
76
|
+
try {
|
|
77
|
+
json = text ? JSON.parse(text) : undefined;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* not JSON */
|
|
81
|
+
}
|
|
82
|
+
return { status: resp.status, ok: resp.ok, body: text, json };
|
|
83
|
+
});
|
|
84
|
+
}
|
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { type TokenConfig } from "../config/store.js";
|
|
2
|
+
/** Thrown when `POST /oauth2/signin` returns HTTP 401 with EACP code `401001017` (initial password must be changed). */
|
|
3
|
+
export declare class InitialPasswordChangeRequiredError extends Error {
|
|
4
|
+
readonly code = 401001017;
|
|
5
|
+
readonly account: string;
|
|
6
|
+
readonly baseUrl: string;
|
|
7
|
+
readonly httpStatus = 401;
|
|
8
|
+
readonly serverMessage: string;
|
|
9
|
+
constructor(opts: {
|
|
10
|
+
account: string;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
serverMessage: string;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
2
15
|
/**
|
|
3
16
|
* Studioweb hardcoded LOGIN public key (PEM) — the single key used for HTTP `/oauth2/signin`.
|
|
4
17
|
* Source: kweaver-ai/kweaver `deploy/auto_cofig/auto_config.sh` `LOGIN_PUBLIC_KEY`.
|
|
@@ -14,18 +27,73 @@ export declare const DEFAULT_SIGNIN_RSA_MODULUS_HEX = "C1D9F84B95AF6B331FBA2D64D
|
|
|
14
27
|
* Build an SPKI PEM from an RSA modulus (hex) and public exponent (default 65537 / 0x10001).
|
|
15
28
|
*/
|
|
16
29
|
export declare function rsaModulusHexToSpkiPem(modulusHex: string, exponent?: number): string;
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
30
|
+
/** Identity resolved from EACP `/api/eacp/v1/user/get` for the bound access token. */
|
|
31
|
+
export interface EacpUserInfo {
|
|
32
|
+
/** "user" — interactive end-user; "app" — service / application identity. */
|
|
33
|
+
type: "user" | "app";
|
|
34
|
+
/** EACP id (`userid` for users, `id` for apps). */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Login name (e.g. "alice@example.com"). User tokens only. */
|
|
37
|
+
account?: string;
|
|
38
|
+
/** Display name — `visionName` for users, app name for apps. */
|
|
39
|
+
name?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Probe EACP for the bound identity. Returns `null` on any failure (network,
|
|
43
|
+
* non-2xx, malformed JSON, unknown `type`). Callers must treat absence as a
|
|
44
|
+
* soft signal — this never throws and never blocks the user's actual command.
|
|
45
|
+
*
|
|
46
|
+
* The shape differs per token type, mirroring the EACP backend:
|
|
47
|
+
* - `type: "user"` → `{ userid, account, name, ... }`
|
|
48
|
+
* - `type: "app"` → `{ id, name }`
|
|
49
|
+
*/
|
|
50
|
+
export declare function fetchEacpUserInfo(baseUrl: string, accessToken: string, tlsInsecure?: boolean): Promise<EacpUserInfo | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Quote a value for safe copy-paste into a shell command.
|
|
53
|
+
*
|
|
54
|
+
* Strategy:
|
|
55
|
+
* - If the value only contains "shell-safe" characters, return it bare. This
|
|
56
|
+
* keeps the printed command portable across shells (issue #74: POSIX single
|
|
57
|
+
* quotes are literal in cmd.exe, so any quoting locks the line to one OS).
|
|
58
|
+
* - Otherwise the value contains characters the shell would interpret
|
|
59
|
+
* (space, `&`, `|`, `$`, `*`, ...), so we must quote per host shell:
|
|
60
|
+
* - win32 (cmd.exe / PowerShell): wrap in `"..."`; embedded `"` -> `""`
|
|
61
|
+
* - POSIX (bash/zsh/sh): wrap in `'...'`; embedded `'` -> `'\''`
|
|
62
|
+
*
|
|
63
|
+
* `platform` defaults to `process.platform`; passable for tests and for
|
|
64
|
+
* generating commands targeted at a specific shell.
|
|
65
|
+
*/
|
|
66
|
+
export declare function shellQuoteForShell(value: string, platform?: NodeJS.Platform): string;
|
|
19
67
|
/**
|
|
20
68
|
* Build a one-line `kweaver auth login ...` command for headless / other machines.
|
|
21
69
|
* Omits `--client-secret` when empty (PKCE-only client); headless refresh may still require a confidential client.
|
|
70
|
+
*
|
|
71
|
+
* `platform` defaults to `process.platform`; pass explicitly in tests or when
|
|
72
|
+
* generating a command meant for a different OS.
|
|
22
73
|
*/
|
|
23
|
-
export declare function buildCopyCommand(baseUrl: string, clientId: string, clientSecret: string, refreshToken: string | undefined, tlsInsecure?: boolean): string;
|
|
74
|
+
export declare function buildCopyCommand(baseUrl: string, clientId: string, clientSecret: string, refreshToken: string | undefined, tlsInsecure?: boolean, platform?: NodeJS.Platform): string;
|
|
24
75
|
/**
|
|
25
76
|
* HTML shown after successful OAuth callback with a copyable headless login command.
|
|
26
77
|
*/
|
|
27
78
|
export declare function buildCallbackHtml(copyCommand: string): string;
|
|
28
79
|
export declare function normalizeBaseUrl(value: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the platform URL the CLI should act on, with explicit precedence:
|
|
82
|
+
*
|
|
83
|
+
* 1. **positional** — caller passed a URL/alias (always wins)
|
|
84
|
+
* 2. **env** — `KWEAVER_BASE_URL` is set (explicit user intent;
|
|
85
|
+
* wins over a stale `~/.kweaver/` saved session)
|
|
86
|
+
* 3. **saved** — `getCurrentPlatform()` from local config
|
|
87
|
+
*
|
|
88
|
+
* `source` lets callers print provenance without re-deriving it. This mirrors
|
|
89
|
+
* `ensureValidToken()` (which is already env-first), so introspection
|
|
90
|
+
* commands (`auth status`, `auth whoami`, `config show|list-bd|set-bd`) and
|
|
91
|
+
* data-path commands agree on which platform "current" means.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveActivePlatform(positional?: string | null): {
|
|
94
|
+
url: string;
|
|
95
|
+
source: "positional" | "env" | "saved";
|
|
96
|
+
} | null;
|
|
29
97
|
/**
|
|
30
98
|
* Temporarily disable TLS certificate verification for Node `fetch` (sets
|
|
31
99
|
* NODE_TLS_REJECT_UNAUTHORIZED). Used for `--insecure` login and token refresh.
|
package/dist/auth/oauth.js
CHANGED
|
@@ -3,6 +3,21 @@ import { createPublicKey } from "node:crypto";
|
|
|
3
3
|
import { isNoAuth } from "../config/no-auth.js";
|
|
4
4
|
import { deleteClientConfig, getCurrentPlatform, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveUserId, saveClientConfig, saveNoAuthPlatform, saveTokenConfig, setCurrentPlatform, } from "../config/store.js";
|
|
5
5
|
import { HttpError, NetworkRequestError, fetchWithRetry } from "../utils/http.js";
|
|
6
|
+
/** Thrown when `POST /oauth2/signin` returns HTTP 401 with EACP code `401001017` (initial password must be changed). */
|
|
7
|
+
export class InitialPasswordChangeRequiredError extends Error {
|
|
8
|
+
code = 401001017;
|
|
9
|
+
account;
|
|
10
|
+
baseUrl;
|
|
11
|
+
httpStatus = 401;
|
|
12
|
+
serverMessage;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
super(opts.serverMessage);
|
|
15
|
+
this.name = "InitialPasswordChangeRequiredError";
|
|
16
|
+
this.account = opts.account;
|
|
17
|
+
this.baseUrl = opts.baseUrl;
|
|
18
|
+
this.serverMessage = opts.serverMessage;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
const TOKEN_TTL_SECONDS = 3600;
|
|
7
22
|
/** Seconds before access token expiry to trigger refresh (matches Python ConfigAuth). */
|
|
8
23
|
const REFRESH_THRESHOLD_SEC = 60;
|
|
@@ -231,42 +246,94 @@ function extractRsaPublicKeyMaterialFromPageProps(pageProps) {
|
|
|
231
246
|
}
|
|
232
247
|
return deepFindSigninRsaMaterial(pageProps, 5, new Set());
|
|
233
248
|
}
|
|
234
|
-
/**
|
|
235
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Probe EACP for the bound identity. Returns `null` on any failure (network,
|
|
251
|
+
* non-2xx, malformed JSON, unknown `type`). Callers must treat absence as a
|
|
252
|
+
* soft signal — this never throws and never blocks the user's actual command.
|
|
253
|
+
*
|
|
254
|
+
* The shape differs per token type, mirroring the EACP backend:
|
|
255
|
+
* - `type: "user"` → `{ userid, account, name, ... }`
|
|
256
|
+
* - `type: "app"` → `{ id, name }`
|
|
257
|
+
*/
|
|
258
|
+
export async function fetchEacpUserInfo(baseUrl, accessToken, tlsInsecure) {
|
|
236
259
|
try {
|
|
237
260
|
const res = await runWithTlsInsecure(tlsInsecure, () => fetch(`${baseUrl}/api/eacp/v1/user/get`, {
|
|
238
261
|
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
|
|
239
262
|
}));
|
|
240
263
|
if (!res.ok)
|
|
241
264
|
return null;
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
const raw = (await res.json());
|
|
266
|
+
const type = raw.type;
|
|
267
|
+
if (type !== "user" && type !== "app")
|
|
268
|
+
return null;
|
|
269
|
+
const idCandidate = type === "user" ? raw.userid : raw.id;
|
|
270
|
+
const id = typeof idCandidate === "string" ? idCandidate : "";
|
|
271
|
+
if (!id)
|
|
272
|
+
return null;
|
|
273
|
+
return {
|
|
274
|
+
type,
|
|
275
|
+
id,
|
|
276
|
+
account: typeof raw.account === "string" ? raw.account : undefined,
|
|
277
|
+
name: typeof raw.name === "string" ? raw.name : undefined,
|
|
278
|
+
};
|
|
249
279
|
}
|
|
250
280
|
catch {
|
|
251
|
-
|
|
281
|
+
return null;
|
|
252
282
|
}
|
|
253
|
-
return null;
|
|
254
283
|
}
|
|
255
|
-
/**
|
|
256
|
-
|
|
284
|
+
/** Best-effort fetch of display name via EACP userinfo (ShareServer). */
|
|
285
|
+
async function fetchDisplayName(baseUrl, accessToken, tlsInsecure) {
|
|
286
|
+
const info = await fetchEacpUserInfo(baseUrl, accessToken, tlsInsecure);
|
|
287
|
+
return info?.account ?? info?.name ?? null;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Characters that bash/zsh/sh AND cmd.exe/PowerShell all leave untouched when
|
|
291
|
+
* a value is written without surrounding quotes. Real-world OAuth values
|
|
292
|
+
* (URLs, UUID-like client IDs, base64url refresh tokens) live in this set, so
|
|
293
|
+
* we can emit them bare and the resulting command is portable across macOS,
|
|
294
|
+
* Linux, Windows cmd, and PowerShell — including the copy-mac-paste-to-windows
|
|
295
|
+
* (and the reverse) workflow.
|
|
296
|
+
*/
|
|
297
|
+
const SHELL_SAFE_VALUE = /^[A-Za-z0-9._:/+=@-]+$/;
|
|
298
|
+
/**
|
|
299
|
+
* Quote a value for safe copy-paste into a shell command.
|
|
300
|
+
*
|
|
301
|
+
* Strategy:
|
|
302
|
+
* - If the value only contains "shell-safe" characters, return it bare. This
|
|
303
|
+
* keeps the printed command portable across shells (issue #74: POSIX single
|
|
304
|
+
* quotes are literal in cmd.exe, so any quoting locks the line to one OS).
|
|
305
|
+
* - Otherwise the value contains characters the shell would interpret
|
|
306
|
+
* (space, `&`, `|`, `$`, `*`, ...), so we must quote per host shell:
|
|
307
|
+
* - win32 (cmd.exe / PowerShell): wrap in `"..."`; embedded `"` -> `""`
|
|
308
|
+
* - POSIX (bash/zsh/sh): wrap in `'...'`; embedded `'` -> `'\''`
|
|
309
|
+
*
|
|
310
|
+
* `platform` defaults to `process.platform`; passable for tests and for
|
|
311
|
+
* generating commands targeted at a specific shell.
|
|
312
|
+
*/
|
|
313
|
+
export function shellQuoteForShell(value, platform = process.platform) {
|
|
314
|
+
if (value !== "" && SHELL_SAFE_VALUE.test(value)) {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
if (platform === "win32") {
|
|
318
|
+
return `"${value.replace(/"/g, `""`)}"`;
|
|
319
|
+
}
|
|
257
320
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
258
321
|
}
|
|
259
322
|
/**
|
|
260
323
|
* Build a one-line `kweaver auth login ...` command for headless / other machines.
|
|
261
324
|
* Omits `--client-secret` when empty (PKCE-only client); headless refresh may still require a confidential client.
|
|
325
|
+
*
|
|
326
|
+
* `platform` defaults to `process.platform`; pass explicitly in tests or when
|
|
327
|
+
* generating a command meant for a different OS.
|
|
262
328
|
*/
|
|
263
|
-
export function buildCopyCommand(baseUrl, clientId, clientSecret, refreshToken, tlsInsecure) {
|
|
264
|
-
const
|
|
329
|
+
export function buildCopyCommand(baseUrl, clientId, clientSecret, refreshToken, tlsInsecure, platform = process.platform) {
|
|
330
|
+
const q = (v) => shellQuoteForShell(v, platform);
|
|
331
|
+
const parts = ["kweaver", "auth", "login", q(normalizeBaseUrl(baseUrl)), "--client-id", q(clientId)];
|
|
265
332
|
if (clientSecret) {
|
|
266
|
-
parts.push("--client-secret",
|
|
333
|
+
parts.push("--client-secret", q(clientSecret));
|
|
267
334
|
}
|
|
268
335
|
if (refreshToken) {
|
|
269
|
-
parts.push("--refresh-token",
|
|
336
|
+
parts.push("--refresh-token", q(refreshToken));
|
|
270
337
|
}
|
|
271
338
|
if (tlsInsecure) {
|
|
272
339
|
parts.push("--insecure");
|
|
@@ -336,6 +403,33 @@ function buildCallbackExchangeErrorHtml(message) {
|
|
|
336
403
|
export function normalizeBaseUrl(value) {
|
|
337
404
|
return value.replace(/\/+$/, "");
|
|
338
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolve the platform URL the CLI should act on, with explicit precedence:
|
|
408
|
+
*
|
|
409
|
+
* 1. **positional** — caller passed a URL/alias (always wins)
|
|
410
|
+
* 2. **env** — `KWEAVER_BASE_URL` is set (explicit user intent;
|
|
411
|
+
* wins over a stale `~/.kweaver/` saved session)
|
|
412
|
+
* 3. **saved** — `getCurrentPlatform()` from local config
|
|
413
|
+
*
|
|
414
|
+
* `source` lets callers print provenance without re-deriving it. This mirrors
|
|
415
|
+
* `ensureValidToken()` (which is already env-first), so introspection
|
|
416
|
+
* commands (`auth status`, `auth whoami`, `config show|list-bd|set-bd`) and
|
|
417
|
+
* data-path commands agree on which platform "current" means.
|
|
418
|
+
*/
|
|
419
|
+
export function resolveActivePlatform(positional) {
|
|
420
|
+
if (positional) {
|
|
421
|
+
const url = /^https?:\/\//.test(positional) ? normalizeBaseUrl(positional) : positional;
|
|
422
|
+
return { url, source: "positional" };
|
|
423
|
+
}
|
|
424
|
+
const envRaw = process.env.KWEAVER_BASE_URL?.trim();
|
|
425
|
+
if (envRaw) {
|
|
426
|
+
return { url: normalizeBaseUrl(envRaw), source: "env" };
|
|
427
|
+
}
|
|
428
|
+
const saved = getCurrentPlatform();
|
|
429
|
+
if (saved)
|
|
430
|
+
return { url: saved, source: "saved" };
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
339
433
|
/**
|
|
340
434
|
* Temporarily disable TLS certificate verification for Node `fetch` (sets
|
|
341
435
|
* NODE_TLS_REJECT_UNAUTHORIZED). Used for `--insecure` login and token refresh.
|
|
@@ -1268,6 +1362,26 @@ export async function oauth2PasswordSigninLogin(baseUrl, options) {
|
|
|
1268
1362
|
}
|
|
1269
1363
|
}
|
|
1270
1364
|
else {
|
|
1365
|
+
if (postResp.status === 401) {
|
|
1366
|
+
try {
|
|
1367
|
+
const j = JSON.parse(bodyText);
|
|
1368
|
+
const c = j.code;
|
|
1369
|
+
if (c === 401001017 || c === "401001017") {
|
|
1370
|
+
const msg = typeof j.message === "string" && j.message.trim() !== ""
|
|
1371
|
+
? j.message.trim()
|
|
1372
|
+
: "Initial password must be changed before login.";
|
|
1373
|
+
throw new InitialPasswordChangeRequiredError({
|
|
1374
|
+
account: options.username,
|
|
1375
|
+
baseUrl: base,
|
|
1376
|
+
serverMessage: msg,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
catch (e) {
|
|
1381
|
+
if (e instanceof InitialPasswordChangeRequiredError)
|
|
1382
|
+
throw e;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1271
1385
|
throw new HttpError(postResp.status, postResp.statusText, bodyText);
|
|
1272
1386
|
}
|
|
1273
1387
|
}
|
|
@@ -1610,12 +1724,20 @@ function isTlsVerificationDisabledForProcess() {
|
|
|
1610
1724
|
process.env.KWEAVER_TLS_INSECURE === "true");
|
|
1611
1725
|
}
|
|
1612
1726
|
export function formatHttpError(error) {
|
|
1727
|
+
if (error instanceof InitialPasswordChangeRequiredError) {
|
|
1728
|
+
return `${error.serverMessage} (code ${error.code})`;
|
|
1729
|
+
}
|
|
1613
1730
|
if (error instanceof HttpError) {
|
|
1614
1731
|
const oauthMessage = formatOAuthErrorBody(error.body);
|
|
1615
1732
|
if (oauthMessage) {
|
|
1616
1733
|
return `HTTP ${error.status} ${error.statusText}\n\n${oauthMessage}`;
|
|
1617
1734
|
}
|
|
1618
|
-
|
|
1735
|
+
const base = `${error.message}\n${error.body}`.trim();
|
|
1736
|
+
if ((error.status === 403 || error.status === 404) &&
|
|
1737
|
+
/BknBackend\.KnowledgeNetwork\.NotFound/.test(error.body)) {
|
|
1738
|
+
return `Hint: this is a "knowledge network not found" error (the kn-id does not exist), not a permission/auth issue. Verify the kn-id you passed.\n${base}`;
|
|
1739
|
+
}
|
|
1740
|
+
return base;
|
|
1619
1741
|
}
|
|
1620
1742
|
if (error instanceof NetworkRequestError) {
|
|
1621
1743
|
return [
|
package/dist/cli.js
CHANGED
|
@@ -23,9 +23,10 @@ Usage:
|
|
|
23
23
|
kweaver --version | -V
|
|
24
24
|
kweaver --help | -h
|
|
25
25
|
|
|
26
|
-
kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--http-signin] [--insecure|-k]
|
|
26
|
+
kweaver auth <platform-url> [--alias name] [--no-auth] [--no-browser] [-u user] [-p pass] [--new-password <pwd>] [--http-signin] [--insecure|-k]
|
|
27
27
|
kweaver auth login <platform-url> (alias for auth <url>)
|
|
28
28
|
kweaver auth login <url> --client-id ID --client-secret S --refresh-token T (run on host without browser)
|
|
29
|
+
kweaver auth change-password [<platform-url>] [-u <account>] [-o <old>] [-n <new>] [--insecure|-k]
|
|
29
30
|
kweaver auth whoami [platform-url|alias] [--json]
|
|
30
31
|
kweaver auth export [platform-url|alias] [--json]
|
|
31
32
|
kweaver auth status [platform-url|alias]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers and orchestrators for managing agent member associations
|
|
3
|
+
* (skills, tools, mcps) via get → mutate(config) → update.
|
|
4
|
+
*/
|
|
5
|
+
export interface MutationReport {
|
|
6
|
+
finalIds: string[];
|
|
7
|
+
added: string[];
|
|
8
|
+
alreadyAttached: string[];
|
|
9
|
+
removed: string[];
|
|
10
|
+
notAttached: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface MutateConfigMembersInput {
|
|
13
|
+
config: Record<string, unknown>;
|
|
14
|
+
path: string[];
|
|
15
|
+
idField: string;
|
|
16
|
+
addIds: string[];
|
|
17
|
+
removeIds: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface MutateConfigMembersResult {
|
|
20
|
+
newConfig: Record<string, unknown>;
|
|
21
|
+
report: MutationReport;
|
|
22
|
+
finalIds: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function mutateConfigMembers(input: MutateConfigMembersInput): MutateConfigMembersResult;
|
|
25
|
+
export interface MemberFetchResult {
|
|
26
|
+
exists: boolean;
|
|
27
|
+
published: boolean;
|
|
28
|
+
name?: string;
|
|
29
|
+
/** Optional free-form status label for `list` output; e.g. "published" | "draft" | "offline". */
|
|
30
|
+
status?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface MemberSpec {
|
|
33
|
+
/** Human-readable noun used in error/warning messages. */
|
|
34
|
+
memberKind: string;
|
|
35
|
+
/** Path inside the agent `config` object where the member array lives. */
|
|
36
|
+
configPath: string[];
|
|
37
|
+
/** Key inside each array element that identifies the member. */
|
|
38
|
+
idField: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AgentMembersDeps {
|
|
41
|
+
getAgent: (agentId: string) => Promise<string>;
|
|
42
|
+
updateAgent: (agentId: string, body: Record<string, unknown>) => Promise<string>;
|
|
43
|
+
fetchById: (id: string) => Promise<MemberFetchResult>;
|
|
44
|
+
}
|
|
45
|
+
export interface PatchAgentMembersInput {
|
|
46
|
+
agentId: string;
|
|
47
|
+
spec: MemberSpec;
|
|
48
|
+
addIds: string[];
|
|
49
|
+
removeIds: string[];
|
|
50
|
+
strict: boolean;
|
|
51
|
+
deps: AgentMembersDeps;
|
|
52
|
+
}
|
|
53
|
+
export interface PatchAgentMembersReport extends MutationReport {
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}
|
|
56
|
+
export declare function patchAgentMembers(input: PatchAgentMembersInput): Promise<PatchAgentMembersReport>;
|
|
57
|
+
export interface ListAgentMembersInput {
|
|
58
|
+
agentId: string;
|
|
59
|
+
spec: MemberSpec;
|
|
60
|
+
deps: Pick<AgentMembersDeps, "getAgent" | "fetchById">;
|
|
61
|
+
}
|
|
62
|
+
export interface ListedMember {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string | null;
|
|
65
|
+
status: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function listAgentMembers(input: ListAgentMembersInput): Promise<ListedMember[]>;
|
|
68
|
+
export declare function runAgentSkillCommand(args: string[]): Promise<number>;
|