@luckypiggy/zhihu-ai-cli 0.1.0-test.1 → 0.2.1-test.4

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.
@@ -0,0 +1,191 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as defaultInput, stderr as defaultOutput } from "node:process";
3
+ export class TerminalPromptCancelledError extends Error {
4
+ constructor() {
5
+ super("Login cancelled");
6
+ this.name = "TerminalPromptCancelledError";
7
+ }
8
+ }
9
+ export class TerminalPromptInteractiveRequiredError extends Error {
10
+ constructor() {
11
+ super("Interactive terminal input is required");
12
+ this.name = "TerminalPromptInteractiveRequiredError";
13
+ }
14
+ }
15
+ function validateValue(value, label, maxLength, required = true) {
16
+ const normalized = value.trim();
17
+ if ((!normalized && required) || normalized.length > maxLength || /[\u0000-\u001f\u007f]/.test(normalized)) {
18
+ throw new Error(`${label} is invalid`);
19
+ }
20
+ return normalized;
21
+ }
22
+ function isTty(stream) {
23
+ return Boolean(stream.isTTY);
24
+ }
25
+ function throwIfAborted(signal) {
26
+ if (signal?.aborted)
27
+ throw new TerminalPromptCancelledError();
28
+ }
29
+ async function ask(rl, label, defaultValue, output, signal, required = true) {
30
+ throwIfAborted(signal);
31
+ const prompt = `${label}${defaultValue === undefined ? "" : ` [${defaultValue}]`}: `;
32
+ let remove = () => { };
33
+ const cancellation = new Promise((_resolve, reject) => {
34
+ const cancel = () => reject(new TerminalPromptCancelledError());
35
+ signal?.addEventListener("abort", cancel, { once: true });
36
+ remove = () => signal?.removeEventListener("abort", cancel);
37
+ });
38
+ try {
39
+ const value = await Promise.race([rl.question(prompt), cancellation]);
40
+ const answer = value.trim() || defaultValue || "";
41
+ if (!answer && required) {
42
+ output.write("A value is required. Please try again.\n");
43
+ return ask(rl, label, defaultValue, output, signal, required);
44
+ }
45
+ return answer;
46
+ }
47
+ finally {
48
+ remove();
49
+ }
50
+ }
51
+ export async function promptHiddenPassword(input, output, label, signal) {
52
+ const rawInput = input;
53
+ if (typeof rawInput.setRawMode !== "function" || typeof input.on !== "function") {
54
+ throw new TerminalPromptInteractiveRequiredError();
55
+ }
56
+ throwIfAborted(signal);
57
+ output.write(label);
58
+ let answer = "";
59
+ let settled = false;
60
+ const previousRaw = rawInput.isRaw === true;
61
+ const previousPaused = typeof rawInput.isPaused === "function" ? rawInput.isPaused() : false;
62
+ const finish = (resolve, reject, error) => {
63
+ if (settled)
64
+ return;
65
+ settled = true;
66
+ input.removeListener?.("data", onData);
67
+ input.removeListener?.("error", onError);
68
+ input.removeListener?.("end", onEnd);
69
+ input.removeListener?.("close", onClose);
70
+ signal?.removeEventListener("abort", onAbort);
71
+ try {
72
+ rawInput.setRawMode?.(previousRaw);
73
+ }
74
+ catch { /* best effort */ }
75
+ if (previousPaused)
76
+ rawInput.pause?.();
77
+ else
78
+ rawInput.resume?.();
79
+ output.write("\n");
80
+ if (error)
81
+ reject(error);
82
+ else
83
+ resolve(answer);
84
+ };
85
+ let resolveValue;
86
+ let rejectValue;
87
+ const promise = new Promise((resolve, reject) => {
88
+ resolveValue = resolve;
89
+ rejectValue = reject;
90
+ });
91
+ const onAbort = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
92
+ const onError = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
93
+ const onEnd = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
94
+ const onClose = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
95
+ const onData = (chunk) => {
96
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
97
+ for (const character of text) {
98
+ if (character === "\u0003" || character === "\u001b") {
99
+ finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
100
+ return;
101
+ }
102
+ if (character === "\r" || character === "\n") {
103
+ if (!answer) {
104
+ output.write("\nA value is required. Please try again.\n");
105
+ output.write(label);
106
+ continue;
107
+ }
108
+ finish(resolveValue, rejectValue);
109
+ return;
110
+ }
111
+ if (character === "\u007f" || character === "\b") {
112
+ answer = answer.slice(0, -1);
113
+ continue;
114
+ }
115
+ if (character < " " || character === "\u007f") {
116
+ finish(resolveValue, rejectValue, new Error("Password is invalid"));
117
+ return;
118
+ }
119
+ if (character >= " " && character !== "\u007f") {
120
+ if (answer.length >= 4096) {
121
+ finish(resolveValue, rejectValue, new Error("Password is invalid"));
122
+ return;
123
+ }
124
+ answer += character;
125
+ }
126
+ }
127
+ };
128
+ input.on("data", onData);
129
+ input.once?.("error", onError);
130
+ input.once?.("end", onEnd);
131
+ input.once?.("close", onClose);
132
+ signal?.addEventListener("abort", onAbort, { once: true });
133
+ try {
134
+ rawInput.setRawMode(true);
135
+ rawInput.resume?.();
136
+ return await promise;
137
+ }
138
+ catch (error) {
139
+ finish(resolveValue, rejectValue, error instanceof Error ? error : new TerminalPromptCancelledError());
140
+ throw error;
141
+ }
142
+ }
143
+ async function secret(input, output, label, signal, injected) {
144
+ throwIfAborted(signal);
145
+ const value = injected
146
+ ? await injected(label)
147
+ : isTty(input)
148
+ ? await promptHiddenPassword(input, output, label, signal)
149
+ : (() => { throw new TerminalPromptInteractiveRequiredError(); })();
150
+ // Passwords are opaque credentials: preserve leading/trailing spaces and
151
+ // validate only the same boundaries enforced by the HTTP contract.
152
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value)) {
153
+ throw new Error("Password is invalid");
154
+ }
155
+ return value;
156
+ }
157
+ export async function promptTerminalCredentials(options = {}) {
158
+ const input = options.input ?? defaultInput;
159
+ const output = options.output ?? defaultOutput;
160
+ const rl = createInterface({ input, output });
161
+ try {
162
+ const usernameLabel = options.usernameLabel ?? "Zhihu username";
163
+ const tenantLabel = options.tenantLabel ?? "Tenant id (optional)";
164
+ const captchaLabel = options.captchaLabel ?? "Captcha verification (optional)";
165
+ const passwordLabel = options.passwordLabel ?? "Zhihu password: ";
166
+ const usernameRaw = options.valuePrompt
167
+ ? await options.valuePrompt(usernameLabel, options.defaultUsername)
168
+ : await ask(rl, usernameLabel, options.defaultUsername, output, options.signal);
169
+ const username = validateValue(usernameRaw, "Username", 256);
170
+ const tenantRaw = options.valuePrompt
171
+ ? await options.valuePrompt(tenantLabel, options.defaultTenantId)
172
+ : await ask(rl, tenantLabel, options.defaultTenantId, output, options.signal, false);
173
+ const tenant_id = tenantRaw.trim() ? validateValue(tenantRaw, "Tenant id", 128) : undefined;
174
+ const captchaRaw = options.valuePrompt
175
+ ? await options.valuePrompt(captchaLabel)
176
+ : await ask(rl, captchaLabel, undefined, output, options.signal, false);
177
+ const captcha_verification = captchaRaw.trim() ? validateValue(captchaRaw, "Captcha verification", 4096) : undefined;
178
+ if (isTty(input) && !options.secretPrompt)
179
+ rl.close();
180
+ const password = await secret(input, output, passwordLabel, options.signal, options.secretPrompt);
181
+ return {
182
+ username,
183
+ password,
184
+ ...(tenant_id ? { tenant_id } : {}),
185
+ ...(captcha_verification ? { captcha_verification } : {}),
186
+ };
187
+ }
188
+ finally {
189
+ rl.close();
190
+ }
191
+ }
@@ -1,6 +1,7 @@
1
1
  import { CredentialStoreError } from "./credentials.js";
