@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/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: "@luckypiggy/zhihu-ai-cli",
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 ready = info.status === "ready" && info.auth?.configured === true;
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 service = authService(flags);
339
- if (action === "login")
340
- return service.login();
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", "auth-mode", "timeout", "language", "ca-file", "codex-skill-dir",
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
- return { ...result };
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>] [--auth-mode browser] [--timeout N] [--language zh-CN|en] [--ca-file <absolute-file>] [--codex-skill-dir <absolute-path>] [--skip-login] [--skip-codex] [--force]",
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; install also accepts ZHIHU_PROFILE, ZHIHU_BASE_URL, ZHIHU_AUTH_MODE=browser, ZHIHU_CA_BUNDLE, and ZHIHU_TIMEOUT; write operations, MCP, customer URL delivery, and public npm publication remain unavailable",
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
- return { data: await installCommand(parsed.flags, parsed.json), exit_code: 0 };
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");
@@ -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: "@luckypiggy/zhihu-ai-cli",
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 !== "@luckypiggy/zhihu-ai-cli"
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 zhihu auth logout --profile ${profileName} or use a new profile`);
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 zhihu auth logout --profile ${profileName} before changing the Pilot origin`);
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 const CLI_VERSION = "0.1.0-test.1";
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
  ];