@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 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/` the CLI decodes the token locally (JWT only). If the token is opaque, identity fields are omitted and a short hint is printed.
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`**:CLI 会在本地解 JWT 展示身份;若 token opaque,则省略身份字段并给出简短提示。
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
@@ -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}/tool list tools
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)}/tool`), {
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
+ }
@@ -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
- /** POSIX shell single-quote escaping for copy-paste commands. */
18
- export declare function shellQuoteForShell(value: string): string;
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.
@@ -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
- /** Best-effort fetch of display name via EACP userinfo (ShareServer). */
235
- async function fetchDisplayName(baseUrl, accessToken, tlsInsecure) {
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 info = (await res.json());
243
- if (typeof info.account === "string")
244
- return info.account;
245
- if (typeof info.name === "string")
246
- return info.name;
247
- if (typeof info.mail === "string")
248
- return info.mail;
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
- /* Non-critical — displayName will be absent. */
281
+ return null;
252
282
  }
253
- return null;
254
283
  }
255
- /** POSIX shell single-quote escaping for copy-paste commands. */
256
- export function shellQuoteForShell(value) {
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 parts = ["kweaver", "auth", "login", shellQuoteForShell(normalizeBaseUrl(baseUrl)), "--client-id", shellQuoteForShell(clientId)];
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", shellQuoteForShell(clientSecret));
333
+ parts.push("--client-secret", q(clientSecret));
267
334
  }
268
335
  if (refreshToken) {
269
- parts.push("--refresh-token", shellQuoteForShell(refreshToken));
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
- return `${error.message}\n${error.body}`.trim();
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>;