@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/cli.js
CHANGED
|
@@ -2,7 +2,9 @@ import { existsSync, lstatSync } from "node:fs";
|
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { CLI_VERSION, MIN_NODE_MAJOR, } from "./contracts.js";
|
|
5
|
+
import { CLI_VERSION, CLI_PACKAGE_NAME, MIN_NODE_MAJOR, } from "./contracts.js";
|
|
6
|
+
import { CLI_RUNNER } from "./release.js";
|
|
7
|
+
import { existingLocalRunnerCommand, inspectLocalRunner, inspectLocalRunnerLock, LocalRunnerError, recoverLocalRunnerLock, } from "./local-runner.js";
|
|
6
8
|
import { ConfigError, DEFAULT_PROFILE, DEFAULT_PROFILE_NAME, checkProfile, configRoot, guardProfileConnectionChange, initializeProfile, ProfileConnectionGuardError, readConfigSnapshot, readProfileRaw, validateProfile, } from "./config.js";
|
|
7
9
|
import { withProfileLock } from "./profile-lock.js";
|
|
8
10
|
import { emitError, emitSuccess } from "./output.js";
|
|
@@ -13,6 +15,7 @@ import { IntegrationHttpClient, HttpError, validateIntegrationInfo, } from "./ht
|
|
|
13
15
|
import { CliAuthError, TokenManager, mapFailure } from "./token-manager.js";
|
|
14
16
|
import { CodexSkillError, inspectCodexSkill, installCodexSkill, uninstallCodexSkill, } from "./codex-skill.js";
|
|
15
17
|
import { InstallError, installWizard } from "./install.js";
|
|
18
|
+
import { promptTerminalCredentials, TerminalPromptCancelledError } from "./terminal-prompt.js";
|
|
16
19
|
class CliFailure extends Error {
|
|
17
20
|
shape;
|
|
18
21
|
constructor(shape) {
|
|
@@ -120,7 +123,7 @@ function versionData() {
|
|
|
120
123
|
cli_version: CLI_VERSION,
|
|
121
124
|
node_version: process.versions.node,
|
|
122
125
|
node_supported: major >= MIN_NODE_MAJOR,
|
|
123
|
-
package:
|
|
126
|
+
package: CLI_PACKAGE_NAME,
|
|
124
127
|
stage: "p1-readonly",
|
|
125
128
|
};
|
|
126
129
|
}
|
|
@@ -208,16 +211,20 @@ export async function doctorData(environment = process.env, options = {}) {
|
|
|
208
211
|
try {
|
|
209
212
|
client = options.clientFactory ? options.clientFactory(profile) : new IntegrationHttpClient(profile);
|
|
210
213
|
const info = validateIntegrationInfo(await client.info(), false);
|
|
211
|
-
const
|
|
214
|
+
const legacyAuth = info.auth?.mode === "pilot_oauth_bff";
|
|
215
|
+
const ready = info.status === "ready"
|
|
216
|
+
&& info.auth?.configured === true
|
|
217
|
+
&& info.auth?.mode === "pilot_terminal_password";
|
|
212
218
|
serviceValidatedReady = ready;
|
|
213
219
|
service = {
|
|
214
220
|
ok: ready,
|
|
215
|
-
state: ready ? "ready" : "configuration_required",
|
|
221
|
+
state: ready ? "ready" : legacyAuth ? "terminal_login_required" : "configuration_required",
|
|
216
222
|
service: info.service,
|
|
217
223
|
protocol_version: info.protocol_version,
|
|
218
224
|
minimum_cli_version: info.minimum_cli_version,
|
|
219
225
|
compatible: true,
|
|
220
226
|
status: info.status,
|
|
227
|
+
...(legacyAuth ? { detail: "This Pilot exposes legacy browser OAuth; terminal login is required for this CLI" } : {}),
|
|
221
228
|
capabilities: info.capabilities,
|
|
222
229
|
};
|
|
223
230
|
network = { ok: true, state: "reachable" };
|
|
@@ -271,9 +278,22 @@ export async function doctorData(environment = process.env, options = {}) {
|
|
|
271
278
|
};
|
|
272
279
|
const runtime = { ok: runtimeMajor >= MIN_NODE_MAJOR, node_version: process.versions.node, minimum_node_major: MIN_NODE_MAJOR };
|
|
273
280
|
const codexCheck = { ok: codex.state === "managed", state: codex.state, ...(codex.target ? { target: codex.target } : {}) };
|
|
281
|
+
const runnerInspection = inspectLocalRunner(environment);
|
|
282
|
+
const lockInspection = inspectLocalRunnerLock(environment);
|
|
283
|
+
const localRunnerLock = {
|
|
284
|
+
ok: lockInspection.state === "missing",
|
|
285
|
+
state: lockInspection.state,
|
|
286
|
+
...(lockInspection.pid !== undefined ? { pid: lockInspection.pid } : {}),
|
|
287
|
+
...(lockInspection.age_ms !== undefined ? { age_ms: lockInspection.age_ms } : {}),
|
|
288
|
+
...(lockInspection.fingerprint ? { fingerprint: lockInspection.fingerprint } : {}),
|
|
289
|
+
...(lockInspection.detail ? { detail: lockInspection.detail } : {}),
|
|
290
|
+
};
|
|
274
291
|
return {
|
|
275
292
|
cli_version: CLI_VERSION,
|
|
276
293
|
stage: "p1-readonly",
|
|
294
|
+
local_runner: runnerInspection.command,
|
|
295
|
+
local_runner_health: runnerInspection.health,
|
|
296
|
+
local_runner_lock: localRunnerLock,
|
|
277
297
|
checks: {
|
|
278
298
|
runtime,
|
|
279
299
|
config: configCheckResult,
|
|
@@ -282,6 +302,7 @@ export async function doctorData(environment = process.env, options = {}) {
|
|
|
282
302
|
authentication,
|
|
283
303
|
capabilities: capabilityCheck,
|
|
284
304
|
codex: codexCheck,
|
|
305
|
+
local_runner_lock: localRunnerLock,
|
|
285
306
|
// Compatibility aliases retained for existing automation; the stable
|
|
286
307
|
// Stage5B contract is the seven checks above.
|
|
287
308
|
base_url: { ok: profileReady, state: profileReady ? "configured" : "not_configured" },
|
|
@@ -326,21 +347,50 @@ function authService(flags) {
|
|
|
326
347
|
throw mapFailure(error);
|
|
327
348
|
}
|
|
328
349
|
const credentials = new SecureCredentialStore(selected.name);
|
|
329
|
-
return new AuthService(client, credentials, selected.name,
|
|
330
|
-
onBrowserFailure: (authorizationUrl) => {
|
|
331
|
-
process.stderr.write(`Browser launch failed. Open this URL manually: ${authorizationUrl}\n`);
|
|
332
|
-
},
|
|
333
|
-
});
|
|
350
|
+
return { service: new AuthService(client, credentials, selected.name), profile: selected.profile, profileName: selected.name };
|
|
334
351
|
}
|
|
335
|
-
async function authCommand(action, flags) {
|
|
352
|
+
async function authCommand(action, flags, json = false) {
|
|
336
353
|
if (action !== "login" && action !== "status" && action !== "logout")
|
|
337
354
|
invalid("Use auth login, auth status, or auth logout");
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
355
|
+
for (const key of flags.keys())
|
|
356
|
+
if (key !== "profile")
|
|
357
|
+
invalid(`Unknown auth option: --${key}`);
|
|
358
|
+
const selected = authService(flags);
|
|
359
|
+
if (action === "login") {
|
|
360
|
+
if (json || !(process.stdin.isTTY
|
|
361
|
+
&& process.stderr.isTTY)) {
|
|
362
|
+
throw new CliFailure({ code: "INTERACTIVE_REQUIRED", message: "Interactive terminal input is required for login; do not pass credentials through flags or environment variables", retryable: false, exit_code: 2 });
|
|
363
|
+
}
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
const onSignal = () => controller.abort();
|
|
366
|
+
process.once("SIGINT", onSignal);
|
|
367
|
+
process.once("SIGTERM", onSignal);
|
|
368
|
+
try {
|
|
369
|
+
const credentials = await promptTerminalCredentials({
|
|
370
|
+
usernameLabel: selected.profile.language === "zh" ? "智护用户名" : "Zhihu username",
|
|
371
|
+
tenantLabel: selected.profile.language === "zh" ? "租户 ID(可留空)" : "Tenant id (optional)",
|
|
372
|
+
captchaLabel: selected.profile.language === "zh" ? "验证码(可留空)" : "Captcha verification (optional)",
|
|
373
|
+
passwordLabel: selected.profile.language === "zh" ? "智护密码:" : "Zhihu password: ",
|
|
374
|
+
defaultUsername: selected.profile.username,
|
|
375
|
+
defaultTenantId: selected.profile.tenant_id,
|
|
376
|
+
signal: controller.signal,
|
|
377
|
+
});
|
|
378
|
+
return await selected.service.login(credentials, { signal: controller.signal });
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof TerminalPromptCancelledError) {
|
|
382
|
+
throw new CliFailure({ code: "USER_CANCELLED", message: "Login cancelled", retryable: false, exit_code: 2 });
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
process.removeListener("SIGINT", onSignal);
|
|
388
|
+
process.removeListener("SIGTERM", onSignal);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
341
391
|
if (action === "status")
|
|
342
|
-
return service.status();
|
|
343
|
-
return service.logout();
|
|
392
|
+
return selected.service.status();
|
|
393
|
+
return selected.service.logout();
|
|
344
394
|
}
|
|
345
395
|
async function capabilitiesCommand(action, flags) {
|
|
346
396
|
if (action !== "list")
|
|
@@ -357,6 +407,108 @@ async function capabilitiesCommand(action, flags) {
|
|
|
357
407
|
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
358
408
|
return tokenManager.callReadOnly((accessToken) => client.capabilities(accessToken));
|
|
359
409
|
}
|
|
410
|
+
const A2A_CAPABILITIES = new Set([
|
|
411
|
+
"resource_query",
|
|
412
|
+
"incident_analysis",
|
|
413
|
+
"inspection_summary",
|
|
414
|
+
"knowledge_qa",
|
|
415
|
+
]);
|
|
416
|
+
function a2aTextOption(flags, key, maxChars, maxBytes) {
|
|
417
|
+
const value = flags.get(key);
|
|
418
|
+
if (typeof value !== "string")
|
|
419
|
+
invalid(`--${key} requires a value`);
|
|
420
|
+
const normalized = value.trim();
|
|
421
|
+
if (!normalized || Array.from(value).length > maxChars || Buffer.byteLength(value, "utf8") > maxBytes
|
|
422
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) {
|
|
423
|
+
invalid(`--${key} must be non-empty text up to ${maxChars} characters and ${maxBytes} UTF-8 bytes`);
|
|
424
|
+
}
|
|
425
|
+
return normalized;
|
|
426
|
+
}
|
|
427
|
+
function a2aTaskIdOption(flags) {
|
|
428
|
+
const value = flags.get("id");
|
|
429
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9:_-]{1,128}$/.test(value)) {
|
|
430
|
+
invalid("--id must be an opaque A2A task id");
|
|
431
|
+
}
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
function a2aHumanSummary(data, language) {
|
|
435
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
436
|
+
return undefined;
|
|
437
|
+
const value = data;
|
|
438
|
+
if (typeof value.task !== "string" || typeof value.target !== "string" || typeof value.result !== "string")
|
|
439
|
+
return undefined;
|
|
440
|
+
if (language === "zh") {
|
|
441
|
+
return [`目标员工:${value.target}`, `任务:${value.task}`, `结果:${value.result}`].join("\n");
|
|
442
|
+
}
|
|
443
|
+
return [`Target employee: ${value.target}`, `Task: ${value.task}`, `Result: ${value.result}`].join("\n");
|
|
444
|
+
}
|
|
445
|
+
async function a2aCommand(command, flags) {
|
|
446
|
+
const [_, action, subaction] = command;
|
|
447
|
+
const allowed = new Set(["profile", "capability", "message", "id"]);
|
|
448
|
+
for (const key of flags.keys())
|
|
449
|
+
if (!allowed.has(key))
|
|
450
|
+
invalid(`Unknown a2a option: --${key}`);
|
|
451
|
+
let operation;
|
|
452
|
+
let capability;
|
|
453
|
+
let message;
|
|
454
|
+
let taskId;
|
|
455
|
+
if (action === "capabilities" && command.length === 2) {
|
|
456
|
+
if (["capability", "message", "id"].some((key) => flags.has(key))) {
|
|
457
|
+
invalid("a2a capabilities does not accept --capability, --message, or --id");
|
|
458
|
+
}
|
|
459
|
+
operation = "capabilities";
|
|
460
|
+
}
|
|
461
|
+
else if (action === "delegate" && command.length === 2) {
|
|
462
|
+
if (flags.has("id"))
|
|
463
|
+
invalid("a2a delegate does not accept --id");
|
|
464
|
+
const rawCapability = a2aTextOption(flags, "capability", 128, 128);
|
|
465
|
+
if (!A2A_CAPABILITIES.has(rawCapability)) {
|
|
466
|
+
invalid("--capability must be resource_query, incident_analysis, inspection_summary, or knowledge_qa");
|
|
467
|
+
}
|
|
468
|
+
capability = rawCapability;
|
|
469
|
+
message = a2aTextOption(flags, "message", 8000, 16 * 1024);
|
|
470
|
+
operation = "delegate";
|
|
471
|
+
}
|
|
472
|
+
else if (action === "task" && subaction === "get" && command.length === 3) {
|
|
473
|
+
if (flags.has("capability") || flags.has("message"))
|
|
474
|
+
invalid("a2a task get does not accept --capability or --message");
|
|
475
|
+
taskId = a2aTaskIdOption(flags);
|
|
476
|
+
operation = "task";
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
invalid("Use a2a capabilities, a2a delegate --capability <id> --message <text>, or a2a task get --id <id>");
|
|
480
|
+
}
|
|
481
|
+
const selected = selectedProfile(flags);
|
|
482
|
+
let client;
|
|
483
|
+
try {
|
|
484
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
throw mapFailure(error);
|
|
488
|
+
}
|
|
489
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
490
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
491
|
+
const controller = new AbortController();
|
|
492
|
+
const onSignal = () => controller.abort();
|
|
493
|
+
process.once("SIGINT", onSignal);
|
|
494
|
+
process.once("SIGTERM", onSignal);
|
|
495
|
+
try {
|
|
496
|
+
if (operation === "capabilities") {
|
|
497
|
+
const data = await tokenManager.callReadOnly((accessToken) => client.a2aCapabilities(accessToken, { signal: controller.signal }));
|
|
498
|
+
return { data };
|
|
499
|
+
}
|
|
500
|
+
if (operation === "delegate") {
|
|
501
|
+
const data = await tokenManager.callReadOnly((accessToken) => client.a2aDelegate(capability, message, accessToken, { signal: controller.signal }));
|
|
502
|
+
return { data, human: a2aHumanSummary(data, selected.profile.language) };
|
|
503
|
+
}
|
|
504
|
+
const data = await tokenManager.callReadOnly((accessToken) => client.a2aTask(taskId, accessToken, { signal: controller.signal }));
|
|
505
|
+
return { data, human: a2aHumanSummary(data, selected.profile.language) };
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
process.removeListener("SIGINT", onSignal);
|
|
509
|
+
process.removeListener("SIGTERM", onSignal);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
360
512
|
async function employeesCommand(action, flags) {
|
|
361
513
|
if (action !== "list")
|
|
362
514
|
invalid("Use employees list");
|
|
@@ -861,7 +1013,7 @@ function codexSkillCommand(action, flags) {
|
|
|
861
1013
|
}
|
|
862
1014
|
async function installCommand(flags, json) {
|
|
863
1015
|
const allowed = new Set([
|
|
864
|
-
"profile", "base-url", "
|
|
1016
|
+
"profile", "base-url", "timeout", "language", "ca-file", "codex-skill-dir",
|
|
865
1017
|
"skip-login", "skip-codex", "force",
|
|
866
1018
|
]);
|
|
867
1019
|
for (const key of flags.keys()) {
|
|
@@ -881,11 +1033,14 @@ async function installCommand(flags, json) {
|
|
|
881
1033
|
invalid(`--${key} requires a value`);
|
|
882
1034
|
return value;
|
|
883
1035
|
};
|
|
1036
|
+
const controller = new AbortController();
|
|
1037
|
+
const onSignal = () => controller.abort();
|
|
1038
|
+
process.on("SIGINT", onSignal);
|
|
1039
|
+
process.on("SIGTERM", onSignal);
|
|
884
1040
|
try {
|
|
885
1041
|
const result = await installWizard({
|
|
886
1042
|
profile: stringOption("profile"),
|
|
887
1043
|
baseUrl: stringOption("base-url"),
|
|
888
|
-
authMode: stringOption("auth-mode"),
|
|
889
1044
|
timeout: stringOption("timeout"),
|
|
890
1045
|
language: stringOption("language"),
|
|
891
1046
|
caFile: stringOption("ca-file"),
|
|
@@ -893,13 +1048,19 @@ async function installCommand(flags, json) {
|
|
|
893
1048
|
skipLogin: flags.get("skip-login") === true,
|
|
894
1049
|
skipCodex: flags.get("skip-codex") === true,
|
|
895
1050
|
force: flags.get("force") === true,
|
|
1051
|
+
signal: controller.signal,
|
|
896
1052
|
json,
|
|
897
1053
|
doctor: (environment, profileName, profile) => doctorData(environment, {
|
|
898
1054
|
...(profileName ? { profile: profileName } : {}),
|
|
899
1055
|
...(profile ? { profileConfig: profile } : {}),
|
|
900
1056
|
}),
|
|
901
1057
|
});
|
|
902
|
-
|
|
1058
|
+
const data = { ...result };
|
|
1059
|
+
// Keep the existing JSON install envelope stable; the locale is used only
|
|
1060
|
+
// for the human summary and is intentionally not a new automation field.
|
|
1061
|
+
if (json)
|
|
1062
|
+
delete data.language;
|
|
1063
|
+
return data;
|
|
903
1064
|
}
|
|
904
1065
|
catch (error) {
|
|
905
1066
|
if (error instanceof InstallError) {
|
|
@@ -912,6 +1073,102 @@ async function installCommand(flags, json) {
|
|
|
912
1073
|
}
|
|
913
1074
|
throw error;
|
|
914
1075
|
}
|
|
1076
|
+
finally {
|
|
1077
|
+
process.removeListener("SIGINT", onSignal);
|
|
1078
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function displayedRunner() {
|
|
1082
|
+
return existingLocalRunnerCommand(process.env) ?? CLI_RUNNER;
|
|
1083
|
+
}
|
|
1084
|
+
export function installHumanSummary(data) {
|
|
1085
|
+
const runner = displayedRunner();
|
|
1086
|
+
const zh = data.language === "zh";
|
|
1087
|
+
const profile = typeof data.profile === "string" ? data.profile : "-";
|
|
1088
|
+
const baseUrl = typeof data.base_url === "string" ? data.base_url : "-";
|
|
1089
|
+
const login = data.login?.status === "logged_in"
|
|
1090
|
+
? (zh ? "已登录" : "logged in")
|
|
1091
|
+
: (zh ? "已跳过" : "skipped");
|
|
1092
|
+
const skillStatus = data.codex?.status;
|
|
1093
|
+
const skill = skillStatus === "unchanged"
|
|
1094
|
+
? (zh ? "已安装(可重复运行相同命令)" : "already installed (safe to rerun)")
|
|
1095
|
+
: skillStatus === "updated"
|
|
1096
|
+
? (zh ? "已更新" : "updated")
|
|
1097
|
+
: skillStatus === "skipped"
|
|
1098
|
+
? (zh ? "已跳过" : "skipped")
|
|
1099
|
+
: (zh ? "已安装" : "installed");
|
|
1100
|
+
if (zh) {
|
|
1101
|
+
return [
|
|
1102
|
+
"安装完成",
|
|
1103
|
+
`Profile:${profile}`,
|
|
1104
|
+
`智护地址:${baseUrl}`,
|
|
1105
|
+
`登录:${login}`,
|
|
1106
|
+
`Codex Skill:${skill}`,
|
|
1107
|
+
"请新开一个 Codex 会话,让它重新发现智护 Skill。",
|
|
1108
|
+
"可以试试:",
|
|
1109
|
+
"- 用智护列出当前活动告警。",
|
|
1110
|
+
"- 用智护查看资产 123 最近一小时 CPU 趋势。",
|
|
1111
|
+
"- 用智护分析告警 456 的根因。",
|
|
1112
|
+
`排查安装:${runner} doctor`,
|
|
1113
|
+
`Skill runner:${runner}`,
|
|
1114
|
+
].join("\n");
|
|
1115
|
+
}
|
|
1116
|
+
return [
|
|
1117
|
+
"Installation complete",
|
|
1118
|
+
`Profile: ${profile}`,
|
|
1119
|
+
`Pilot: ${baseUrl}`,
|
|
1120
|
+
`Login: ${login}`,
|
|
1121
|
+
`Codex Skill: ${skill}`,
|
|
1122
|
+
"Open a new Codex session so it can discover the Zhihu Skill.",
|
|
1123
|
+
"Try natural-language requests:",
|
|
1124
|
+
"- List the current firing alarms.",
|
|
1125
|
+
"- Show the last hour of CPU trend for asset 123.",
|
|
1126
|
+
"- Diagnose the root cause of alarm 456.",
|
|
1127
|
+
`Troubleshoot installation: ${runner} doctor`,
|
|
1128
|
+
`Skill runner: ${runner}`,
|
|
1129
|
+
].join("\n");
|
|
1130
|
+
}
|
|
1131
|
+
function runnerLockCommand(command, flags) {
|
|
1132
|
+
const [, second, third] = command;
|
|
1133
|
+
if (second !== "lock" || command.length !== 3 || (third !== "status" && third !== "recover")) {
|
|
1134
|
+
invalid("Use runner lock status or runner lock recover");
|
|
1135
|
+
}
|
|
1136
|
+
if (third === "status") {
|
|
1137
|
+
for (const key of flags.keys())
|
|
1138
|
+
invalid(`Unknown runner option: --${key}`);
|
|
1139
|
+
const inspection = inspectLocalRunnerLock(process.env);
|
|
1140
|
+
return {
|
|
1141
|
+
state: inspection.state,
|
|
1142
|
+
...(inspection.pid !== undefined ? { pid: inspection.pid } : {}),
|
|
1143
|
+
...(inspection.age_ms !== undefined ? { age_ms: inspection.age_ms } : {}),
|
|
1144
|
+
...(inspection.fingerprint ? { fingerprint: inspection.fingerprint } : {}),
|
|
1145
|
+
...(inspection.detail ? { detail: inspection.detail } : {}),
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
for (const key of flags.keys()) {
|
|
1149
|
+
if (key !== "confirm" && key !== "fingerprint")
|
|
1150
|
+
invalid(`Unknown runner option: --${key}`);
|
|
1151
|
+
}
|
|
1152
|
+
const confirm = flags.get("confirm");
|
|
1153
|
+
const fingerprint = flags.get("fingerprint");
|
|
1154
|
+
if (typeof confirm !== "string" || typeof fingerprint !== "string") {
|
|
1155
|
+
invalid("runner lock recover requires --confirm and --fingerprint");
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
recoverLocalRunnerLock(process.env, { confirm, fingerprint });
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
if (error instanceof LocalRunnerError) {
|
|
1162
|
+
throw new CliFailure({
|
|
1163
|
+
code: error.code,
|
|
1164
|
+
message: error.message,
|
|
1165
|
+
retryable: error.code === "LOCAL_RUNNER_LOCK_TIMEOUT",
|
|
1166
|
+
exit_code: 2,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
return { state: "recovered" };
|
|
915
1172
|
}
|
|
916
1173
|
function helpData() {
|
|
917
1174
|
return {
|
|
@@ -924,6 +1181,9 @@ function helpData() {
|
|
|
924
1181
|
"auth status",
|
|
925
1182
|
"auth logout",
|
|
926
1183
|
"capabilities list",
|
|
1184
|
+
"a2a capabilities",
|
|
1185
|
+
"a2a delegate --capability <id> --message <text>",
|
|
1186
|
+
"a2a task get --id <opaque-task-id>",
|
|
927
1187
|
"employees list",
|
|
928
1188
|
"employees chat <employee-id> --message <text>",
|
|
929
1189
|
"assets list [--keyword <text>] [--model <name|code|id>] [--page N] [--page-size N]",
|
|
@@ -933,12 +1193,14 @@ function helpData() {
|
|
|
933
1193
|
"alarms get --id <id>",
|
|
934
1194
|
"incidents diagnose --alarm-id <id>",
|
|
935
1195
|
"inspections run [--model <name|code|id>] [--window 5m..24h]",
|
|
936
|
-
"install [--profile <name>] [--base-url <https-url>] [--
|
|
1196
|
+
"install [--profile <name>] [--base-url <https-url>] [--timeout N] [--language zh-CN|en] [--ca-file <absolute-file>] [--codex-skill-dir <absolute-path>] [--skip-login] [--skip-codex] [--force]",
|
|
937
1197
|
"install codex [--profile <name>] [--codex-skill-dir <absolute-path>] [--force]",
|
|
938
1198
|
"uninstall codex [--profile <name>] [--codex-skill-dir <absolute-path>] [--force]",
|
|
1199
|
+
"runner lock status",
|
|
1200
|
+
"runner lock recover --confirm recover-stale-runner-lock --fingerprint <fingerprint>",
|
|
939
1201
|
],
|
|
940
1202
|
json: "Append --json for the stable success/error envelope",
|
|
941
|
-
note: "Authentication plus read-only employee, asset, metric, alarm, incident diagnosis, inspection queries, local installation, and the Codex Skill installer are available;
|
|
1203
|
+
note: "Authentication plus read-only employee, asset, metric, alarm, incident diagnosis, inspection queries, local installation, and the Codex Skill installer are available; login is terminal-only and passwords are never accepted through flags or environment variables; formal GA, write operations, and MCP are not available",
|
|
942
1204
|
};
|
|
943
1205
|
}
|
|
944
1206
|
async function run(parsed) {
|
|
@@ -964,9 +1226,12 @@ async function run(parsed) {
|
|
|
964
1226
|
return { data: showConfig(parsed.flags), exit_code: 0 };
|
|
965
1227
|
if (first === "config" && second === "init")
|
|
966
1228
|
return { data: await configInit(parsed.flags), exit_code: 0 };
|
|
1229
|
+
if (first === "runner")
|
|
1230
|
+
return { data: runnerLockCommand(parsed.command, parsed.flags), exit_code: 0 };
|
|
967
1231
|
if (first === "install") {
|
|
968
1232
|
if (!second && parsed.command.length === 1) {
|
|
969
|
-
|
|
1233
|
+
const data = await installCommand(parsed.flags, parsed.json);
|
|
1234
|
+
return { data, exit_code: 0, ...(parsed.json ? {} : { human: installHumanSummary(data) }) };
|
|
970
1235
|
}
|
|
971
1236
|
if (second !== "codex" || parsed.command.length !== 2)
|
|
972
1237
|
invalid("Use install or install codex");
|
|
@@ -978,7 +1243,11 @@ async function run(parsed) {
|
|
|
978
1243
|
return { data: codexSkillCommand("uninstall", parsed.flags), exit_code: 0 };
|
|
979
1244
|
}
|
|
980
1245
|
if (first === "auth")
|
|
981
|
-
return { data: await authCommand(second, parsed.flags), exit_code: 0 };
|
|
1246
|
+
return { data: await authCommand(second, parsed.flags, parsed.json), exit_code: 0 };
|
|
1247
|
+
if (first === "a2a") {
|
|
1248
|
+
const result = await a2aCommand(parsed.command, parsed.flags);
|
|
1249
|
+
return { data: result.data, exit_code: 0, ...(parsed.json || !result.human ? {} : { human: result.human }) };
|
|
1250
|
+
}
|
|
982
1251
|
if (first === "capabilities") {
|
|
983
1252
|
if (parsed.command.length !== 2)
|
|
984
1253
|
invalid("Use capabilities list");
|
package/dist/codex-skill.js
CHANGED
|
@@ -3,7 +3,8 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, parse, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { CLI_VERSION, } from "./contracts.js";
|
|
6
|
+
import { CLI_PACKAGE_NAME, CLI_RUNNER, CLI_VERSION, } from "./contracts.js";
|
|
7
|
+
import { LocalRunnerError, persistLocalCliRunner, retargetPackagedRunner, runnerCommandToken } from "./local-runner.js";
|
|
7
8
|
import { ConfigError, readConfigSnapshot, validateProfile, } from "./config.js";
|
|
8
9
|
export const CODEX_SKILL_NAME = "zhihu-aiops";
|
|
9
10
|
export const CODEX_SKILL_VERSION = CLI_VERSION;
|
|
@@ -118,7 +119,7 @@ function markerFor(files) {
|
|
|
118
119
|
hashes[file] = sha256(files.get(file));
|
|
119
120
|
return {
|
|
120
121
|
owner: "zhihu-ai-cli",
|
|
121
|
-
package:
|
|
122
|
+
package: CLI_PACKAGE_NAME,
|
|
122
123
|
skill: CODEX_SKILL_NAME,
|
|
123
124
|
schema: CODEX_SKILL_SCHEMA,
|
|
124
125
|
schema_version: CODEX_SKILL_SCHEMA,
|
|
@@ -219,7 +220,7 @@ function validMarker(value) {
|
|
|
219
220
|
if (Object.keys(object).some((key) => !allowedKeys.has(key)))
|
|
220
221
|
return false;
|
|
221
222
|
if (object.owner !== "zhihu-ai-cli"
|
|
222
|
-
|| object.package !==
|
|
223
|
+
|| object.package !== CLI_PACKAGE_NAME
|
|
223
224
|
|| object.skill !== CODEX_SKILL_NAME
|
|
224
225
|
|| object.schema !== CODEX_SKILL_SCHEMA
|
|
225
226
|
|| object.schema_version !== CODEX_SKILL_SCHEMA
|
|
@@ -431,12 +432,58 @@ function inspectTarget(target) {
|
|
|
431
432
|
localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not a directory");
|
|
432
433
|
return "directory";
|
|
433
434
|
}
|
|
435
|
+
function installedSkillFiles(environment) {
|
|
436
|
+
let command;
|
|
437
|
+
try {
|
|
438
|
+
command = runnerCommandToken(persistLocalCliRunner(environment));
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (error instanceof CodexSkillError)
|
|
442
|
+
throw error;
|
|
443
|
+
if (error instanceof LocalRunnerError && error.code === "LOCAL_RUNNER_SYMLINK") {
|
|
444
|
+
localError("CODEX_SKILL_SYMLINK", "Local CLI runner path is a symbolic link");
|
|
445
|
+
}
|
|
446
|
+
if (error instanceof LocalRunnerError && error.code === "LOCAL_RUNNER_UNSAFE_PATH") {
|
|
447
|
+
localError("CODEX_SKILL_PATH_INVALID", "Local CLI runner path cannot be embedded in a command");
|
|
448
|
+
}
|
|
449
|
+
if (error instanceof LocalRunnerError && error.code === "LOCAL_RUNNER_LOCK_TIMEOUT") {
|
|
450
|
+
localError("LOCAL_RUNNER_LOCK_TIMEOUT", error.message);
|
|
451
|
+
}
|
|
452
|
+
localError("CODEX_SKILL_IO_ERROR", "Local CLI runner could not be installed");
|
|
453
|
+
}
|
|
454
|
+
const files = sourceFiles();
|
|
455
|
+
let replaced = false;
|
|
456
|
+
for (const file of CODEX_SKILL_FILES) {
|
|
457
|
+
const current = files.get(file);
|
|
458
|
+
if (!current)
|
|
459
|
+
localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets are incomplete");
|
|
460
|
+
const text = current.toString("utf8");
|
|
461
|
+
const packagedRunner = CLI_RUNNER;
|
|
462
|
+
const next = retargetPackagedRunner(text, packagedRunner, command);
|
|
463
|
+
if (next !== text)
|
|
464
|
+
replaced = true;
|
|
465
|
+
if (packagedRunner !== "zhihu" && next.includes(packagedRunner)) {
|
|
466
|
+
localError("CODEX_SKILL_IO_ERROR", "Installed Skill still references the packaged runner");
|
|
467
|
+
}
|
|
468
|
+
files.set(file, Buffer.from(next, "utf8"));
|
|
469
|
+
}
|
|
470
|
+
if (!replaced)
|
|
471
|
+
localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets do not contain the packaged runner");
|
|
472
|
+
return files;
|
|
473
|
+
}
|
|
434
474
|
export function installCodexSkill(options = {}) {
|
|
435
475
|
const target = resolveCodexSkillTarget(options);
|
|
436
|
-
const files = sourceFiles();
|
|
437
|
-
const marker = markerFor(files);
|
|
438
476
|
ensureParent(target);
|
|
439
477
|
const state = inspectTarget(target);
|
|
478
|
+
const existingMarker = state === "missing" ? undefined : readMarker(target);
|
|
479
|
+
if (state !== "missing" && !existingMarker)
|
|
480
|
+
localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
|
|
481
|
+
const managedState = existingMarker ? regularManagedState(target, existingMarker) : undefined;
|
|
482
|
+
if (managedState === "modified" && !options.force) {
|
|
483
|
+
localError("CODEX_SKILL_MODIFIED", "Codex Skill files were modified; use --force to replace them");
|
|
484
|
+
}
|
|
485
|
+
const files = installedSkillFiles(options.environment ?? process.env);
|
|
486
|
+
const marker = markerFor(files);
|
|
440
487
|
if (state === "missing") {
|
|
441
488
|
const temporary = temporaryDirectory(dirname(target), CODEX_SKILL_NAME);
|
|
442
489
|
try {
|
|
@@ -449,13 +496,8 @@ export function installCodexSkill(options = {}) {
|
|
|
449
496
|
}
|
|
450
497
|
return { action: "install", status: "installed", skill: CODEX_SKILL_NAME, version: CODEX_SKILL_VERSION, target };
|
|
451
498
|
}
|
|
452
|
-
const existingMarker = readMarker(target);
|
|
453
499
|
if (!existingMarker)
|
|
454
500
|
localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
|
|
455
|
-
const managedState = regularManagedState(target, existingMarker);
|
|
456
|
-
if (managedState === "modified" && !options.force) {
|
|
457
|
-
localError("CODEX_SKILL_MODIFIED", "Codex Skill files were modified; use --force to replace them");
|
|
458
|
-
}
|
|
459
501
|
const sourceMatches = existingMarker.version === CODEX_SKILL_VERSION
|
|
460
502
|
&& CODEX_SKILL_FILES.every((file) => existingMarker.files[file] === marker.files[file]);
|
|
461
503
|
if (managedState === "complete" && sourceMatches) {
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { CONFIG_SCHEMA_VERSION } from "./contracts.js";
|
|
5
|
+
import { cliCommand } from "./release.js";
|
|
5
6
|
export const DEFAULT_PROFILE_NAME = "default";
|
|
6
7
|
export const DEFAULT_PROFILE = {
|
|
7
8
|
base_url: "",
|
|
@@ -110,7 +111,7 @@ function validateBaseUrl(value) {
|
|
|
110
111
|
}
|
|
111
112
|
export function validateProfile(value) {
|
|
112
113
|
const object = objectOrEmpty(value ?? {}, "Profile configuration");
|
|
113
|
-
const allowed = new Set(["base_url", "timeout_seconds", "language", "ca_bundle", "codex_skill_dir"]);
|
|
114
|
+
const allowed = new Set(["base_url", "timeout_seconds", "language", "ca_bundle", "codex_skill_dir", "username", "tenant_id"]);
|
|
114
115
|
for (const key of Object.keys(object)) {
|
|
115
116
|
if (!allowed.has(key))
|
|
116
117
|
throw new ConfigError(`Profile field ${key} is not supported`);
|
|
@@ -124,12 +125,22 @@ export function validateProfile(value) {
|
|
|
124
125
|
throw new ConfigError("Profile language must be zh or en");
|
|
125
126
|
const caBundle = optionalString(object, "ca_bundle");
|
|
126
127
|
const codexSkillDir = optionalString(object, "codex_skill_dir");
|
|
128
|
+
const username = optionalString(object, "username");
|
|
129
|
+
const tenantId = optionalString(object, "tenant_id");
|
|
130
|
+
if (username !== undefined && (username.length === 0 || /[\u0000-\u001f\u007f]/.test(username))) {
|
|
131
|
+
throw new ConfigError("Profile field username is invalid");
|
|
132
|
+
}
|
|
133
|
+
if (tenantId !== undefined && (tenantId.length === 0 || !/^[A-Za-z0-9_-]{1,128}$/.test(tenantId))) {
|
|
134
|
+
throw new ConfigError("Profile field tenant_id is invalid");
|
|
135
|
+
}
|
|
127
136
|
return {
|
|
128
137
|
base_url: validateBaseUrl(object.base_url),
|
|
129
138
|
timeout_seconds: timeout,
|
|
130
139
|
language,
|
|
131
140
|
...(caBundle ? { ca_bundle: caBundle } : {}),
|
|
132
141
|
...(codexSkillDir ? { codex_skill_dir: codexSkillDir } : {}),
|
|
142
|
+
...(username ? { username } : {}),
|
|
143
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
133
144
|
};
|
|
134
145
|
}
|
|
135
146
|
/**
|
|
@@ -158,13 +169,13 @@ export async function guardProfileConnectionChange(profileName, previous, next,
|
|
|
158
169
|
}
|
|
159
170
|
const credential = await store.get();
|
|
160
171
|
if (credential) {
|
|
161
|
-
throw new ProfileConnectionGuardError("AUTH_ALREADY_LOGGED_IN", `Profile ${profileName} has an active login on another Pilot origin; run
|
|
172
|
+
throw new ProfileConnectionGuardError("AUTH_ALREADY_LOGGED_IN", `Profile ${profileName} has an active login on another Pilot origin; run ${cliCommand("auth", "logout", "--profile", profileName)} or use a new profile`);
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
175
|
catch (error) {
|
|
165
176
|
if (error instanceof ProfileConnectionGuardError)
|
|
166
177
|
throw error;
|
|
167
|
-
throw new ProfileConnectionGuardError("CREDENTIAL_STORE_UNAVAILABLE", `Unable to verify existing credentials for profile ${profileName}; run
|
|
178
|
+
throw new ProfileConnectionGuardError("CREDENTIAL_STORE_UNAVAILABLE", `Unable to verify existing credentials for profile ${profileName}; run ${cliCommand("auth", "logout", "--profile", profileName)} before changing the Pilot origin`);
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
export function writeJsonAtomic(path, value) {
|
package/dist/contracts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { CLI_PACKAGE_NAME, CLI_VERSION, CLI_RUNNER, CLI_DOCTOR_COMMAND, cliCommand, } from "./release.js";
|
|
2
2
|
export const OUTPUT_SCHEMA_VERSION = "1.0";
|
|
3
3
|
export const CONFIG_SCHEMA_VERSION = "1.0";
|
|
4
4
|
export const MIN_NODE_MAJOR = 22;
|
|
@@ -11,5 +11,7 @@ export const P1_CAPABILITY_SCOPES = [
|
|
|
11
11
|
"metrics:read",
|
|
12
12
|
"alarms:read",
|
|
13
13
|
"incidents:analyze",
|
|
14
|
+
"a2a:delegate:read",
|
|
15
|
+
"a2a:task:read",
|
|
14
16
|
"inspections:run-readonly",
|
|
15
17
|
];
|