2
2
  import { HttpError } from "./http.js";
3
3
  import { withProfileLock, ProfileLockError } from "./profile-lock.js";
4
+ import { cliCommand } from "./release.js";
4
5
  const CLOCK_SKEW_SECONDS = 60;
5
6
  export class CliAuthError extends Error {
6
7
  code;
@@ -64,21 +65,39 @@ function mapFailure(error) {
64
65
  : error.code === "FORBIDDEN"
65
66
  || error.code === "CAPABILITY_SCOPE_REQUIRED"
66
67
  || error.code === "CAPABILITY_NOT_AVAILABLE"
68
+ || error.code === "A2A_DISABLED"
69
+ || error.code === "A2A_CAPABILITY_NOT_FOUND"
70
+ || error.code === "A2A_CAPABILITY_DISABLED"
71
+ || error.code === "A2A_TASK_NOT_FOUND"
72
+ || error.code === "A2A_FORBIDDEN"
73
+ || error.code === "A2A_CANCELLED"
74
+ || error.code === "A2A_EMPTY_RESPONSE"
75
+ || error.code === "A2A_EXECUTION_INDETERMINATE"
67
76
  || error.code === "EMPLOYEE_NOT_FOUND"
68
77
  || error.code === "EMPLOYEE_NOT_CHATABLE"
69
78
  || error.code === "CONVERSATION_NOT_FOUND"
70
79
  ? 5
71
80
  : error.code === "SERVER_ERROR"
81
+ || error.code === "A2A_RUNTIME_ERROR"
82
+ || error.code === "A2A_TIMEOUT"
83
+ || error.code === "A2A_DUPLICATE"
84
+ || error.code === "A2A_TASK_START_FAILED"
72
85
  ? 6
73
86
  : error.code === "VERSION_INCOMPATIBLE"
74
87
  || error.code === "SERVICE_CONFIGURATION_REQUIRED"
88
+ || error.code === "TERMINAL_LOGIN_REQUIRED"
75
89
  ? 7
76
- : error.code === "PROTOCOL_ERROR"
77
- || error.code === "RESPONSE_TOO_LARGE"
78
- || error.code === "UPSTREAM_PROTOCOL_ERROR"
79
- || error.code === "UPSTREAM_RESPONSE_TOO_LARGE"
80
- ? 8
81
- : 4;
90
+ : error.code === "AUTH_LOGIN_FAILED"
91
+ || error.code === "AUTH_ACCOUNT_LOCKED"
92
+ || error.code === "AUTH_CAPTCHA_REQUIRED"
93
+ || error.code === "INTERACTIVE_REQUIRED"
94
+ ? 2
95
+ : error.code === "PROTOCOL_ERROR"
96
+ || error.code === "RESPONSE_TOO_LARGE"
97
+ || error.code === "UPSTREAM_PROTOCOL_ERROR"
98
+ || error.code === "UPSTREAM_RESPONSE_TOO_LARGE"
99
+ ? 8
100
+ : 4;
82
101
  return new CliAuthError(error.code, error.message, { retryable: error.retryable, exitCode });
83
102
  }
84
103
  return new CliAuthError("NETWORK_ERROR", "Unable to reach the Zhihu service", { retryable: true, exitCode: 4 });
@@ -87,13 +106,15 @@ export class TokenManager {
87
106
  client;
88
107
  credentials;
89
108
  profile;
109
+ environment;
90
110
  operationTail = Promise.resolve();
91
111
  accessTokenValue;
92
112
  accessInfoValue;
93
- constructor(client, credentials, profile) {
113
+ constructor(client, credentials, profile, options = {}) {
94
114
  this.client = client;
95
115
  this.credentials = credentials;
96
116
  this.profile = profile;
117
+ this.environment = options.environment;
97
118
  }
98
119
  get accessInfo() {
99
120
  return this.accessInfoValue;
@@ -110,7 +131,7 @@ export class TokenManager {
110
131
  }
111
132
  async refreshWithProfileLock() {
112
133
  try {
113
- return await withProfileLock(this.profile, () => this.refreshWithinLock());
134
+ return await withProfileLock(this.profile, () => this.refreshWithinLock(), { environment: this.environment });
114
135
  }
115
136
  catch (error) {
116
137
  throw mapFailure(error);
@@ -119,7 +140,7 @@ export class TokenManager {
119
140
  async refreshWithinLock() {
120
141
  const oldCredential = await this.credentials.get();
121
142
  if (!oldCredential) {
122
- throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "No active login; run zhihu auth login.", { exitCode: 3 });
143
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `No active login; run ${cliCommand("auth", "login")}.`, { exitCode: 3 });
123
144
  }
124
145
  let refreshed;
125
146
  try {
@@ -157,7 +178,7 @@ export class TokenManager {
157
178
  // Keep the primary credential-store failure stable.
158
179
  }
159
180
  const mapped = mapFailure(error);
160
- throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "Credential storage failed; run zhihu auth login again.", { retryable: mapped.retryable, exitCode: 3 });
181
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `Credential storage failed; run ${cliCommand("auth", "login")} again.`, { retryable: mapped.retryable, exitCode: 3 });
161
182
  }
162
183
  this.accessTokenValue = refreshed.access_token;
163
184
  this.accessInfoValue = {
@@ -210,9 +231,9 @@ export class TokenManager {
210
231
  if (!isAuthenticationFailure(error))
211
232
  throw mapFailure(error);
212
233
  await this.clearDeadCredentialWithinLock();
213
- throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "Your Zhihu session has expired. Run `zhihu auth login`.", { exitCode: 3 });
234
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `Your Zhihu session has expired. Run \`${cliCommand("auth", "login")}\`.`, { exitCode: 3 });
214
235
  }
215
- }));
236
+ }, { environment: this.environment }));
216
237
  }
