@luckypiggy/zhihu-ai-cli 0.1.0-test.1 → 0.2.1-test.5
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 +39 -20
- package/assets/codex-skills/zhihu-aiops/SKILL.md +54 -16
- package/assets/codex-skills/zhihu-aiops/references/commands.md +40 -14
- package/assets/codex-skills/zhihu-aiops/references/errors.md +2 -1
- package/assets/codex-skills/zhihu-aiops/references/safety.md +11 -4
- package/dist/auth-service.js +71 -83
- package/dist/cli.js +291 -22
- package/dist/codex-skill.js +52 -10
- package/dist/config.js +14 -3
- package/dist/contracts.js +3 -1
- package/dist/credentials.js +176 -33
- package/dist/http.js +413 -67
- package/dist/install.js +318 -60
- package/dist/local-runner.js +1717 -0
- package/dist/profile-lock.js +39 -3
- package/dist/release.js +27 -0
- package/dist/terminal-prompt.js +191 -0
- package/dist/token-manager.js +36 -15
- package/package.json +4 -2
- package/dist/browser.js +0 -49
- package/dist/pkce.js +0 -15
package/dist/credentials.js
CHANGED
|
@@ -17,26 +17,113 @@ function safeProfile(profile) {
|
|
|
17
17
|
}
|
|
18
18
|
return profile;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const MAC_SECURITY_EXECUTABLE = "/usr/bin/security";
|
|
21
|
+
const MAC_EXPECT_EXECUTABLE = "/usr/bin/expect";
|
|
22
|
+
const MAC_EXPECT_FAILURE = 70;
|
|
23
|
+
/** Reserved exit status of the fixed macOS Expect helper. Other children may also exit 124. */
|
|
24
|
+
export const MAC_EXPECT_TIMEOUT = 124;
|
|
25
|
+
/** Read, delete, availability, and Linux writes keep the original five-second bound. */
|
|
26
|
+
export const CREDENTIAL_COMMAND_TIMEOUT_MS = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* Bounded Expect wait for the whole macOS Keychain handshake. Both prompts
|
|
29
|
+
* share this clock because exp_continue does not restart it.
|
|
30
|
+
*/
|
|
31
|
+
export const MAC_KEYCHAIN_WRITE_EXPECT_TIMEOUT_SECONDS = 15;
|
|
32
|
+
/** Lets Expect exit and the child be reaped after its own timeout. */
|
|
33
|
+
export const MAC_KEYCHAIN_WRITE_CLEANUP_MARGIN_MS = 2000;
|
|
34
|
+
export const MAC_KEYCHAIN_WRITE_PROCESS_TIMEOUT_MS = MAC_KEYCHAIN_WRITE_EXPECT_TIMEOUT_SECONDS * 1000 + MAC_KEYCHAIN_WRITE_CLEANUP_MARGIN_MS;
|
|
35
|
+
const MAC_EXPECT_SCRIPT_BODY = `
|
|
36
|
+
set timeout ${MAC_KEYCHAIN_WRITE_EXPECT_TIMEOUT_SECONDS}
|
|
37
|
+
log_user 0
|
|
38
|
+
if {[gets stdin secret] < 0} { exit ${MAC_EXPECT_FAILURE} }
|
|
39
|
+
set prompt_count 0
|
|
40
|
+
spawn -noecho $zhihu_command {*}$zhihu_args
|
|
41
|
+
expect {
|
|
42
|
+
"password data for new item:" {
|
|
43
|
+
if {$prompt_count != 0} { exit ${MAC_EXPECT_FAILURE} }
|
|
44
|
+
set prompt_count 1
|
|
45
|
+
send -- "$secret\\r"
|
|
46
|
+
exp_continue -continue_timer
|
|
47
|
+
}
|
|
48
|
+
"retype password for new item:" {
|
|
49
|
+
if {$prompt_count != 1} { exit ${MAC_EXPECT_FAILURE} }
|
|
50
|
+
set prompt_count 2
|
|
51
|
+
send -- "$secret\\r"
|
|
52
|
+
exp_continue -continue_timer
|
|
53
|
+
}
|
|
54
|
+
timeout { exit ${MAC_EXPECT_TIMEOUT} }
|
|
55
|
+
eof {}
|
|
56
|
+
}
|
|
57
|
+
if {[catch {wait} wait_result] != 0} { exit ${MAC_EXPECT_FAILURE} }
|
|
58
|
+
if {[llength $wait_result] == 4
|
|
59
|
+
&& [string is integer -strict [lindex $wait_result 2]]
|
|
60
|
+
&& [string is integer -strict [lindex $wait_result 3]]} {
|
|
61
|
+
if {[lindex $wait_result 2] != 0} { exit ${MAC_EXPECT_FAILURE} }
|
|
62
|
+
set exit_status [lindex $wait_result 3]
|
|
63
|
+
if {$exit_status == 0 && $prompt_count != 2} { exit ${MAC_EXPECT_FAILURE} }
|
|
64
|
+
exit $exit_status
|
|
65
|
+
}
|
|
66
|
+
exit ${MAC_EXPECT_FAILURE}
|
|
67
|
+
`;
|
|
68
|
+
function commandEnvironment() {
|
|
69
|
+
const allowedEnvironmentKeys = new Set([
|
|
70
|
+
"HOME",
|
|
71
|
+
"PATH",
|
|
72
|
+
"LANG",
|
|
73
|
+
"LC_ALL",
|
|
74
|
+
"LC_CTYPE",
|
|
75
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
76
|
+
"XDG_RUNTIME_DIR",
|
|
77
|
+
"XDG_CONFIG_HOME",
|
|
78
|
+
"DISPLAY",
|
|
79
|
+
]);
|
|
80
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => allowedEnvironmentKeys.has(key) || key.startsWith("LC_")));
|
|
81
|
+
}
|
|
82
|
+
function isMacSecurityWrite(command, args) {
|
|
83
|
+
return process.platform === "darwin"
|
|
84
|
+
&& command === MAC_SECURITY_EXECUTABLE
|
|
85
|
+
&& args[0] === "add-generic-password"
|
|
86
|
+
&& args.at(-1) === "-w";
|
|
87
|
+
}
|
|
88
|
+
function tclQuote(value) {
|
|
89
|
+
return `"${value
|
|
90
|
+
.replaceAll("\\", "\\\\")
|
|
91
|
+
.replaceAll("\"", "\\\"")
|
|
92
|
+
.replaceAll("$", "\\$")
|
|
93
|
+
.replaceAll("[", "\\[")
|
|
94
|
+
.replaceAll("]", "\\]")
|
|
95
|
+
.replaceAll("\r", "\\r")
|
|
96
|
+
.replaceAll("\n", "\\n")}"`;
|
|
97
|
+
}
|
|
98
|
+
function macSecurityExpectArgs(command, args) {
|
|
99
|
+
const commandLiteral = tclQuote(command);
|
|
100
|
+
const argsLiteral = args.map(tclQuote).join(" ");
|
|
101
|
+
const script = `set zhihu_command ${commandLiteral}\nset zhihu_args [list ${argsLiteral}]${MAC_EXPECT_SCRIPT_BODY}`;
|
|
102
|
+
return ["-c", script];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Remove an input value from command output before it can reach diagnostics.
|
|
106
|
+
*/
|
|
107
|
+
function removeInputFromOutput(output, input) {
|
|
108
|
+
if (!input)
|
|
109
|
+
return output;
|
|
110
|
+
const candidates = [input, input.endsWith("\n") ? input.slice(0, -1) : input];
|
|
111
|
+
return candidates.reduce((value, candidate) => candidate ? value.split(candidate).join("") : value, output);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Spawn a fixed executable without putting secret input in argv/env.
|
|
115
|
+
*/
|
|
21
116
|
export const processCommandRunner = (command, args, options = {}) => new Promise((resolve, reject) => {
|
|
117
|
+
const useMacSecurityExpect = isMacSecurityWrite(command, args);
|
|
118
|
+
const executable = useMacSecurityExpect ? MAC_EXPECT_EXECUTABLE : command;
|
|
119
|
+
const executableArgs = useMacSecurityExpect ? macSecurityExpectArgs(command, args) : [...args];
|
|
22
120
|
let child;
|
|
23
121
|
try {
|
|
24
|
-
|
|
25
|
-
"HOME",
|
|
26
|
-
"PATH",
|
|
27
|
-
"LANG",
|
|
28
|
-
"LC_ALL",
|
|
29
|
-
"LC_CTYPE",
|
|
30
|
-
"DBUS_SESSION_BUS_ADDRESS",
|
|
31
|
-
"XDG_RUNTIME_DIR",
|
|
32
|
-
"XDG_CONFIG_HOME",
|
|
33
|
-
"DISPLAY",
|
|
34
|
-
]);
|
|
35
|
-
const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => allowedEnvironmentKeys.has(key) || key.startsWith("LC_")));
|
|
36
|
-
child = spawn(command, [...args], {
|
|
122
|
+
child = spawn(executable, executableArgs, {
|
|
37
123
|
shell: false,
|
|
38
124
|
stdio: ["pipe", "pipe", "pipe"],
|
|
39
|
-
env:
|
|
125
|
+
env: commandEnvironment(),
|
|
126
|
+
detached: false,
|
|
40
127
|
});
|
|
41
128
|
}
|
|
42
129
|
catch {
|
|
@@ -46,36 +133,87 @@ export const processCommandRunner = (command, args, options = {}) => new Promise
|
|
|
46
133
|
let stdout = "";
|
|
47
134
|
let stderr = "";
|
|
48
135
|
let timer;
|
|
49
|
-
|
|
136
|
+
let settled = false;
|
|
137
|
+
let inputSent = false;
|
|
138
|
+
let terminated = false;
|
|
139
|
+
const clearMainTimer = () => {
|
|
50
140
|
if (timer)
|
|
51
141
|
clearTimeout(timer);
|
|
52
|
-
|
|
142
|
+
};
|
|
143
|
+
const terminateChild = () => {
|
|
144
|
+
if (terminated)
|
|
145
|
+
return;
|
|
146
|
+
terminated = true;
|
|
147
|
+
try {
|
|
148
|
+
child.kill();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Cleanup is best effort; do not turn a child race into an uncaught
|
|
152
|
+
// stream error.
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const finish = (result) => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
clearMainTimer();
|
|
160
|
+
const cleanStdout = removeInputFromOutput(stdout, options.input);
|
|
161
|
+
const cleanStderr = removeInputFromOutput(stderr, options.input);
|
|
162
|
+
resolve({ ...result, stdout: cleanStdout, stderr: cleanStderr });
|
|
163
|
+
};
|
|
164
|
+
const failUnavailable = (message) => {
|
|
165
|
+
if (settled)
|
|
166
|
+
return;
|
|
167
|
+
settled = true;
|
|
168
|
+
terminateChild();
|
|
169
|
+
clearMainTimer();
|
|
170
|
+
reject(new CredentialStoreError("CREDENTIAL_STORE_UNAVAILABLE", message));
|
|
171
|
+
};
|
|
172
|
+
const sendInput = () => {
|
|
173
|
+
if (inputSent || settled)
|
|
174
|
+
return;
|
|
175
|
+
inputSent = true;
|
|
176
|
+
try {
|
|
177
|
+
if (options.input !== undefined) {
|
|
178
|
+
// For macOS `security ... -w`, the expect helper reads the single
|
|
179
|
+
// logical credential input and answers each prompt on its PTY.
|
|
180
|
+
// The input is still only written to the child stream, never argv/env.
|
|
181
|
+
child.stdin.write(options.input);
|
|
182
|
+
}
|
|
183
|
+
child.stdin.end();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
failUnavailable("System credential store is unavailable");
|
|
187
|
+
}
|
|
53
188
|
};
|
|
54
189
|
child.stdout.on("data", (chunk) => {
|
|
55
190
|
stdout += chunk.toString();
|
|
56
191
|
if (stdout.length > 16384)
|
|
57
|
-
|
|
192
|
+
terminateChild();
|
|
58
193
|
});
|
|
59
194
|
child.stderr.on("data", (chunk) => {
|
|
60
195
|
stderr += chunk.toString();
|
|
61
196
|
if (stderr.length > 16384)
|
|
62
|
-
|
|
197
|
+
terminateChild();
|
|
63
198
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
199
|
+
// Keep listeners attached after settlement so a late EPIPE from a child
|
|
200
|
+
// that exited during the handshake cannot become an uncaught error.
|
|
201
|
+
child.stdin.on("error", () => failUnavailable("System credential store is unavailable"));
|
|
202
|
+
child.on("error", () => failUnavailable("System credential store is unavailable"));
|
|
203
|
+
child.once("close", (code) => {
|
|
204
|
+
const status = code ?? 1;
|
|
205
|
+
if (useMacSecurityExpect && status === MAC_EXPECT_TIMEOUT) {
|
|
206
|
+
failUnavailable("System credential store timed out");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
finish({ code: status, stdout, stderr });
|
|
68
210
|
});
|
|
69
|
-
child.once("close", (code) => finish({ code: code ?? 1, stdout, stderr }));
|
|
70
211
|
if (options.timeoutMs) {
|
|
71
212
|
timer = setTimeout(() => {
|
|
72
|
-
|
|
73
|
-
reject(new CredentialStoreError("CREDENTIAL_STORE_UNAVAILABLE", "System credential store timed out"));
|
|
213
|
+
failUnavailable("System credential store timed out");
|
|
74
214
|
}, options.timeoutMs);
|
|
75
215
|
}
|
|
76
|
-
|
|
77
|
-
child.stdin.write(options.input);
|
|
78
|
-
child.stdin.end();
|
|
216
|
+
sendInput();
|
|
79
217
|
});
|
|
80
218
|
function missingResult(result) {
|
|
81
219
|
return result.code !== 0 && !result.stdout.trim() && !result.stderr.trim();
|
|
@@ -118,7 +256,7 @@ export class SecureCredentialStore {
|
|
|
118
256
|
async available() {
|
|
119
257
|
try {
|
|
120
258
|
const command = this.command();
|
|
121
|
-
const result = await this.runner(command.executable, command.checkArgs, { timeoutMs:
|
|
259
|
+
const result = await this.runner(command.executable, command.checkArgs, { timeoutMs: CREDENTIAL_COMMAND_TIMEOUT_MS });
|
|
122
260
|
if (result.code !== 0)
|
|
123
261
|
throw new CredentialStoreError("CREDENTIAL_STORE_UNAVAILABLE", "System credential store is unavailable");
|
|
124
262
|
return true;
|
|
@@ -132,7 +270,7 @@ export class SecureCredentialStore {
|
|
|
132
270
|
async get() {
|
|
133
271
|
try {
|
|
134
272
|
const command = this.command();
|
|
135
|
-
const result = await this.runner(command.executable, command.readArgs, { timeoutMs:
|
|
273
|
+
const result = await this.runner(command.executable, command.readArgs, { timeoutMs: CREDENTIAL_COMMAND_TIMEOUT_MS });
|
|
136
274
|
if (result.code !== 0) {
|
|
137
275
|
const diagnostic = result.stderr.toLowerCase();
|
|
138
276
|
if (missingResult(result) || diagnostic.includes("could not be found") || diagnostic.includes("no such secret") || diagnostic.includes("not found"))
|
|
@@ -158,8 +296,13 @@ export class SecureCredentialStore {
|
|
|
158
296
|
try {
|
|
159
297
|
const command = this.command();
|
|
160
298
|
const result = await this.runner(command.executable, command.writeArgs, {
|
|
299
|
+
// The process runner passes this single logical credential to the
|
|
300
|
+
// fixed macOS Expect helper, which answers both security prompts in
|
|
301
|
+
// the child PTY without placing the secret in argv or the environment.
|
|
161
302
|
input: `${credential}\n`,
|
|
162
|
-
timeoutMs:
|
|
303
|
+
timeoutMs: this.platform === "darwin"
|
|
304
|
+
? MAC_KEYCHAIN_WRITE_PROCESS_TIMEOUT_MS
|
|
305
|
+
: CREDENTIAL_COMMAND_TIMEOUT_MS,
|
|
163
306
|
});
|
|
164
307
|
if (result.code !== 0)
|
|
165
308
|
throw new CredentialStoreError("CREDENTIAL_STORE_ERROR", "Unable to write system credential");
|
|
@@ -173,7 +316,7 @@ export class SecureCredentialStore {
|
|
|
173
316
|
async delete() {
|
|
174
317
|
try {
|
|
175
318
|
const command = this.command();
|
|
176
|
-
const result = await this.runner(command.executable, command.deleteArgs, { timeoutMs:
|
|
319
|
+
const result = await this.runner(command.executable, command.deleteArgs, { timeoutMs: CREDENTIAL_COMMAND_TIMEOUT_MS });
|
|
177
320
|
const diagnostic = result.stderr.toLowerCase();
|
|
178
321
|
if (result.code !== 0 && !missingResult(result) && !diagnostic.includes("could not be found") && !diagnostic.includes("no such secret") && !diagnostic.includes("not found")) {
|
|
179
322
|
throw new CredentialStoreError("CREDENTIAL_STORE_ERROR", "Unable to delete system credential");
|