@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.
package/dist/http.js CHANGED
@@ -4,6 +4,7 @@ import { request as httpRequest } from "node:http";
4
4
  import { request as httpsRequest } from "node:https";
5
5
  import { isAbsolute } from "node:path";
6
6
  import { CLI_VERSION, P1_CAPABILITY_SCOPES } from "./contracts.js";
7
+ import { cliCommand } from "./release.js";
7
8
  const INTEGRATION_PATH = "/api/zhihu/integrations/v1";
8
9
  const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
9
10
  const MAX_REQUEST_BYTES = 128 * 1024;
@@ -18,24 +19,85 @@ const INFO_CAPABILITIES = [
18
19
  "inspections",
19
20
  "metrics",
20
21
  "alarms",
22
+ "a2a",
21
23
  ];
22
- function semver(value) {
23
- if (typeof value !== "string" || !/^\d+\.\d+\.\d+$/.test(value))
24
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
25
+ function parseNumericCore(value) {
26
+ const parsed = Number(value);
27
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
28
+ }
29
+ /** Parse SemVer without accepting loose prefixes, missing parts or bad zeros. */
30
+ export function parseSemVer(value) {
31
+ if (typeof value !== "string")
24
32
  return null;
25
- const parts = value.split(".").map(Number);
26
- if (parts.some((part) => !Number.isSafeInteger(part) || part < 0))
33
+ const match = SEMVER_PATTERN.exec(value);
34
+ if (!match)
35
+ return null;
36
+ const major = parseNumericCore(match[1]);
37
+ const minor = parseNumericCore(match[2]);
38
+ const patch = parseNumericCore(match[3]);
39
+ if (major === null || minor === null || patch === null)
40
+ return null;
41
+ const prerelease = match[4]?.split(".") ?? [];
42
+ if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) {
27
43
  return null;
28
- return parts;
29
- }
30
- function compareSemver(left, right) {
31
- for (let index = 0; index < 3; index += 1) {
32
- if (left[index] !== right[index])
33
- return left[index] - right[index];
34
44
  }
35
- return 0;
45
+ return {
46
+ major,
47
+ minor,
48
+ patch,
49
+ prerelease,
50
+ build: match[5]?.split(".") ?? [],
51
+ };
36
52
  }
53
+ function asSemVer(value) {
54
+ return typeof value === "string" ? parseSemVer(value) : value;
55
+ }
56
+ function comparePrereleaseIdentifier(left, right) {
57
+ const leftNumeric = /^\d+$/.test(left);
58
+ const rightNumeric = /^\d+$/.test(right);
59
+ if (leftNumeric && rightNumeric) {
60
+ if (left.length !== right.length)
61
+ return left.length < right.length ? -1 : 1;
62
+ return left === right ? 0 : left < right ? -1 : 1;
63
+ }
64
+ if (leftNumeric !== rightNumeric)
65
+ return leftNumeric ? -1 : 1;
66
+ return left === right ? 0 : left < right ? -1 : 1;
67
+ }
68
+ /** Compare SemVer precedence; build metadata does not affect the result. */
69
+ export function compareSemVer(left, right) {
70
+ const parsedLeft = asSemVer(left);
71
+ const parsedRight = asSemVer(right);
72
+ if (!parsedLeft || !parsedRight)
73
+ throw new TypeError("Invalid SemVer value");
74
+ for (const key of ["major", "minor", "patch"]) {
75
+ if (parsedLeft[key] !== parsedRight[key])
76
+ return parsedLeft[key] < parsedRight[key] ? -1 : 1;
77
+ }
78
+ const leftPrerelease = parsedLeft.prerelease;
79
+ const rightPrerelease = parsedRight.prerelease;
80
+ if (leftPrerelease.length === 0 && rightPrerelease.length === 0)
81
+ return 0;
82
+ if (leftPrerelease.length === 0)
83
+ return 1;
84
+ if (rightPrerelease.length === 0)
85
+ return -1;
86
+ const count = Math.min(leftPrerelease.length, rightPrerelease.length);
87
+ for (let index = 0; index < count; index += 1) {
88
+ const comparison = comparePrereleaseIdentifier(leftPrerelease[index], rightPrerelease[index]);
89
+ if (comparison !== 0)
90
+ return comparison;
91
+ }
92
+ if (leftPrerelease.length === rightPrerelease.length)
93
+ return 0;
94
+ return leftPrerelease.length < rightPrerelease.length ? -1 : 1;
95
+ }
96
+ // Preserve the spelling used by older internal tests while exposing the
97
+ // standards-correct SemVer comparator to release validation tooling.
98
+ export const compareSemver = compareSemVer;
37
99
  /** Validate the exact, unauthenticated integration discovery contract. */
38
- export function validateIntegrationInfo(value, requireReady = false) {
100
+ export function validateIntegrationInfo(value, requireReady = false, cliVersion = CLI_VERSION) {
39
101
  const object = validateResponseObject(value, "Info");
40
102
  const expectedKeys = new Set(["service", "protocol_version", "minimum_cli_version", "status", "capabilities", "auth"]);
41
103
  if (Object.keys(object).length !== expectedKeys.size || Object.keys(object).some((key) => !expectedKeys.has(key))) {
@@ -44,9 +106,9 @@ export function validateIntegrationInfo(value, requireReady = false) {
44
106
  if (object.service !== "zhihu-pilot" || object.protocol_version !== "1.0") {
45
107
  throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot does not support this CLI version");
46
108
  }
47
- const minimum = semver(object.minimum_cli_version);
48
- const current = semver(CLI_VERSION);
49
- if (!minimum || !current || compareSemver(minimum, current) > 0) {
109
+ const minimum = parseSemVer(object.minimum_cli_version);
110
+ const current = parseSemVer(cliVersion);
111
+ if (!minimum || !current || compareSemVer(minimum, current) > 0) {
50
112
  throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot requires a newer CLI version");
51
113
  }
52
114
  if (object.status !== "ready" && object.status !== "configuration_required") {
@@ -75,8 +137,14 @@ export function validateIntegrationInfo(value, requireReady = false) {
75
137
  if (Object.keys(auth).length !== authKeys.size || Object.keys(auth).some((key) => !authKeys.has(key))) {
76
138
  throw new HttpError("PROTOCOL_ERROR", "Info auth is invalid");
77
139
  }
78
- if (auth.mode !== "pilot_oauth_bff" || typeof auth.configured !== "boolean"
79
- || !Array.isArray(auth.pkce_methods) || auth.pkce_methods.length !== 1 || auth.pkce_methods[0] !== "S256") {
140
+ const terminalAuth = auth.mode === "pilot_terminal_password"
141
+ && Array.isArray(auth.pkce_methods)
142
+ && auth.pkce_methods.length === 0;
143
+ const legacyAuth = auth.mode === "pilot_oauth_bff"
144
+ && Array.isArray(auth.pkce_methods)
145
+ && auth.pkce_methods.length === 1
146
+ && auth.pkce_methods[0] === "S256";
147
+ if (typeof auth.configured !== "boolean" || (!terminalAuth && !legacyAuth)) {
80
148
  throw new HttpError("PROTOCOL_ERROR", "Info auth is invalid");
81
149
  }
82
150
  const baseCapabilities = INFO_CAPABILITIES.slice(0, 4);
@@ -86,14 +154,21 @@ export function validateIntegrationInfo(value, requireReady = false) {
86
154
  const hasAlarms = seen.has("alarms");
87
155
  const hasIncidents = seen.has("incidents");
88
156
  const hasInspections = seen.has("inspections");
157
+ const hasA2A = seen.has("a2a");
158
+ if (hasA2A && capabilities[capabilities.length - 1] !== "a2a") {
159
+ throw new HttpError("PROTOCOL_ERROR", "Info capabilities are invalid");
160
+ }
161
+ const coreCapabilities = hasA2A ? capabilities.slice(0, -1) : capabilities;
162
+ const coreSet = new Set(coreCapabilities);
89
163
  // The backend advertises a monotonic capability set. The base four are
90
164
  // always present; metrics and alarms may be enabled independently. The
91
165
  // incident/inspection pair is available only when both data sources exist,
92
- // and the pair itself is all-or-nothing.
93
- const optionalCombinationValid = baseMatch && (capabilities.length === 4
94
- || (capabilities.length === 5 && hasMetrics && !hasAlarms && !hasIncidents && !hasInspections)
95
- || (capabilities.length === 5 && hasAlarms && !hasMetrics && !hasIncidents && !hasInspections)
96
- || (capabilities.length === 8 && hasMetrics && hasAlarms && hasIncidents && hasInspections));
166
+ // and the pair itself is all-or-nothing. A2A is a final, independent
167
+ // marker so older discovery suffix ordering remains valid.
168
+ const optionalCombinationValid = baseMatch && coreSet.has("info") && (coreCapabilities.length === 4
169
+ || (coreCapabilities.length === 5 && hasMetrics && !hasAlarms && !hasIncidents && !hasInspections)
170
+ || (coreCapabilities.length === 5 && hasAlarms && !hasMetrics && !hasIncidents && !hasInspections)
171
+ || (coreCapabilities.length === 8 && hasMetrics && hasAlarms && hasIncidents && hasInspections));
97
172
  if (object.status === "ready" && (auth.configured !== true || !optionalCombinationValid)) {
98
173
  throw new HttpError("PROTOCOL_ERROR", "Info readiness is inconsistent");
99
174
  }
@@ -107,6 +182,9 @@ export function validateIntegrationInfo(value, requireReady = false) {
107
182
  }
108
183
  throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot is not ready for CLI installation");
109
184
  }
185
+ if (requireReady && !terminalAuth) {
186
+ throw new HttpError("TERMINAL_LOGIN_REQUIRED", "This Pilot still exposes browser OAuth. Upgrade or enable terminal password login before installing this CLI.");
187
+ }
110
188
  return {
111
189
  service: object.service,
112
190
  protocol_version: object.protocol_version,
@@ -116,7 +194,7 @@ export function validateIntegrationInfo(value, requireReady = false) {
116
194
  auth: {
117
195
  mode: auth.mode,
118
196
  configured: auth.configured,
119
- pkce_methods: ["S256"],
197
+ pkce_methods: [...auth.pkce_methods],
120
198
  },
121
199
  };
122
200
  }
@@ -142,9 +220,17 @@ const RETRYABLE_ERROR_CODES = new Set([
142
220
  "CONVERSATION_CAPACITY_REACHED",
143
221
  "DATA_SOURCE_UNAVAILABLE",
144
222
  "TIMEOUT",
223
+ "A2A_TIMEOUT",
224
+ "A2A_RUNTIME_ERROR",
225
+ "A2A_TASK_START_FAILED",
226
+ "A2A_DUPLICATE",
145
227
  ]);
146
228
  const NON_RETRYABLE_ERROR_CODES = new Set([
147
229
  "AUTH_RELOGIN_REQUIRED",
230
+ "AUTH_LOGIN_FAILED",
231
+ "AUTH_ACCOUNT_LOCKED",
232
+ "AUTH_CAPTCHA_REQUIRED",
233
+ "TERMINAL_LOGIN_REQUIRED",
148
234
  "CAPABILITY_SCOPE_REQUIRED",
149
235
  "CAPABILITY_NOT_AVAILABLE",
150
236
  "FORBIDDEN",
@@ -160,6 +246,14 @@ const NON_RETRYABLE_ERROR_CODES = new Set([
160
246
  "UPSTREAM_RESPONSE_TOO_LARGE",
161
247
  "USER_CANCELLED",
162
248
  "PROTOCOL_ERROR",
249
+ "A2A_DISABLED",
250
+ "A2A_CAPABILITY_NOT_FOUND",
251
+ "A2A_CAPABILITY_DISABLED",
252
+ "A2A_TASK_NOT_FOUND",
253
+ "A2A_FORBIDDEN",
254
+ "A2A_CANCELLED",
255
+ "A2A_EMPTY_RESPONSE",
256
+ "A2A_EXECUTION_INDETERMINATE",
163
257
  ]);
164
258
  function retryableForCode(code, fallback) {
165
259
  if (RETRYABLE_ERROR_CODES.has(code))
@@ -173,6 +267,8 @@ function stableCode(status) {
173
267
  return { code: "AUTH_RELOGIN_REQUIRED", retryable: false };
174
268
  if (status === 403)
175
269
  return { code: "FORBIDDEN", retryable: false };
270
+ if (status === 423)
271
+ return { code: "AUTH_ACCOUNT_LOCKED", retryable: false };
176
272
  if (status === 429)
177
273
  return { code: "RATE_LIMITED", retryable: true };
178
274
  if (status >= 500)
@@ -182,7 +278,11 @@ function stableCode(status) {
182
278
  function responseError(status, body) {
183
279
  const fallback = stableCode(status);
184
280
  const messages = {
185
- AUTH_RELOGIN_REQUIRED: "Your Zhihu session has expired. Run `zhihu auth login`.",
281
+ AUTH_RELOGIN_REQUIRED: `Your Zhihu session has expired. Run \`${cliCommand("auth", "login")}\`.`,
282
+ AUTH_LOGIN_FAILED: "Zhihu login was rejected. Check the username and password and try again.",
283
+ AUTH_ACCOUNT_LOCKED: "Zhihu login is temporarily locked. Try again later or contact an administrator.",
284
+ AUTH_CAPTCHA_REQUIRED: "Zhihu login requires captcha verification. Complete the challenge and try again.",
285
+ TERMINAL_LOGIN_REQUIRED: "This Pilot requires terminal password login; browser OAuth is not supported by this CLI.",
186
286
  CAPABILITY_NOT_AVAILABLE: "This capability is not available on the configured Pilot.",
187
287
  CAPABILITY_SCOPE_REQUIRED: "This CLI session is not allowed to use this capability.",
188
288
  FORBIDDEN: "The server denied this operation.",
@@ -207,6 +307,18 @@ function responseError(status, body) {
207
307
  ASSET_METRIC_IDENTITY_UNAVAILABLE: "The asset has no monitorable IP address for metric lookup.",
208
308
  TIMEOUT: "The request timed out.",
209
309
  USER_CANCELLED: "The request was cancelled.",
310
+ A2A_DISABLED: "A2A routing is not enabled on the configured Pilot.",
311
+ A2A_CAPABILITY_NOT_FOUND: "The requested A2A capability was not found.",
312
+ A2A_CAPABILITY_DISABLED: "The requested A2A capability is not enabled.",
313
+ A2A_TASK_NOT_FOUND: "The requested A2A task was not found.",
314
+ A2A_FORBIDDEN: "You are not allowed to access this A2A task.",
315
+ A2A_TIMEOUT: "The A2A delegation timed out.",
316
+ A2A_CANCELLED: "The A2A delegation was cancelled.",
317
+ A2A_RUNTIME_ERROR: "The A2A target employee failed to complete the request.",
318
+ A2A_EMPTY_RESPONSE: "The A2A target employee returned no usable result.",
319
+ A2A_EXECUTION_INDETERMINATE: "The A2A execution state is unknown; verify the task before retrying.",
320
+ A2A_DUPLICATE: "The A2A request is already running.",
321
+ A2A_TASK_START_FAILED: "The A2A task could not be started.",
210
322
  };
211
323
  try {
212
324
  const parsed = JSON.parse(body);
@@ -323,6 +435,163 @@ function validateEmployeeList(value) {
323
435
  };
324
436
  });
325
437
  }
438
+ const A2A_CAPABILITY_IDS = new Set([
439
+ "resource_query",
440
+ "incident_analysis",
441
+ "inspection_summary",
442
+ "knowledge_qa",
443
+ ]);
444
+ const A2A_WRITE_IDS = new Set(["asset_onboarding", "asset_discovery"]);
445
+ const A2A_TASK_STATUSES = new Set([
446
+ "running",
447
+ "completed",
448
+ "failed",
449
+ "timed_out",
450
+ "cancelled",
451
+ ]);
452
+ function validateA2AText(value, field, maxLength = 4096) {
453
+ if (typeof value !== "string"
454
+ || value.length === 0
455
+ || value.length > maxLength
456
+ || /[\u0000-\u001f\u007f]/.test(value)) {
457
+ throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
458
+ }
459
+ return value;
460
+ }
461
+ function validateA2AResultText(value, field) {
462
+ if (typeof value !== "string"
463
+ || value.length > 64 * 1024
464
+ // Natural-language conclusions may be multiline. Keep tabs/newlines
465
+ // while rejecting NUL, other C0 controls and DEL/escape characters that
466
+ // could corrupt a terminal or a JSON consumer's display.
467
+ || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) {
468
+ throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
469
+ }
470
+ return value;
471
+ }
472
+ function validateA2ACapabilityRecord(value, index) {
473
+ const item = validateResponseObject(value, `A2A capability ${index}`);
474
+ const expected = new Set(["id", "name", "risk_level", "enabled"]);
475
+ if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
476
+ throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
477
+ }
478
+ if (typeof item.id !== "string" || (!A2A_CAPABILITY_IDS.has(item.id) && !A2A_WRITE_IDS.has(item.id))) {
479
+ throw new HttpError("PROTOCOL_ERROR", "A2A capability id is invalid");
480
+ }
481
+ if (typeof item.risk_level !== "string" || !["readonly", "write"].includes(item.risk_level)) {
482
+ throw new HttpError("PROTOCOL_ERROR", "A2A capability risk is invalid");
483
+ }
484
+ if (typeof item.enabled !== "boolean")
485
+ throw new HttpError("PROTOCOL_ERROR", "A2A capability state is invalid");
486
+ if ((A2A_WRITE_IDS.has(item.id) && (item.risk_level !== "write" || item.enabled))
487
+ || (!A2A_WRITE_IDS.has(item.id) && (item.risk_level !== "readonly" || !item.enabled))) {
488
+ throw new HttpError("PROTOCOL_ERROR", "A2A capability state is invalid");
489
+ }
490
+ return {
491
+ id: item.id,
492
+ name: validateA2AText(item.name, "A2A capability name"),
493
+ risk_level: item.risk_level,
494
+ enabled: item.enabled,
495
+ };
496
+ }
497
+ function validateA2ACapabilitiesData(value) {
498
+ const data = validateResponseObject(value, "A2A capabilities data");
499
+ const expected = new Set(["capabilities", "disabled_write"]);
500
+ if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
501
+ throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
502
+ }
503
+ if (!Array.isArray(data.capabilities) || data.capabilities.length > A2A_CAPABILITY_IDS.size) {
504
+ throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
505
+ }
506
+ if (!Array.isArray(data.disabled_write) || data.disabled_write.length > A2A_WRITE_IDS.size) {
507
+ throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
508
+ }
509
+ const capabilities = data.capabilities.map((item, index) => validateA2ACapabilityRecord(item, index));
510
+ const disabledWrite = data.disabled_write.map((item, index) => validateA2ACapabilityRecord(item, index));
511
+ const seen = new Set();
512
+ for (const item of capabilities) {
513
+ if (item.risk_level !== "readonly" || seen.has(item.id))
514
+ throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
515
+ seen.add(item.id);
516
+ }
517
+ const writeSeen = new Set();
518
+ for (const item of disabledWrite) {
519
+ if (item.risk_level !== "write" || item.enabled || writeSeen.has(item.id))
520
+ throw new HttpError("PROTOCOL_ERROR", "A2A capabilities response is invalid");
521
+ writeSeen.add(item.id);
522
+ }
523
+ return { capabilities, disabled_write: disabledWrite };
524
+ }
525
+ function validateA2AEnvelope(value, label) {
526
+ const envelope = validateResponseObject(value, label);
527
+ const expected = new Set(["ok", "data", "meta"]);
528
+ if (Object.keys(envelope).length !== expected.size || Object.keys(envelope).some((key) => !expected.has(key)) || envelope.ok !== true) {
529
+ throw new HttpError("PROTOCOL_ERROR", `${label} response is invalid`);
530
+ }
531
+ const meta = validateResponseObject(envelope.meta, `${label} metadata`);
532
+ if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
533
+ || typeof meta.capability !== "string" || !/^a2a\.[a-z.]+$/.test(meta.capability)
534
+ || meta.schema_version !== "1.0"
535
+ || validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
536
+ throw new HttpError("PROTOCOL_ERROR", `${label} metadata is invalid`);
537
+ }
538
+ return envelope;
539
+ }
540
+ function validateA2ATaskId(value) {
541
+ return validateA2AText(value, "A2A task id", 128).match(/^[A-Za-z0-9:_-]+$/)
542
+ ? value
543
+ : (() => { throw new HttpError("PROTOCOL_ERROR", "A2A task id is invalid"); })();
544
+ }
545
+ function validateA2AResult(value) {
546
+ const data = validateResponseObject(value, "A2A delegation data");
547
+ const expected = new Set(["accepted", "capability", "target", "task", "trace", "status", "result"]);
548
+ if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
549
+ throw new HttpError("PROTOCOL_ERROR", "A2A delegation response is invalid");
550
+ }
551
+ if (data.accepted !== true || typeof data.capability !== "string" || !A2A_CAPABILITY_IDS.has(data.capability)) {
552
+ throw new HttpError("PROTOCOL_ERROR", "A2A delegation response is invalid");
553
+ }
554
+ if (data.status !== "success")
555
+ throw new HttpError("PROTOCOL_ERROR", "A2A delegation status is invalid");
556
+ return {
557
+ accepted: true,
558
+ capability: data.capability,
559
+ target: validateIdentityId(data.target, "A2A target"),
560
+ task: validateA2ATaskId(data.task),
561
+ trace: validateA2AText(data.trace, "A2A trace", 256),
562
+ status: "success",
563
+ result: validateA2AResultText(data.result, "A2A result"),
564
+ };
565
+ }
566
+ function validateA2ATask(value) {
567
+ const data = validateResponseObject(value, "A2A task data");
568
+ const expected = new Set(["task", "capability", "target", "trace", "status", "result", "created_at", "finished_at"]);
569
+ if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
570
+ throw new HttpError("PROTOCOL_ERROR", "A2A task response is invalid");
571
+ }
572
+ if (typeof data.capability !== "string" || !A2A_CAPABILITY_IDS.has(data.capability)) {
573
+ throw new HttpError("PROTOCOL_ERROR", "A2A task capability is invalid");
574
+ }
575
+ if (typeof data.status !== "string" || !A2A_TASK_STATUSES.has(data.status)) {
576
+ throw new HttpError("PROTOCOL_ERROR", "A2A task status is invalid");
577
+ }
578
+ if (typeof data.created_at !== "number" || !Number.isFinite(data.created_at) || data.created_at <= 0
579
+ || (data.finished_at !== null && (typeof data.finished_at !== "number" || !Number.isFinite(data.finished_at) || data.finished_at <= 0))) {
580
+ throw new HttpError("PROTOCOL_ERROR", "A2A task timestamps are invalid");
581
+ }
582
+ if (data.status === "completed" && data.finished_at === null)
583
+ throw new HttpError("PROTOCOL_ERROR", "A2A task timestamps are invalid");
584
+ return {
585
+ task: validateA2ATaskId(data.task),
586
+ capability: data.capability,
587
+ target: validateIdentityId(data.target, "A2A target"),
588
+ trace: validateA2AText(data.trace, "A2A trace", 256),
589
+ status: data.status,
590
+ result: validateA2AResultText(data.result, "A2A result"),
591
+ created_at: data.created_at,
592
+ finished_at: data.finished_at,
593
+ };
594
+ }
326
595
  const ASSET_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
327
596
  const ASSET_IP_PATTERN = /^[0-9a-fA-F:.]{1,128}$/;
328
597
  function validateNullableAssetText(value, field) {
@@ -1332,7 +1601,8 @@ export class IntegrationHttpClient {
1332
1601
  headers.authorization = `Bearer ${accessToken}`;
1333
1602
  const requestConfig = requestOptions(url, this.profile, headers);
1334
1603
  requestConfig.method = method;
1335
- const raw = await this.send(requestConfig, encoded);
1604
+ const timeoutSeconds = options.timeoutSeconds ?? this.profile.timeout_seconds;
1605
+ const raw = await this.send(requestConfig, encoded, options.signal, timeoutSeconds);
1336
1606
  if (raw.status >= 300 && raw.status < 400) {
1337
1607
  throw new HttpError("REDIRECT_NOT_ALLOWED", "Server redirects are not allowed", { status: raw.status });
1338
1608
  }
@@ -1347,51 +1617,130 @@ export class IntegrationHttpClient {
1347
1617
  throw new HttpError("PROTOCOL_ERROR", "Server response is not valid JSON", { status: raw.status });
1348
1618
  }
1349
1619
  }
1350
- send(options, body) {
1620
+ send(options, body, signal, timeoutSeconds) {
1351
1621
  return new Promise((resolve, reject) => {
1352
1622
  const protocol = options.protocol === "https:" ? httpsRequest : httpRequest;
1353
1623
  let request;
1624
+ let settled = false;
1625
+ let abortListener;
1626
+ const cleanup = () => {
1627
+ if (abortListener && signal)
1628
+ signal.removeEventListener("abort", abortListener);
1629
+ abortListener = undefined;
1630
+ };
1631
+ const succeed = (value) => {
1632
+ if (settled)
1633
+ return;
1634
+ settled = true;
1635
+ cleanup();
1636
+ resolve(value);
1637
+ };
1638
+ const fail = (error) => {
1639
+ if (settled)
1640
+ return;
1641
+ settled = true;
1642
+ cleanup();
1643
+ request?.destroy();
1644
+ reject(error);
1645
+ };
1354
1646
  try {
1355
1647
  request = protocol(options, async (response) => {
1356
1648
  try {
1357
1649
  const responseBody = await collectResponse(response, this.maxResponseBytes);
1358
- resolve({ status: response.statusCode ?? 0, headers: response.headers, body: responseBody });
1650
+ succeed({ status: response.statusCode ?? 0, headers: response.headers, body: responseBody });
1359
1651
  }
1360
1652
  catch (error) {
1361
- reject(error instanceof HttpError ? error : new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
1653
+ fail(error instanceof HttpError ? error : new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
1362
1654
  }
1363
1655
  });
1364
1656
  }
1365
1657
  catch {
1366
- reject(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
1658
+ fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
1367
1659
  return;
1368
1660
  }
1369
- const timeout = Math.max(1, this.profile.timeout_seconds) * 1000;
1661
+ const timeout = Math.max(1, timeoutSeconds ?? this.profile.timeout_seconds) * 1000;
1370
1662
  request.setTimeout(timeout, () => {
1371
- request.destroy();
1372
- reject(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
1663
+ fail(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
1373
1664
  });
1374
1665
  request.once("error", (error) => {
1375
1666
  if (error.code === "ETIMEDOUT")
1376
- reject(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
1667
+ fail(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
1377
1668
  else
1378
- reject(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
1669
+ fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
1379
1670
  });
1671
+ abortListener = () => fail(new HttpError("USER_CANCELLED", "The request was cancelled."));
1672
+ if (signal?.aborted) {
1673
+ abortListener();
1674
+ return;
1675
+ }
1676
+ signal?.addEventListener("abort", abortListener, { once: true });
1380
1677
  if (body)
1381
1678
  request.write(body);
1382
1679
  request.end();
1383
1680
  });
1384
1681
  }
1385
1682
  async info(options = {}) {
1386
- const value = await this.requestJson("GET", "/info");
1683
+ const value = await this.requestJson("GET", "/info", undefined, undefined, { signal: options.signal });
1387
1684
  if (options.requireReady)
1388
1685
  return validateIntegrationInfo(value, true);
1389
1686
  // Keep the low-level discovery call backwards compatible for existing
1390
1687
  // clients/tests; installation and doctor opt into the strict contract.
1391
1688
  return validateResponseObject(value, "Info");
1392
1689
  }
1393
- async capabilities(accessToken) {
1394
- const envelope = validateResponseObject(await this.requestJson("GET", "/capabilities", undefined, accessToken), "Capabilities");
1690
+ /**
1691
+ * Authenticate through the Pilot terminal-login contract. Credentials are
1692
+ * accepted for this call only and are sent in the JSON body; they are never
1693
+ * placed in a URL, header, error, or log value.
1694
+ */
1695
+ async terminalLogin(username, password, tenantId, captchaVerification, options = {}) {
1696
+ if (typeof username !== "string" || username.length === 0 || username.length > 256 || /[\u0000-\u001f\u007f]/.test(username)) {
1697
+ throw new HttpError("INVALID_ARGUMENT", "Username is invalid");
1698
+ }
1699
+ if (typeof password !== "string" || password.length === 0 || password.length > 4096 || /[\u0000-\u001f\u007f]/.test(password)) {
1700
+ throw new HttpError("INVALID_ARGUMENT", "Password is invalid");
1701
+ }
1702
+ if (tenantId !== undefined && (typeof tenantId !== "string" || tenantId.length === 0 || tenantId.length > 128 || !/^[A-Za-z0-9_-]+$/.test(tenantId))) {
1703
+ throw new HttpError("INVALID_ARGUMENT", "Tenant id is invalid");
1704
+ }
1705
+ if (captchaVerification !== undefined && (typeof captchaVerification !== "string" || captchaVerification.length === 0 || captchaVerification.length > 4096 || /[\u0000-\u001f\u007f]/.test(captchaVerification))) {
1706
+ throw new HttpError("INVALID_ARGUMENT", "Captcha verification is invalid");
1707
+ }
1708
+ const body = {
1709
+ username,
1710
+ password,
1711
+ ...(tenantId === undefined ? {} : { tenant_id: tenantId }),
1712
+ ...(captchaVerification === undefined ? {} : { captcha_verification: captchaVerification }),
1713
+ };
1714
+ let result;
1715
+ try {
1716
+ result = validateResponseObject(await this.requestJson("POST", "/auth/login", body, undefined, { signal: options.signal }), "Terminal login");
1717
+ }
1718
+ catch (error) {
1719
+ if (!(error instanceof HttpError))
1720
+ throw error;
1721
+ // Keep terminal-login failures stable and non-sensitive. The generic
1722
+ // response parser intentionally ignores provider messages.
1723
+ if (error.code === "PROTOCOL_ERROR" && error.status === 400) {
1724
+ throw new HttpError("AUTH_LOGIN_FAILED", "Zhihu login was rejected. Check the username and password and try again.", { status: error.status });
1725
+ }
1726
+ if (error.code === "AUTH_RELOGIN_REQUIRED" && error.status === 401) {
1727
+ throw new HttpError("AUTH_LOGIN_FAILED", "Zhihu login was rejected. Check the username and password and try again.", { status: error.status });
1728
+ }
1729
+ throw error;
1730
+ }
1731
+ const expectedKeys = new Set(["access_token", "refresh_credential", "expires_in"]);
1732
+ const keys = Object.keys(result);
1733
+ if (keys.length !== expectedKeys.size || keys.some((key) => !expectedKeys.has(key))) {
1734
+ throw new HttpError("PROTOCOL_ERROR", "Terminal login response is invalid");
1735
+ }
1736
+ return {
1737
+ access_token: validateCredentialField(result.access_token, "access_token"),
1738
+ refresh_credential: validateCredentialField(result.refresh_credential, "refresh_credential"),
1739
+ expires_in: validateExpires(result.expires_in),
1740
+ };
1741
+ }
1742
+ async capabilities(accessToken, options = {}) {
1743
+ const envelope = validateResponseObject(await this.requestJson("GET", "/capabilities", undefined, accessToken, { signal: options.signal }), "Capabilities");
1395
1744
  if (envelope.ok !== true) {
1396
1745
  throw new HttpError("PROTOCOL_ERROR", "Capabilities response is invalid");
1397
1746
  }
@@ -1412,6 +1761,33 @@ export class IntegrationHttpClient {
1412
1761
  }
1413
1762
  return { user_id: userId, tenant_id: tenantId, scopes, capabilities };
1414
1763
  }
1764
+ async a2aCapabilities(accessToken, options = {}) {
1765
+ const envelope = validateA2AEnvelope(await this.requestJson("GET", "/a2a/capabilities", undefined, accessToken, { signal: options.signal }), "A2A capabilities");
1766
+ return validateA2ACapabilitiesData(envelope.data);
1767
+ }
1768
+ async a2aDelegate(capabilityId, question, accessToken, options = {}) {
1769
+ if (!A2A_CAPABILITY_IDS.has(capabilityId)) {
1770
+ throw new HttpError("INVALID_ARGUMENT", "A2A capability is invalid");
1771
+ }
1772
+ if (typeof question !== "string" || question.trim().length === 0 || Array.from(question).length > 8000
1773
+ || Buffer.byteLength(question, "utf8") > 16 * 1024
1774
+ || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(question)) {
1775
+ throw new HttpError("INVALID_ARGUMENT", "A2A question is invalid");
1776
+ }
1777
+ const envelope = validateA2AEnvelope(await this.requestJson("POST", "/a2a/delegations", {
1778
+ capability_id: capabilityId,
1779
+ question: question.trim(),
1780
+ }, accessToken, {
1781
+ signal: options.signal,
1782
+ timeoutSeconds: Math.max(200, this.profile.timeout_seconds),
1783
+ }), "A2A delegation");
1784
+ return validateA2AResult(envelope.data);
1785
+ }
1786
+ async a2aTask(taskId, accessToken, options = {}) {
1787
+ const normalized = validateA2ATaskId(taskId);
1788
+ const envelope = validateA2AEnvelope(await this.requestJson("GET", `/a2a/tasks/${encodeURIComponent(normalized)}`, undefined, accessToken, { signal: options.signal }), "A2A task");
1789
+ return validateA2ATask(envelope.data);
1790
+ }
1415
1791
  async employees(accessToken) {
1416
1792
  const envelope = validateResponseObject(await this.requestJson("GET", "/employees", undefined, accessToken), "Employees");
1417
1793
  if (envelope.ok !== true) {
@@ -1828,36 +2204,6 @@ export class IntegrationHttpClient {
1828
2204
  async chat(employeeId, message, conversationId, accessToken, options = {}) {
1829
2205
  return this.employeeChat(employeeId, message, conversationId, accessToken, options);
1830
2206
  }
1831
- async start(codeChallenge) {
1832
- const result = validateResponseObject(await this.requestJson("POST", "/auth/start", {
1833
- code_challenge: codeChallenge,
1834
- code_challenge_method: "S256",
1835
- }), "Auth start");
1836
- return {
1837
- transaction_id: validateCredentialField(result.transaction_id, "transaction_id"),
1838
- polling_secret: validateCredentialField(result.polling_secret, "polling_secret"),
1839
- authorization_url: validateCredentialField(result.authorization_url, "authorization_url"),
1840
- expires_in: validateExpires(result.expires_in),
1841
- };
1842
- }
1843
- async poll(transactionId, pollingSecret, codeVerifier) {
1844
- const result = validateResponseObject(await this.requestJson("POST", "/auth/poll", {
1845
- transaction_id: transactionId,
1846
- polling_secret: pollingSecret,
1847
- code_verifier: codeVerifier,
1848
- }), "Auth poll");
1849
- const status = validateCredentialField(result.status, "status");
1850
- const output = { status };
1851
- if (typeof result.error_code === "string")
1852
- output.error_code = result.error_code;
1853
- if (result.access_token !== undefined)
1854
- output.access_token = validateCredentialField(result.access_token, "access_token");
1855
- if (result.refresh_credential !== undefined)
1856
- output.refresh_credential = validateCredentialField(result.refresh_credential, "refresh_credential");
1857
- if (result.expires_in !== undefined)
1858
- output.expires_in = validateExpires(result.expires_in);
1859
- return output;
1860
- }
1861
2207
  async refresh(refreshCredential) {
1862
2208
  const result = validateResponseObject(await this.requestJson("POST", "/auth/refresh", {
1863
2209
  refresh_credential: refreshCredential,