217
238
  catch (error) {
218
239
  throw mapFailure(error);
@@ -251,7 +272,7 @@ export class TokenManager {
251
272
  if (!isAuthenticationFailure(error) || observedEvent)
252
273
  throw mapFailure(error);
253
274
  await this.clearDeadCredential();
254
- throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "Your Zhihu session has expired. Run `zhihu auth login`.", { exitCode: 3 });
275
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `Your Zhihu session has expired. Run \`${cliCommand("auth", "login")}\`.`, { exitCode: 3 });
255
276
  }
256
277
  }
257
278
  catch (error) {
@@ -259,7 +280,7 @@ export class TokenManager {
259
280
  }
260
281
  }
261
282
  async clearDeadCredential() {
262
- await this.exclusive(() => withProfileLock(this.profile, () => this.clearDeadCredentialWithinLock()));
283
+ await this.exclusive(() => withProfileLock(this.profile, () => this.clearDeadCredentialWithinLock(), { environment: this.environment }));
263
284
  }
264
285
  async clearDeadCredentialWithinLock() {
265
286
  let currentCredential = null;
@@ -307,7 +328,7 @@ export class TokenManager {
307
328
  this.accessTokenValue = undefined;
308
329
  this.accessInfoValue = undefined;
309
330
  return "revoked";
310
- }));
331
+ }, { environment: this.environment }));
311
332
  }
312
333
  catch (error) {
313
334
  throw mapFailure(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckypiggy/zhihu-ai-cli",
3
- "version": "0.1.0-test.1",
3
+ "version": "0.2.1-test.4",
4
4
  "description": "智护 Pilot CLI:认证、数字员工、资产、指标、告警、指定告警诊断和巡检只读能力(P1 Read-only)",
5
5
  "keywords": [
6
6
  "zhihu-pilot",
@@ -35,7 +35,9 @@
35
35
  "test": "npm run build && node --test tests/*.mjs",
36
36
  "pack:dry-run": "npm_config_cache=\"${TMPDIR:-/tmp}/zhihu-ai-cli-npm-cache\" npm pack --dry-run --json --loglevel=error",
37
37
  "build:offline": "node scripts/build-offline-release.mjs",
38
- "verify:offline": "node scripts/verify-offline-release.mjs"
38
+ "build:public-test": "node scripts/build-public-test-package.mjs",
39
+ "verify:offline": "node scripts/verify-offline-release.mjs",
40
+ "verify:public-test": "node scripts/verify-public-test-release.mjs"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@types/node": "22.15.0",
package/dist/browser.js DELETED
@@ -1,49 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- export function validAuthorizationUrl(value) {
3
- try {
4
- const parsed = new URL(value);
5
- const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
6
- return /^https?:$/.test(parsed.protocol)
7
- && (parsed.protocol === "https:" || loopback)
8
- && !parsed.username
9
- && !parsed.password
10
- && !/[\u0000-\u001f\u007f]/.test(value);
11
- }
12
- catch {
13
- return false;
14
- }
15
- }
16
- /** Launch a browser without a shell; the URL is never logged by this helper. */
17
- export const openSystemBrowser = (authorizationUrl) => new Promise((resolve) => {
18
- if (!validAuthorizationUrl(authorizationUrl)) {
19
- resolve(false);
20
- return;
21
- }
22
- const command = process.platform === "darwin" ? "/usr/bin/open" : process.platform === "linux" ? "/usr/bin/xdg-open" : undefined;
23
- if (!command) {
24
- resolve(false);
25
- return;
26
- }
27
- let child;
28
- try {
29
- child = spawn(command, [authorizationUrl], { shell: false, stdio: ["ignore", "ignore", "ignore"] });
30
- }
31
- catch {
32
- resolve(false);
33
- return;
34
- }
35
- let settled = false;
36
- const timer = setTimeout(() => {
37
- child.kill();
38
- finish(false);
39
- }, 5000);
40
- const finish = (ok) => {
41
- if (settled)
42
- return;
43
- settled = true;
44
- clearTimeout(timer);
45
- resolve(ok);
46
- };
47
- child.once("error", () => finish(false));
48
- child.once("close", (code) => finish(code === 0));
49
- });
package/dist/pkce.js DELETED
@@ -1,15 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- const VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;
3
- function base64Url(value) {
4
- return value.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
5
- }
6
- /** Generate RFC 7636 S256 material without retaining it outside the caller. */
7
- export function createPkcePair() {
8
- const verifier = base64Url(randomBytes(32));
9
- const challenge = base64Url(createHash("sha256").update(verifier, "ascii").digest());
10
- return Object.freeze({ verifier, challenge, method: "S256" });
11
- }
12
- export function isValidCodeVerifier(value) {
13
- return typeof value === "string" && VERIFIER_PATTERN.test(value);
14
- }
15
- export const generatePkce = createPkcePair;