@lobehub/cli 0.0.56 → 0.0.58

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/index.js CHANGED
@@ -13,7 +13,7 @@ import { finished, pipeline } from "node:stream/promises";
13
13
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark, pipeline as pipeline$1 } from "node:stream";
14
14
  import readline, { createInterface } from "node:readline";
15
15
  import { EventEmitter, addAbortListener, on, once, setMaxListeners } from "node:events";
16
- import fs$1, { access, copyFile, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink as unlink$1, writeFile } from "node:fs/promises";
16
+ import fs$1, { access, copyFile, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink as unlink$1, writeFile } from "node:fs/promises";
17
17
  import crypto$1, { X509Certificate, createHash, randomBytes, randomUUID, sign } from "node:crypto";
18
18
  import { text } from "node:stream/consumers";
19
19
  import { ReadableStream as ReadableStream$1 } from "node:stream/web";
@@ -24,8 +24,8 @@ import zlib, { zstdDecompress } from "node:zlib";
24
24
  import { Buffer as Buffer$1 } from "node:buffer";
25
25
  import { aborted, callbackify, debuglog, inspect, promisify, stripVTControlCharacters } from "node:util";
26
26
  import { URL as URL$1, fileURLToPath, pathToFileURL } from "node:url";
27
- import * as net from "node:net";
28
- import { BlockList, connect, createServer as createServer$2, isIP } from "node:net";
27
+ import * as net$1 from "node:net";
28
+ import net, { BlockList, connect, createServer as createServer$2, isIP } from "node:net";
29
29
  import { spawn as spawn$1 } from "child_process";
30
30
  import * as fs$3 from "fs";
31
31
  import { createRequire as createRequire$1 } from "module";
@@ -20140,28 +20140,68 @@ function clearCredentials() {
20140
20140
  //#endregion
20141
20141
  //#region src/auth/refresh.ts
20142
20142
  const CLIENT_ID$1 = "lobehub-cli";
20143
+ /** HTTP statuses that say "ask again later" rather than "this token is dead". */
20144
+ const TRANSIENT_STATUSES = new Set([
20145
+ 408,
20146
+ 425,
20147
+ 429
20148
+ ]);
20149
+ /**
20150
+ * The one OAuth error that proves the stored refresh token itself is finished (RFC 6749 §5.2).
20151
+ * Its neighbours — `invalid_client`, `invalid_request`, `unauthorized_client` — describe how the
20152
+ * request was built, not whether the credential still lives, so re-authenticating over them just
20153
+ * walks the user into the same wall.
20154
+ */
20155
+ const CREDENTIAL_SPENT_ERROR = "invalid_grant";
20143
20156
  /**
20144
20157
  * Get a valid access token, refreshing if expired.
20145
- * Returns null if no credentials or refresh fails.
20146
20158
  */
20147
20159
  async function getValidToken(bufferSeconds = 60) {
20148
20160
  const credentials = loadCredentials();
20149
- if (!credentials) return null;
20150
- if (credentials.expiresAt && Date.now() / 1e3 < credentials.expiresAt - bufferSeconds) return { credentials };
20151
- if (!credentials.refreshToken) return null;
20161
+ if (!credentials) return { status: "no-login" };
20162
+ if (credentials.expiresAt && Date.now() / 1e3 < credentials.expiresAt - bufferSeconds) return {
20163
+ credentials,
20164
+ status: "ok"
20165
+ };
20166
+ if (!credentials.refreshToken) return { status: "no-login" };
20152
20167
  const refreshed = await refreshAccessToken$1(resolveServerUrl(), credentials.refreshToken);
20153
- if (!refreshed) return null;
20168
+ if (refreshed.status !== "ok") return refreshed;
20154
20169
  const updated = {
20155
- accessToken: refreshed.access_token,
20156
- expiresAt: refreshed.expires_in ? Math.floor(Date.now() / 1e3) + refreshed.expires_in : void 0,
20157
- refreshToken: refreshed.refresh_token || credentials.refreshToken
20170
+ accessToken: refreshed.token.access_token,
20171
+ expiresAt: refreshed.token.expires_in ? Math.floor(Date.now() / 1e3) + refreshed.token.expires_in : void 0,
20172
+ refreshToken: refreshed.token.refresh_token || credentials.refreshToken
20158
20173
  };
20159
20174
  saveCredentials(updated);
20160
- return { credentials: updated };
20175
+ return {
20176
+ credentials: updated,
20177
+ status: "ok"
20178
+ };
20179
+ }
20180
+ /**
20181
+ * How to explain a lookup that produced no token, or `undefined` when there is nothing to
20182
+ * diagnose and the caller's own "log in first" message fits.
20183
+ */
20184
+ function describeTokenLookup(lookup) {
20185
+ switch (lookup.status) {
20186
+ case "spent": return {
20187
+ detail: `The stored login is no longer valid (${lookup.detail}).`,
20188
+ fix: `Run '${"lh"} login' again.`
20189
+ };
20190
+ case "refused": return {
20191
+ detail: `The server refused the refresh request: ${lookup.detail}. The stored login is intact, so the credential is not what it is objecting to.`,
20192
+ fix: "Check the server URL and anything sitting in front of it — signing in again will not change the answer."
20193
+ };
20194
+ case "unavailable": return {
20195
+ detail: `Could not reach the server to refresh the access token: ${lookup.detail}. The stored login is untouched, so this is the network or the server rather than a signed-out session.`,
20196
+ fix: "Retry in a moment; the stored login does not need renewing."
20197
+ };
20198
+ default: return;
20199
+ }
20161
20200
  }
20162
20201
  async function refreshAccessToken$1(serverUrl, refreshToken) {
20202
+ let res;
20163
20203
  try {
20164
- const res = await fetch(`${serverUrl}/oidc/token`, {
20204
+ res = await fetch(`${serverUrl}/oidc/token`, {
20165
20205
  body: new URLSearchParams({
20166
20206
  client_id: CLIENT_ID$1,
20167
20207
  grant_type: "refresh_token",
@@ -20170,12 +20210,42 @@ async function refreshAccessToken$1(serverUrl, refreshToken) {
20170
20210
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
20171
20211
  method: "POST"
20172
20212
  });
20173
- const body = await res.json();
20174
- if (!res.ok || body.error || !body.access_token) return null;
20175
- return body;
20176
- } catch {
20177
- return null;
20213
+ } catch (error) {
20214
+ return {
20215
+ detail: error instanceof Error ? error.message : String(error),
20216
+ status: "unavailable"
20217
+ };
20218
+ }
20219
+ const body = await res.json().catch(() => void 0);
20220
+ if (!res.ok) {
20221
+ const detail = body?.error ? `${body.error}${body.error_description ? `: ${body.error_description}` : ""}` : `HTTP ${res.status}`;
20222
+ if (res.status >= 500 || TRANSIENT_STATUSES.has(res.status)) return {
20223
+ detail,
20224
+ status: "unavailable"
20225
+ };
20226
+ return body?.error === CREDENTIAL_SPENT_ERROR ? {
20227
+ detail,
20228
+ status: "spent"
20229
+ } : {
20230
+ detail,
20231
+ status: "refused"
20232
+ };
20178
20233
  }
20234
+ if (body?.error) return body.error === CREDENTIAL_SPENT_ERROR ? {
20235
+ detail: body.error,
20236
+ status: "spent"
20237
+ } : {
20238
+ detail: body.error,
20239
+ status: "refused"
20240
+ };
20241
+ if (!body?.access_token) return {
20242
+ detail: "the response carried no access token",
20243
+ status: "unavailable"
20244
+ };
20245
+ return {
20246
+ status: "ok",
20247
+ token: body
20248
+ };
20179
20249
  }
20180
20250
 
20181
20251
  //#endregion
@@ -20377,8 +20447,9 @@ async function getAuthAndServer() {
20377
20447
  serverUrl: resolveServerUrl()
20378
20448
  };
20379
20449
  const result = await getValidToken();
20380
- if (!result) {
20381
- log$12.error(`No authentication found. Run '${"lh"} login' (or 'npx -y ${cliPackageName} login') first, or set ${CLI_API_KEY_ENV}.`);
20450
+ if (result.status !== "ok") {
20451
+ const report = describeTokenLookup(result);
20452
+ log$12.error(report ? `${report.detail} ${report.fix}` : `No authentication found. Run '${"lh"} login' (or 'npx -y ${cliPackageName} login') first, or set ${CLI_API_KEY_ENV}.`);
20382
20453
  process.exit(1);
20383
20454
  }
20384
20455
  const serverUrl = resolveServerUrl();
@@ -20455,12 +20526,13 @@ async function getAuthInfo(workspaceId) {
20455
20526
  serverUrl
20456
20527
  };
20457
20528
  const result = await getValidToken();
20458
- if (!result) {
20529
+ if (result.status !== "ok") {
20459
20530
  if (readCliApiKeyEnv()) {
20460
20531
  log$12.error(`API key auth from ${CLI_API_KEY_ENV} is not supported for /webapi/* routes. Run OIDC login instead.`);
20461
20532
  process.exit(1);
20462
20533
  }
20463
- log$12.error(`No authentication found. Run '${"lh"} login' first.`);
20534
+ const report = describeTokenLookup(result);
20535
+ log$12.error(report ? `${report.detail} ${report.fix}` : `No authentication found. Run '${"lh"} login' first.`);
20464
20536
  process.exit(1);
20465
20537
  }
20466
20538
  const accessToken = result.credentials.accessToken;
@@ -20490,8 +20562,9 @@ async function getAgentStreamAuthInfo(workspaceId) {
20490
20562
  tokenType: "apiKey"
20491
20563
  };
20492
20564
  const result = await getValidToken();
20493
- if (!result) {
20494
- log$12.error(`No authentication found. Run '${"lh"} login' first, or set ${CLI_API_KEY_ENV}.`);
20565
+ if (result.status !== "ok") {
20566
+ const report = describeTokenLookup(result);
20567
+ log$12.error(report ? `${report.detail} ${report.fix}` : `No authentication found. Run '${"lh"} login' first, or set ${CLI_API_KEY_ENV}.`);
20495
20568
  process.exit(1);
20496
20569
  return {
20497
20570
  headers: {},
@@ -26088,7 +26161,11 @@ var init_heteroSelectorCapabilities = __esmMin((() => {
26088
26161
  source: "catalog"
26089
26162
  }
26090
26163
  },
26091
- "kimi-code": {},
26164
+ "kimi-code": { model: {
26165
+ encodings: [MODEL_FLAGS_ENCODING],
26166
+ resolve: resolvePersistedModel,
26167
+ source: "catalog"
26168
+ } },
26092
26169
  "opencode": { model: {
26093
26170
  encodings: [MODEL_FLAGS_ENCODING],
26094
26171
  resolve: resolvePersistedModel,
@@ -26128,7 +26205,7 @@ var init_heteroSelectorCapabilities = __esmMin((() => {
26128
26205
 
26129
26206
  //#endregion
26130
26207
  //#region ../../packages/types/src/agent/agencyConfig.ts
26131
- var HETEROGENEOUS_AGENT_TYPES, modelFlagsOf, CODEX_MODEL_FLAGS$1, GROK_BUILD_MODEL_FLAGS, OPENCODE_MODEL_FLAGS, PI_MODEL_FLAGS, QODER_MODEL_FLAGS;
26208
+ var HETEROGENEOUS_AGENT_TYPES, modelFlagsOf, CODEX_MODEL_FLAGS$1, GROK_BUILD_MODEL_FLAGS, KIMI_CODE_MODEL_FLAGS, OPENCODE_MODEL_FLAGS, PI_MODEL_FLAGS, QODER_MODEL_FLAGS;
26132
26209
  var init_agencyConfig = __esmMin((() => {
26133
26210
  init_heterogeneousAgent();
26134
26211
  init_heteroSelectorCapabilities();
@@ -26136,6 +26213,7 @@ var init_agencyConfig = __esmMin((() => {
26136
26213
  modelFlagsOf = (type) => HETERO_SELECTOR_CAPABILITIES[type].model.encodings.flatMap((encoding) => encoding.kind === "flag" ? encoding.flags : []);
26137
26214
  CODEX_MODEL_FLAGS$1 = modelFlagsOf("codex");
26138
26215
  GROK_BUILD_MODEL_FLAGS = modelFlagsOf("grok-build");
26216
+ KIMI_CODE_MODEL_FLAGS = modelFlagsOf("kimi-code");
26139
26217
  OPENCODE_MODEL_FLAGS = modelFlagsOf("opencode");
26140
26218
  PI_MODEL_FLAGS = modelFlagsOf("pi");
26141
26219
  QODER_MODEL_FLAGS = modelFlagsOf("qoder");
@@ -26282,6 +26360,12 @@ var init_chatConfig = __esmMin((() => {
26282
26360
  "high",
26283
26361
  "xhigh"
26284
26362
  ]).optional(),
26363
+ grok4_7ReasoningEffort: _enum([
26364
+ "low",
26365
+ "medium",
26366
+ "high",
26367
+ "xhigh"
26368
+ ]).optional(),
26285
26369
  hy3ReasoningEffort: _enum([
26286
26370
  "no_think",
26287
26371
  "low",
@@ -26728,6 +26812,8 @@ var init_agentRuntime = __esmMin((() => {
26728
26812
  RequestTrigger["Openapi"] = "openapi";
26729
26813
  /** A run the user deferred to a future time (`topic.metadata.scheduledRun`). */
26730
26814
  RequestTrigger["Scheduled"] = "scheduled";
26815
+ /** A provider event on a pull request (CI failure, review) woke the agent that opened it. */
26816
+ RequestTrigger["Scm"] = "scm";
26731
26817
  RequestTrigger["SemanticSearch"] = "semantic_search";
26732
26818
  RequestTrigger["SignupEmailLLMReview"] = "signup_email_llm_review";
26733
26819
  RequestTrigger["Topic"] = "topic";
@@ -26758,7 +26844,7 @@ var init_pageSelection = __esmMin((() => {
26758
26844
 
26759
26845
  //#endregion
26760
26846
  //#region ../../packages/types/src/message/common/metadata.ts
26761
- var LocalSystemToolSnapshotSchema, ModelUsageSchema, ModelPerformanceSchema, EmojiReactionSchema, MessageSignalSchema, MessageTaskCallbackSchema, MessageWorkMetadataSchema, AgentDispatchMetadataSchema, BotSenderMetadataSchema, MessageMetadataSchema;
26847
+ var LocalSystemToolSnapshotSchema, ModelUsageSchema, ModelPerformanceSchema, EmojiReactionSchema, MessageSignalSchema, MessageTaskCallbackSchema, MessageWorkMetadataSchema, AgentDispatchMetadataSchema, ExternalOriginMetadataSchema, BotSenderMetadataSchema, MessageMetadataSchema;
26762
26848
  var init_metadata = __esmMin((() => {
26763
26849
  init_zod$1();
26764
26850
  init_agentRuntime();
@@ -26846,6 +26932,13 @@ var init_metadata = __esmMin((() => {
26846
26932
  kind: _enum(["callAgent"]),
26847
26933
  visibility: literal("internal")
26848
26934
  });
26935
+ ExternalOriginMetadataSchema = object$2({
26936
+ kind: string(),
26937
+ label: string(),
26938
+ provider: string(),
26939
+ resourceId: string().optional(),
26940
+ url: string().optional()
26941
+ });
26849
26942
  BotSenderMetadataSchema = object$2({
26850
26943
  avatar: string().optional(),
26851
26944
  fullName: string().optional(),
@@ -26855,6 +26948,7 @@ var init_metadata = __esmMin((() => {
26855
26948
  });
26856
26949
  MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSchema).extend({
26857
26950
  botSender: BotSenderMetadataSchema.optional(),
26951
+ externalOrigin: ExternalOriginMetadataSchema.optional(),
26858
26952
  agentDispatch: AgentDispatchMetadataSchema.optional(),
26859
26953
  collapsed: boolean().optional(),
26860
26954
  contextSelections: array(ContextSelectionSchema).optional(),
@@ -28572,6 +28666,12 @@ var init_aiModel = __esmMin((() => {
28572
28666
  "high",
28573
28667
  "xhigh"
28574
28668
  ]).optional(),
28669
+ grok4_7ReasoningEffort: _enum([
28670
+ "low",
28671
+ "medium",
28672
+ "high",
28673
+ "xhigh"
28674
+ ]).optional(),
28575
28675
  grok4_20ReasoningEffort: _enum([
28576
28676
  "low",
28577
28677
  "medium",
@@ -28644,6 +28744,7 @@ var init_aiModel = __esmMin((() => {
28644
28744
  "grok4_3ReasoningEffort",
28645
28745
  "grok4_5ReasoningEffort",
28646
28746
  "grok4_6ReasoningEffort",
28747
+ "grok4_7ReasoningEffort",
28647
28748
  "hy3ReasoningEffort",
28648
28749
  "kimiK3ReasoningEffort",
28649
28750
  "ring2_6ReasoningEffort",
@@ -29056,7 +29157,12 @@ var init_preference = __esmMin((() => {
29056
29157
  * enable the per-topic acceptance tray above the composer (author a topic's
29057
29158
  * delivery checklist inline)
29058
29159
  */
29059
- enableTopicAcceptance: boolean().optional()
29160
+ enableTopicAcceptance: boolean().optional(),
29161
+ /**
29162
+ * expose a port on the working device through a tunnel link, so a dev server
29163
+ * running there can be opened from this UI
29164
+ */
29165
+ enableDeviceTunnel: boolean().optional()
29060
29166
  });
29061
29167
  GithubIntegrationPreferenceSchema = object$2({
29062
29168
  acceptOnMerge: boolean().optional(),
@@ -40423,8 +40529,12 @@ function registerBotCommand(program) {
40423
40529
  const client = await getTrpcClient();
40424
40530
  const input = { id: botId };
40425
40531
  const existing = await findBot(client, botId);
40426
- const { credentials } = extractCredentials(await resolvePlatform(client, options.platform ?? existing.platform), options);
40427
- if (Object.keys(credentials).length > 0) input.credentials = credentials;
40532
+ const platform = options.platform ?? existing.platform;
40533
+ const { credentials } = extractCredentials(await resolvePlatform(client, platform), options);
40534
+ if (Object.keys(credentials).length > 0) input.credentials = platform === existing.platform ? {
40535
+ ...existing.credentials,
40536
+ ...credentials
40537
+ } : credentials;
40428
40538
  if (options.appId) input.applicationId = options.appId;
40429
40539
  if (options.platform) input.platform = options.platform;
40430
40540
  const settingsPatch = {};
@@ -42916,8 +43026,20 @@ var AmpAdapter = class {
42916
43026
  };
42917
43027
 
42918
43028
  //#endregion
42919
- //#region ../../packages/heterogeneous-agents/src/errors/claudeCodeQuota.ts
42920
- const CLI_CREDIT_LIMIT_PATTERNS = [/you'?ve reached your .{0,40}\blimit\b/i, /\/usage-credits\b/i];
43029
+ //#region ../../packages/heterogeneous-agents/src/errors/cliQuota.ts
43030
+ /**
43031
+ * Quota wording shared by CLI-wrapped agents.
43032
+ *
43033
+ * Every local CLI agent bills against its own subscription and prints the
43034
+ * same kind of sentence when that subscription is spent — CC's `You've hit
43035
+ * your weekly limit`, Kimi Code's `You've reached your weekly (7-day) usage
43036
+ * limit`, a gateway's `已达到 5 小时使用上限`. The patterns therefore live here
43037
+ * rather than in one adapter, and `classifyCliQuotaMessage` is the single
43038
+ * place that turns them into a taxonomy kind.
43039
+ */
43040
+ /** Remedies that only a top-up clears — waiting for a window never helps. */
43041
+ const CLI_CREDIT_REMEDY_PATTERNS = [/\/usage-credits\b/i];
43042
+ const CLI_CREDIT_LIMIT_PATTERNS = [/you'?ve reached your .{0,40}\blimit\b/i, ...CLI_CREDIT_REMEDY_PATTERNS];
42921
43043
  const CLI_USER_RATE_LIMIT_PATTERNS = [
42922
43044
  /you'?ve hit your limit/i,
42923
43045
  /you'?ve hit your \S+ limit/i,
@@ -42935,6 +43057,50 @@ const CLI_USER_RATE_LIMIT_PATTERNS = [
42935
43057
  * misleading "usage limit reached" reset-time guide).
42936
43058
  */
42937
43059
  const CLI_SERVER_THROTTLE_PATTERNS = [/not your usage limit/i, /server is temporarily limiting requests/i];
43060
+ /**
43061
+ * Wording that promises the window reopens by itself — Kimi Code's
43062
+ * `Your quota will reset when the current 7-day window ends`, CC's
43063
+ * `resets 6:30pm (Asia/Shanghai)`, a gateway's `2026-06-19 22:00:00 后可继续
43064
+ * 使用`. It is what separates a plan window (wait, or schedule the
43065
+ * continuation) from exhausted credits (top up or switch model), because both
43066
+ * open with the same "you've reached your … limit" sentence.
43067
+ */
43068
+ const CLI_QUOTA_RESET_PATTERNS = [
43069
+ /\bresets?\b/i,
43070
+ /\b(?:refresh|renew)(?:ed|es|s)?\b/i,
43071
+ /后可继续使用/,
43072
+ /(?:额度|配额|上限)[^。]{0,12}(?:重置|刷新|恢复)/
43073
+ ];
43074
+ /**
43075
+ * The window a rejected quota belongs to, in the vocabulary the rate-limit
43076
+ * guide already renders (`seven_day` → 周期上限, `five_hour` → 5 小时上限).
43077
+ * CC ships it as structured `rate_limit_info`; CLIs that only print prose
43078
+ * (Kimi Code: `You've reached your weekly (7-day) usage limit`) get it read
43079
+ * back out of the sentence so the card can name the window.
43080
+ */
43081
+ const CLI_RATE_LIMIT_TYPE_PATTERNS = [[/\bweekly\b|\b7[\s-]?day\b|\bseven[\s-]?day\b|每周|7\s*天|七天/i, "seven_day"], [/\b5[\s-]?hour\b|\bfive[\s-]?hour\b|\bsession limit\b|5\s*小时/i, "five_hour"]];
43082
+ /**
43083
+ * Classify a CLI's free-form failure text as a user-side quota rejection.
43084
+ *
43085
+ * Deliberately agent-agnostic: every CLI-wrapped agent bills against its own
43086
+ * subscription and prints the same kind of sentence when that subscription is
43087
+ * spent, so the classification lives with the patterns rather than in one
43088
+ * adapter. Returns `undefined` for anything that is not unambiguously the
43089
+ * user's own quota — a bare 429, `rate limited`, or a provider-side throttle
43090
+ * that disclaims the plan limit belongs to the retry (overloaded) path.
43091
+ */
43092
+ const classifyCliQuotaMessage = (message) => {
43093
+ if (!message) return;
43094
+ if (CLI_SERVER_THROTTLE_PATTERNS.some((pattern) => pattern.test(message))) return;
43095
+ if (!CLI_USER_RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(message))) return;
43096
+ const resets = CLI_QUOTA_RESET_PATTERNS.some((pattern) => pattern.test(message));
43097
+ const kind = CLI_CREDIT_REMEDY_PATTERNS.some((pattern) => pattern.test(message)) || !resets && CLI_CREDIT_LIMIT_PATTERNS.some((pattern) => pattern.test(message)) ? "credit_limit" : "usage_limit";
43098
+ const rateLimitType = CLI_RATE_LIMIT_TYPE_PATTERNS.find(([pattern]) => pattern.test(message))?.[1];
43099
+ return {
43100
+ kind,
43101
+ ...rateLimitType ? { rateLimitType } : {}
43102
+ };
43103
+ };
42938
43104
 
42939
43105
  //#endregion
42940
43106
  //#region ../../packages/heterogeneous-agents/src/adapters/claudeCode.ts
@@ -43483,7 +43649,7 @@ const getRateLimitTerminalError = (result, agentType, rateLimitInfo) => {
43483
43649
  agentType,
43484
43650
  clearEchoedContent: true,
43485
43651
  code: "rate_limit",
43486
- details: { kind: CLI_CREDIT_LIMIT_PATTERNS.some((pattern) => pattern.test(rawMessage)) ? "credit_limit" : "usage_limit" },
43652
+ details: { kind: classifyCliQuotaMessage(rawMessage)?.kind ?? "usage_limit" },
43487
43653
  error: rawMessage,
43488
43654
  message: rawMessage,
43489
43655
  rateLimitInfo,
@@ -47374,9 +47540,9 @@ init_config();
47374
47540
  * error card instead of the heterogeneous status guide (install CLI / sign in).
47375
47541
  *
47376
47542
  * This helper mirrors the desktop in-process classifier
47377
- * (`HeterogeneousAgentCtr.getSessionErrorPayload`) for the two guide codes a
47543
+ * (`HeterogeneousAgentCtr.getSessionErrorPayload`) for the guide codes a
47378
47544
  * process-level failure can produce: `cli_not_found`,
47379
- * `working_directory_not_found`, and `auth_required`.
47545
+ * `working_directory_not_found`, `rate_limit` and `auth_required`.
47380
47546
  * The returned shape is persisted verbatim as the `ChatMessageError.body`, so
47381
47547
  * it must carry `agentType` + `code` — that pair is what
47382
47548
  * `isHeterogeneousAgentStatusGuideError` gates the dedicated UI on.
@@ -47434,6 +47600,19 @@ const classifyHeteroProcessFailure = (params) => {
47434
47600
  command,
47435
47601
  stderr: detail
47436
47602
  });
47603
+ const quota = classifyCliQuotaMessage(detail);
47604
+ if (detail && quota) return {
47605
+ agentType,
47606
+ code: "rate_limit",
47607
+ details: { kind: quota.kind },
47608
+ error: detail,
47609
+ message: detail,
47610
+ ...quota.rateLimitType ? { rateLimitInfo: {
47611
+ rateLimitType: quota.rateLimitType,
47612
+ status: "rejected"
47613
+ } } : {},
47614
+ stderr: detail
47615
+ };
47437
47616
  if (detail && isHeterogeneousAgentAuthRequired(agentType, detail)) return buildHeterogeneousAgentAuthRequiredError({
47438
47617
  agentType,
47439
47618
  command,
@@ -234816,7 +234995,7 @@ const createLogger = (namespace) => ({
234816
234995
  //#endregion
234817
234996
  //#region ../../packages/local-file-shell/src/git/branches.ts
234818
234997
  const log$3 = createLogger("local-file-shell:git");
234819
- const execFileAsync$6 = promisify(execFile);
234998
+ const execFileAsync$7 = promisify(execFile);
234820
234999
  /** Reject obviously invalid branch refs early to avoid a confusing git error. */
234821
235000
  const isInvalidBranchRef = (name) => /[\s~^:?*[\\]/.test(name) || name.startsWith("-") || name.includes("..");
234822
235001
  /**
@@ -234825,7 +235004,7 @@ const isInvalidBranchRef = (name) => /[\s~^:?*[\\]/.test(name) || name.startsWit
234825
235004
  */
234826
235005
  const listGitBranches = async (dirPath) => {
234827
235006
  try {
234828
- const { stdout } = await execFileAsync$6("git", [
235007
+ const { stdout } = await execFileAsync$7("git", [
234829
235008
  "for-each-ref",
234830
235009
  "--sort=-committerdate",
234831
235010
  "--format=%(HEAD)%09%(refname:short)%09%(upstream:short)",
@@ -234860,7 +235039,7 @@ const listGitBranches = async (dirPath) => {
234860
235039
  const listGitRemoteBranches = async (dirPath) => {
234861
235040
  let defaultRef;
234862
235041
  try {
234863
- const { stdout } = await execFileAsync$6("git", [
235042
+ const { stdout } = await execFileAsync$7("git", [
234864
235043
  "symbolic-ref",
234865
235044
  "--short",
234866
235045
  "refs/remotes/origin/HEAD"
@@ -234873,7 +235052,7 @@ const listGitRemoteBranches = async (dirPath) => {
234873
235052
  defaultRef = void 0;
234874
235053
  }
234875
235054
  try {
234876
- const { stdout } = await execFileAsync$6("git", [
235055
+ const { stdout } = await execFileAsync$7("git", [
234877
235056
  "for-each-ref",
234878
235057
  "--sort=-committerdate",
234879
235058
  "--format=%(refname:short)",
@@ -234917,7 +235096,7 @@ const checkoutGitBranch = async (payload) => {
234917
235096
  branch
234918
235097
  ] : ["checkout", branch];
234919
235098
  try {
234920
- await execFileAsync$6("git", args, {
235099
+ await execFileAsync$7("git", args, {
234921
235100
  cwd: dirPath,
234922
235101
  timeout: 1e4
234923
235102
  });
@@ -234950,7 +235129,7 @@ const renameGitBranch = async (payload) => {
234950
235129
  success: false
234951
235130
  };
234952
235131
  try {
234953
- await execFileAsync$6("git", [
235132
+ await execFileAsync$7("git", [
234954
235133
  "branch",
234955
235134
  "-m",
234956
235135
  from,
@@ -234989,7 +235168,7 @@ const deleteGitBranch = async (payload) => {
234989
235168
  success: false
234990
235169
  };
234991
235170
  try {
234992
- await execFileAsync$6("git", [
235171
+ await execFileAsync$7("git", [
234993
235172
  "branch",
234994
235173
  "-D",
234995
235174
  branch
@@ -235017,7 +235196,7 @@ const deleteGitBranch = async (payload) => {
235017
235196
  const pullGitBranch = async (payload) => {
235018
235197
  const { path: dirPath } = payload;
235019
235198
  try {
235020
- const { stdout } = await execFileAsync$6("git", ["pull", "--ff-only"], {
235199
+ const { stdout } = await execFileAsync$7("git", ["pull", "--ff-only"], {
235021
235200
  cwd: dirPath,
235022
235201
  timeout: 6e4
235023
235202
  });
@@ -235042,7 +235221,7 @@ const pullGitBranch = async (payload) => {
235042
235221
  const pushGitBranch = async (payload) => {
235043
235222
  const { path: dirPath } = payload;
235044
235223
  try {
235045
- const { stderr } = await execFileAsync$6("git", [
235224
+ const { stderr } = await execFileAsync$7("git", [
235046
235225
  "push",
235047
235226
  "-u",
235048
235227
  "origin",
@@ -235089,7 +235268,7 @@ const revertGitFile = async (payload) => {
235089
235268
  };
235090
235269
  let existsAtHead;
235091
235270
  try {
235092
- await execFileAsync$6("git", [
235271
+ await execFileAsync$7("git", [
235093
235272
  "cat-file",
235094
235273
  "-e",
235095
235274
  `HEAD:${filePath}`
@@ -235102,7 +235281,7 @@ const revertGitFile = async (payload) => {
235102
235281
  existsAtHead = false;
235103
235282
  }
235104
235283
  try {
235105
- if (existsAtHead) await execFileAsync$6("git", [
235284
+ if (existsAtHead) await execFileAsync$7("git", [
235106
235285
  "checkout",
235107
235286
  "HEAD",
235108
235287
  "--",
@@ -235113,7 +235292,7 @@ const revertGitFile = async (payload) => {
235113
235292
  });
235114
235293
  else {
235115
235294
  try {
235116
- await execFileAsync$6("git", [
235295
+ await execFileAsync$7("git", [
235117
235296
  "rm",
235118
235297
  "--cached",
235119
235298
  "--quiet",
@@ -235203,12 +235382,12 @@ const detectRepoType = async (dirPath) => {
235203
235382
 
235204
235383
  //#endregion
235205
235384
  //#region ../../packages/local-file-shell/src/git/upstream.ts
235206
- const execFileAsync$5 = promisify(execFile);
235385
+ const execFileAsync$6 = promisify(execFile);
235207
235386
  const GIT_TIMEOUT = 5e3;
235208
235387
  /** Local-only git read. Every step of the resolver degrades to "unknown", never throws. */
235209
235388
  const runGit = async (args, cwd) => {
235210
235389
  try {
235211
- const { stdout } = await execFileAsync$5("git", args, {
235390
+ const { stdout } = await execFileAsync$6("git", args, {
235212
235391
  cwd,
235213
235392
  timeout: GIT_TIMEOUT
235214
235393
  });
@@ -235396,7 +235575,7 @@ const getDefaultRemote = async (dirPath) => {
235396
235575
  /** Exit-status-only probe — `runGit` cannot tell success-with-no-output from failure. */
235397
235576
  const gitSucceeds = async (args, cwd) => {
235398
235577
  try {
235399
- await execFileAsync$5("git", args, {
235578
+ await execFileAsync$6("git", args, {
235400
235579
  cwd,
235401
235580
  timeout: GIT_TIMEOUT
235402
235581
  });
@@ -235436,7 +235615,7 @@ const isCommitSafeForPullRequestLookup = async (dirPath, sha) => {
235436
235615
  //#endregion
235437
235616
  //#region ../../packages/local-file-shell/src/git/info.ts
235438
235617
  const log$2 = createLogger("local-file-shell:git");
235439
- const execFileAsync$4 = promisify(execFile);
235618
+ const execFileAsync$5 = promisify(execFile);
235440
235619
  const GITHUB_PULL_REQUEST_FIELDS = "number,url,title,state,isDraft,mergeable,mergeStateStatus,mergedAt,reviewDecision,statusCheckRollup,headRefName";
235441
235620
  const failureConclusions$1 = new Set([
235442
235621
  "action_required",
@@ -235541,7 +235720,7 @@ const getGitBranch = async (dirPath) => {
235541
235720
  */
235542
235721
  const findPullRequestByCommit = async (dirPath, sha) => {
235543
235722
  try {
235544
- const { stdout } = await execFileAsync$4("gh", ["api", `repos/{owner}/{repo}/commits/${sha}/pulls`], {
235723
+ const { stdout } = await execFileAsync$5("gh", ["api", `repos/{owner}/{repo}/commits/${sha}/pulls`], {
235545
235724
  cwd: dirPath,
235546
235725
  timeout: 8e3
235547
235726
  });
@@ -235594,7 +235773,7 @@ const getLinkedPullRequest = async (payload) => {
235594
235773
  status: "ok"
235595
235774
  };
235596
235775
  const viewPullRequest = async (number) => {
235597
- const { stdout } = await execFileAsync$4("gh", [
235776
+ const { stdout } = await execFileAsync$5("gh", [
235598
235777
  "pr",
235599
235778
  "view",
235600
235779
  String(number),
@@ -235620,7 +235799,7 @@ const getLinkedPullRequest = async (payload) => {
235620
235799
  ...upstream ? { upstream } : {}
235621
235800
  };
235622
235801
  }
235623
- const { stdout } = await execFileAsync$4("gh", [
235802
+ const { stdout } = await execFileAsync$5("gh", [
235624
235803
  "pr",
235625
235804
  "list",
235626
235805
  "--head",
@@ -235687,7 +235866,7 @@ const getLinkedPullRequest = async (payload) => {
235687
235866
  /** Bucket dirty files into added / modified / deleted via `git status --porcelain -z`. */
235688
235867
  const getGitWorkingTreeStatus = async (dirPath) => {
235689
235868
  try {
235690
- const { stdout } = await execFileAsync$4("git", [
235869
+ const { stdout } = await execFileAsync$5("git", [
235691
235870
  "status",
235692
235871
  "--porcelain",
235693
235872
  "-u",
@@ -235738,7 +235917,7 @@ const getGitWorkingTreeStatus = async (dirPath) => {
235738
235917
  */
235739
235918
  const getGitAheadBehind = async (dirPath) => {
235740
235919
  try {
235741
- await execFileAsync$4("git", [
235920
+ await execFileAsync$5("git", [
235742
235921
  "fetch",
235743
235922
  "--no-tags",
235744
235923
  "--quiet",
@@ -235749,7 +235928,7 @@ const getGitAheadBehind = async (dirPath) => {
235749
235928
  });
235750
235929
  } catch {}
235751
235930
  try {
235752
- const { stdout: upstreamOut } = await execFileAsync$4("git", [
235931
+ const { stdout: upstreamOut } = await execFileAsync$5("git", [
235753
235932
  "rev-parse",
235754
235933
  "--abbrev-ref",
235755
235934
  "--symbolic-full-name",
@@ -235764,7 +235943,7 @@ const getGitAheadBehind = async (dirPath) => {
235764
235943
  behind: 0,
235765
235944
  hasUpstream: false
235766
235945
  };
235767
- const { stdout } = await execFileAsync$4("git", [
235946
+ const { stdout } = await execFileAsync$5("git", [
235768
235947
  "rev-list",
235769
235948
  "--left-right",
235770
235949
  "--count",
@@ -235779,7 +235958,7 @@ const getGitAheadBehind = async (dirPath) => {
235779
235958
  let pushTarget;
235780
235959
  let pushTargetExists = false;
235781
235960
  try {
235782
- const { stdout: branchOut } = await execFileAsync$4("git", [
235961
+ const { stdout: branchOut } = await execFileAsync$5("git", [
235783
235962
  "symbolic-ref",
235784
235963
  "--short",
235785
235964
  "HEAD"
@@ -235791,7 +235970,7 @@ const getGitAheadBehind = async (dirPath) => {
235791
235970
  if (branch) {
235792
235971
  pushTarget = `origin/${branch}`;
235793
235972
  try {
235794
- await execFileAsync$4("git", [
235973
+ await execFileAsync$5("git", [
235795
235974
  "rev-parse",
235796
235975
  "--verify",
235797
235976
  "--quiet",
@@ -235826,7 +236005,7 @@ const getGitAheadBehind = async (dirPath) => {
235826
236005
  //#endregion
235827
236006
  //#region ../../packages/local-file-shell/src/git/pullRequest.ts
235828
236007
  const log$1 = createLogger("local-file-shell:git");
235829
- const execFileAsync$3 = promisify(execFile);
236008
+ const execFileAsync$4 = promisify(execFile);
235830
236009
  const GITHUB_PULL_REQUEST_DETAIL_FIELDS = "number,title,body,state,isDraft,isCrossRepository,mergedAt,mergeable,mergeStateStatus,reviewDecision,autoMergeRequest,baseRefName,headRefName,headRefOid,url,author,additions,deletions,changedFiles,commits,comments,reviews,statusCheckRollup";
235831
236010
  const failureConclusions = new Set([
235832
236011
  "action_required",
@@ -235901,7 +236080,7 @@ const normalizePullRequestActivity = (raw) => ({
235901
236080
  }))
235902
236081
  });
235903
236082
  const getPullRequestActivity = async (payload) => {
235904
- const { stdout } = await execFileAsync$3("gh", [
236083
+ const { stdout } = await execFileAsync$4("gh", [
235905
236084
  "pr",
235906
236085
  "view",
235907
236086
  String(payload.number),
@@ -235977,7 +236156,7 @@ const getPullRequestMergeContext = async (payload) => {
235977
236156
  const { path: dirPath, number, repo, baseRefName, headRefOid } = payload;
235978
236157
  assertBranchName(baseRefName);
235979
236158
  if (!/^[a-f\d]{40}$/i.test(headRefOid)) return EMPTY_MERGE_CONTEXT;
235980
- const exec = (args) => execFileAsync$3("gh", args, {
236159
+ const exec = (args) => execFileAsync$4("gh", args, {
235981
236160
  cwd: dirPath,
235982
236161
  timeout: 8e3
235983
236162
  });
@@ -236016,7 +236195,7 @@ const isGhMissing = (error) => {
236016
236195
  const getPullRequestDetail = async (payload) => {
236017
236196
  const { path: dirPath, number } = payload;
236018
236197
  try {
236019
- const { stdout } = await execFileAsync$3("gh", [
236198
+ const { stdout } = await execFileAsync$4("gh", [
236020
236199
  "pr",
236021
236200
  "view",
236022
236201
  String(number),
@@ -236137,7 +236316,7 @@ const pullRequestActionArgs = (number, action) => {
236137
236316
  const runPullRequestAction = async (payload) => {
236138
236317
  const { path: dirPath, number, action } = payload;
236139
236318
  try {
236140
- for (const argv of pullRequestActionArgs(number, action)) await execFileAsync$3("gh", argv, {
236319
+ for (const argv of pullRequestActionArgs(number, action)) await execFileAsync$4("gh", argv, {
236141
236320
  cwd: dirPath,
236142
236321
  timeout: 6e4
236143
236322
  });
@@ -236158,7 +236337,7 @@ const runPullRequestAction = async (payload) => {
236158
236337
  //#endregion
236159
236338
  //#region ../../packages/local-file-shell/src/git/workingTree.ts
236160
236339
  const log = createLogger("local-file-shell:git");
236161
- const execFileAsync$2 = promisify(execFile);
236340
+ const execFileAsync$3 = promisify(execFile);
236162
236341
  const MAX_PATCH_BYTES = 256 * 1024;
236163
236342
  /**
236164
236343
  * Split the output of `git diff HEAD --` into one block per file. Each block
@@ -236425,7 +236604,7 @@ const runGitCaptureStream = (cwd, args, timeoutMs) => new Promise((resolve, reje
236425
236604
  const fetchTrackedPatchPerFile = async (cwd, entry, maxBytes) => {
236426
236605
  let text;
236427
236606
  try {
236428
- const { stdout } = await execFileAsync$2("git", [
236607
+ const { stdout } = await execFileAsync$3("git", [
236429
236608
  "-c",
236430
236609
  "core.quotepath=off",
236431
236610
  "diff",
@@ -236489,7 +236668,7 @@ const mapWithConcurrency$1 = async (items, limit, fn) => {
236489
236668
  */
236490
236669
  const listSubmodulePaths = async (dirPath) => {
236491
236670
  try {
236492
- const { stdout } = await execFileAsync$2("git", ["submodule", "status"], {
236671
+ const { stdout } = await execFileAsync$3("git", ["submodule", "status"], {
236493
236672
  cwd: dirPath,
236494
236673
  timeout: 5e3
236495
236674
  });
@@ -236527,7 +236706,7 @@ const getGitWorkingTreeFiles = async (dirPath) => {
236527
236706
  const modified = [];
236528
236707
  const deleted = [];
236529
236708
  try {
236530
- const { stdout } = await execFileAsync$2("git", [
236709
+ const { stdout } = await execFileAsync$3("git", [
236531
236710
  "status",
236532
236711
  "--porcelain",
236533
236712
  "-u",
@@ -236575,7 +236754,7 @@ const collectWorkingTreePatches = async (dirPath, recurseSubmodules) => {
236575
236754
  const entries = [];
236576
236755
  const submoduleDirtyEntries = [];
236577
236756
  try {
236578
- const { stdout } = await execFileAsync$2("git", [
236757
+ const { stdout } = await execFileAsync$3("git", [
236579
236758
  "status",
236580
236759
  "--porcelain",
236581
236760
  "-u",
@@ -236700,7 +236879,7 @@ const getGitWorkingTreePatches = (dirPath) => collectWorkingTreePatches(dirPath,
236700
236879
  */
236701
236880
  const collectBranchDiff = async (dirPath, baseRefOverride, recurseSubmodules) => {
236702
236881
  try {
236703
- await execFileAsync$2("git", [
236882
+ await execFileAsync$3("git", [
236704
236883
  "fetch",
236705
236884
  "--no-tags",
236706
236885
  "--quiet",
@@ -236712,7 +236891,7 @@ const collectBranchDiff = async (dirPath, baseRefOverride, recurseSubmodules) =>
236712
236891
  } catch {}
236713
236892
  let baseRef = baseRefOverride;
236714
236893
  if (!baseRef) try {
236715
- const { stdout } = await execFileAsync$2("git", [
236894
+ const { stdout } = await execFileAsync$3("git", [
236716
236895
  "symbolic-ref",
236717
236896
  "--short",
236718
236897
  "refs/remotes/origin/HEAD"
@@ -236801,7 +236980,7 @@ const getGitBranchDiff = (payload) => collectBranchDiff(payload.path, payload.ba
236801
236980
 
236802
236981
  //#endregion
236803
236982
  //#region ../../packages/local-file-shell/src/git/worktrees.ts
236804
- const execFileAsync$1 = promisify(execFile);
236983
+ const execFileAsync$2 = promisify(execFile);
236805
236984
  const normalizeBranchRef = (ref) => ref.startsWith("refs/heads/") ? ref.slice(11) : ref;
236806
236985
  const safeRealpath$1 = async (target) => {
236807
236986
  try {
@@ -236861,10 +237040,10 @@ const readStatus = async (worktree) => {
236861
237040
  };
236862
237041
  const listGitWorktrees = async (dirPath) => {
236863
237042
  try {
236864
- const [{ stdout: rootStdout }, { stdout }] = await Promise.all([execFileAsync$1("git", ["rev-parse", "--show-toplevel"], {
237043
+ const [{ stdout: rootStdout }, { stdout }] = await Promise.all([execFileAsync$2("git", ["rev-parse", "--show-toplevel"], {
236865
237044
  cwd: dirPath,
236866
237045
  timeout: 5e3
236867
- }), execFileAsync$1("git", [
237046
+ }), execFileAsync$2("git", [
236868
237047
  "worktree",
236869
237048
  "list",
236870
237049
  "--porcelain",
@@ -236918,7 +237097,7 @@ const removeGitWorktree = async (payload) => {
236918
237097
  success: false
236919
237098
  };
236920
237099
  try {
236921
- await execFileAsync$1("git", [
237100
+ await execFileAsync$2("git", [
236922
237101
  "worktree",
236923
237102
  "remove",
236924
237103
  worktree.path
@@ -236960,7 +237139,7 @@ const addGitWorktree = async (payload) => {
236960
237139
  success: false
236961
237140
  };
236962
237141
  try {
236963
- await execFileAsync$1("git", [
237142
+ await execFileAsync$2("git", [
236964
237143
  "worktree",
236965
237144
  "add",
236966
237145
  "-b",
@@ -237224,6 +237403,244 @@ const quotaCache = new QuotaSnapshotCache({ freshMs: KIMI_CODE_QUOTA_FRESH_MS })
237224
237403
  */
237225
237404
  const getKimiCodeQuota = (params = {}) => quotaCache.get(createQuotaCacheKey("kimi-code", params.env), () => fetchKimiCodeQuota({ env: params.env }), { force: params.force });
237226
237405
 
237406
+ //#endregion
237407
+ //#region ../../packages/device-control/src/listeningPorts.ts
237408
+ const execFileAsync$1 = promisify(execFile);
237409
+ const MAX_PORTS = 50;
237410
+ const COMMAND_TIMEOUT_MS = 5e3;
237411
+ const classifyAddress = (address) => {
237412
+ const bare = address.replaceAll(/^\[|\]$/g, "");
237413
+ if (bare === "*" || bare === "0.0.0.0" || bare === "::" || bare === "::0") return "any";
237414
+ if (bare.startsWith("127.")) return "ipv4-loopback";
237415
+ if (bare === "::1" || bare === "::ffff:127.0.0.1") return "ipv6-loopback";
237416
+ return "unreachable";
237417
+ };
237418
+ /** Parse `lsof -nP -iTCP -sTCP:LISTEN -F pcn` field output. */
237419
+ const parseLsofListen = (output) => {
237420
+ const entries = [];
237421
+ let pid;
237422
+ let command;
237423
+ for (const line of output.split("\n")) {
237424
+ const tag = line[0];
237425
+ const value = line.slice(1);
237426
+ if (tag === "p") {
237427
+ pid = Number(value);
237428
+ command = void 0;
237429
+ } else if (tag === "c") command = value;
237430
+ else if (tag === "n") {
237431
+ const sep = value.lastIndexOf(":");
237432
+ const port = Number(value.slice(sep + 1));
237433
+ if (sep > 0 && Number.isInteger(port) && port > 0) entries.push({
237434
+ address: value.slice(0, sep),
237435
+ command,
237436
+ pid,
237437
+ port
237438
+ });
237439
+ }
237440
+ }
237441
+ return entries;
237442
+ };
237443
+ /** Parse `lsof -a -p <pids> -d cwd -Fpn` into pid → cwd. */
237444
+ const parseLsofCwd = (output) => {
237445
+ const cwds = /* @__PURE__ */ new Map();
237446
+ let pid;
237447
+ for (const line of output.split("\n")) if (line[0] === "p") pid = Number(line.slice(1));
237448
+ else if (line[0] === "n" && pid !== void 0) cwds.set(pid, line.slice(1));
237449
+ return cwds;
237450
+ };
237451
+ const LISTEN_STATE = "0A";
237452
+ const decodeProcIpv4 = (hex) => [
237453
+ hex.slice(6, 8),
237454
+ hex.slice(4, 6),
237455
+ hex.slice(2, 4),
237456
+ hex.slice(0, 2)
237457
+ ].map((byte) => Number.parseInt(byte, 16)).join(".");
237458
+ const decodeProcIpv6 = (hex) => {
237459
+ if (/^0+$/.test(hex)) return "::";
237460
+ if (hex === "00000000000000000000000001000000") return "::1";
237461
+ if (hex === "0000000000000000FFFF00000100007F") return "::ffff:127.0.0.1";
237462
+ return hex;
237463
+ };
237464
+ /** Parse `/proc/net/tcp` or `/proc/net/tcp6` into listening sockets keyed by inode. */
237465
+ const parseProcNetTcp = (content, family) => content.split("\n").slice(1).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length > 9 && cols[3] === LISTEN_STATE).map((cols) => {
237466
+ const [addrHex, portHex] = cols[1].split(":");
237467
+ return {
237468
+ address: family === "v4" ? decodeProcIpv4(addrHex) : decodeProcIpv6(addrHex.toUpperCase()),
237469
+ inode: cols[9],
237470
+ port: Number.parseInt(portHex, 16)
237471
+ };
237472
+ });
237473
+ /** Parse `netstat -ano -p TCP` / `-p TCPv6` LISTENING rows. */
237474
+ const parseNetstat = (output) => output.split("\n").map((line) => line.trim().split(/\s+/)).filter((cols) => cols[0] === "TCP" && cols[3] === "LISTENING").map((cols) => {
237475
+ const local = cols[1];
237476
+ const sep = local.lastIndexOf(":");
237477
+ return {
237478
+ address: local.slice(0, sep),
237479
+ pid: Number(cols[4]),
237480
+ port: Number(local.slice(sep + 1))
237481
+ };
237482
+ });
237483
+ /** Parse `tasklist /FO CSV /NH` into pid → image name. */
237484
+ const parseTasklist = (output) => {
237485
+ const names = /* @__PURE__ */ new Map();
237486
+ for (const line of output.split("\n")) {
237487
+ const cols = line.match(/"([^"]*)"/g)?.map((col) => col.slice(1, -1));
237488
+ if (cols && cols.length > 1) names.set(Number(cols[1]), cols[0]);
237489
+ }
237490
+ return names;
237491
+ };
237492
+ const isInside = (dir, root) => {
237493
+ if (!dir || !root) return false;
237494
+ const rel = path.relative(path.resolve(root), path.resolve(dir));
237495
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
237496
+ };
237497
+ /**
237498
+ * Fold raw sockets into one row per port, keep only loopback-reachable ones,
237499
+ * drop this process's own listeners, and put the project's ports first.
237500
+ */
237501
+ const summarizeListeners = (entries, options) => {
237502
+ const byPort = /* @__PURE__ */ new Map();
237503
+ for (const entry of entries) {
237504
+ if (options.ownPid !== void 0 && entry.pid === options.ownPid) continue;
237505
+ const kind = classifyAddress(entry.address);
237506
+ if (kind === "unreachable") continue;
237507
+ const existing = byPort.get(entry.port);
237508
+ if (existing) existing.kinds.add(kind);
237509
+ else byPort.set(entry.port, {
237510
+ entry,
237511
+ kinds: new Set([kind])
237512
+ });
237513
+ }
237514
+ return [...byPort.values()].map(({ entry, kinds }) => {
237515
+ const v4 = kinds.has("any") || kinds.has("ipv4-loopback");
237516
+ const v6 = kinds.has("any") || kinds.has("ipv6-loopback");
237517
+ const cwd = entry.pid === void 0 ? void 0 : options.cwds?.get(entry.pid);
237518
+ return {
237519
+ ...entry.command ? { command: entry.command } : {},
237520
+ ...cwd ? { cwd } : {},
237521
+ inProject: isInside(cwd, options.cwd),
237522
+ loopback: v4 && v6 ? "both" : v4 ? "ipv4" : "ipv6",
237523
+ ...entry.pid === void 0 ? {} : { pid: entry.pid },
237524
+ port: entry.port
237525
+ };
237526
+ }).sort((a, b) => Number(b.inProject) - Number(a.inProject) || a.port - b.port).slice(0, MAX_PORTS);
237527
+ };
237528
+ const run = async (file, args) => (await execFileAsync$1(file, args, {
237529
+ maxBuffer: 4 * 1024 * 1024,
237530
+ timeout: COMMAND_TIMEOUT_MS
237531
+ })).stdout;
237532
+ const collectDarwin = async () => {
237533
+ const entries = parseLsofListen(await run("lsof", [
237534
+ "-nP",
237535
+ "-iTCP",
237536
+ "-sTCP:LISTEN",
237537
+ "-F",
237538
+ "pcn"
237539
+ ]).catch((error) => {
237540
+ if (error.code === 1) return error.stdout ?? "";
237541
+ throw error;
237542
+ }));
237543
+ const pids = [...new Set(entries.map((e) => e.pid).filter((p) => !!p))];
237544
+ return {
237545
+ cwds: pids.length ? parseLsofCwd(await run("lsof", [
237546
+ "-a",
237547
+ "-p",
237548
+ pids.join(","),
237549
+ "-d",
237550
+ "cwd",
237551
+ "-Fpn"
237552
+ ]).catch((error) => error.stdout ?? "")) : /* @__PURE__ */ new Map(),
237553
+ entries
237554
+ };
237555
+ };
237556
+ const collectLinux = async () => {
237557
+ const sockets = [...parseProcNetTcp(await readFile("/proc/net/tcp", "utf8").catch(() => ""), "v4"), ...parseProcNetTcp(await readFile("/proc/net/tcp6", "utf8").catch(() => ""), "v6")];
237558
+ const wanted = new Set(sockets.map((s) => s.inode));
237559
+ const owner = /* @__PURE__ */ new Map();
237560
+ for (const pidDir of await readdir("/proc").catch(() => [])) {
237561
+ if (!/^\d+$/.test(pidDir)) continue;
237562
+ const fds = await readdir(`/proc/${pidDir}/fd`).catch(() => []);
237563
+ for (const fd of fds) {
237564
+ const target = await readlink(`/proc/${pidDir}/fd/${fd}`).catch(() => "");
237565
+ const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
237566
+ if (inode && wanted.has(inode)) owner.set(inode, Number(pidDir));
237567
+ }
237568
+ }
237569
+ const cwds = /* @__PURE__ */ new Map();
237570
+ const commands = /* @__PURE__ */ new Map();
237571
+ for (const pid of new Set(owner.values())) {
237572
+ const cwd = await readlink(`/proc/${pid}/cwd`).catch(() => void 0);
237573
+ if (cwd) cwds.set(pid, cwd);
237574
+ const comm = await readFile(`/proc/${pid}/comm`, "utf8").catch(() => void 0);
237575
+ if (comm) commands.set(pid, comm.trim());
237576
+ }
237577
+ return {
237578
+ cwds,
237579
+ entries: sockets.map((socket) => {
237580
+ const pid = owner.get(socket.inode);
237581
+ return {
237582
+ address: socket.address,
237583
+ ...pid === void 0 ? {} : {
237584
+ command: commands.get(pid),
237585
+ pid
237586
+ },
237587
+ port: socket.port
237588
+ };
237589
+ })
237590
+ };
237591
+ };
237592
+ const collectWindows = async () => {
237593
+ const [v4, v6, tasks] = await Promise.all([
237594
+ run("netstat", [
237595
+ "-ano",
237596
+ "-p",
237597
+ "TCP"
237598
+ ]),
237599
+ run("netstat", [
237600
+ "-ano",
237601
+ "-p",
237602
+ "TCPv6"
237603
+ ]),
237604
+ run("tasklist", [
237605
+ "/FO",
237606
+ "CSV",
237607
+ "/NH"
237608
+ ]).catch(() => "")
237609
+ ]);
237610
+ const names = parseTasklist(tasks);
237611
+ const entries = [...parseNetstat(v4), ...parseNetstat(v6)].map((entry) => ({
237612
+ ...entry,
237613
+ command: entry.pid === void 0 ? void 0 : names.get(entry.pid)
237614
+ }));
237615
+ return {
237616
+ cwds: /* @__PURE__ */ new Map(),
237617
+ entries
237618
+ };
237619
+ };
237620
+ const listListeningPorts = async (params = {}) => {
237621
+ const collect = process.platform === "darwin" ? collectDarwin : process.platform === "linux" ? collectLinux : process.platform === "win32" ? collectWindows : void 0;
237622
+ if (!collect) return {
237623
+ ports: [],
237624
+ supported: false
237625
+ };
237626
+ try {
237627
+ const { cwds, entries } = await collect();
237628
+ return {
237629
+ ports: summarizeListeners(entries, {
237630
+ cwd: params.cwd ? await realpath(params.cwd).catch(() => params.cwd) : void 0,
237631
+ cwds,
237632
+ ownPid: process.pid
237633
+ }),
237634
+ supported: true
237635
+ };
237636
+ } catch {
237637
+ return {
237638
+ ports: [],
237639
+ supported: false
237640
+ };
237641
+ }
237642
+ };
237643
+
237227
237644
  //#endregion
237228
237645
  //#region ../../packages/device-control/src/projectFileSearchManager.ts
237229
237646
  var import_out = /* @__PURE__ */ __toESM$2(require_out());
@@ -246808,6 +247225,7 @@ const executeDeviceRpc = async (method, params, deps) => {
246808
247225
  case "getKimiCodeQuota": return getKimiCodeQuota(params);
246809
247226
  case "listProjectSkills": return listProjectSkills(params, deps);
246810
247227
  case "prepareSkillDirectory": return prepareSkillDirectory(params, deps);
247228
+ case "listListeningPorts": return listListeningPorts(params);
246811
247229
  case "browseDirectory": return browseDirectory(params);
246812
247230
  case "statPath": return statPath(params);
246813
247231
  case "getProjectFileIndex": return deps.getProjectFileIndex(params);
@@ -248905,7 +249323,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
248905
249323
  const EventEmitter$2 = __require("events");
248906
249324
  const https = __require("https");
248907
249325
  const http$2 = __require("http");
248908
- const net$1 = __require("net");
249326
+ const net$2 = __require("net");
248909
249327
  const tls = __require("tls");
248910
249328
  const { randomBytes: randomBytes$1, createHash: createHash$2 } = __require("crypto");
248911
249329
  const { Duplex: Duplex$3, Readable: Readable$1 } = __require("stream");
@@ -249651,7 +250069,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
249651
250069
  */
249652
250070
  function netConnect(options) {
249653
250071
  options.path = options.socketPath;
249654
- return net$1.connect(options);
250072
+ return net$2.connect(options);
249655
250073
  }
249656
250074
  /**
249657
250075
  * Create a `tls.TLSSocket` and initiate a connection.
@@ -249662,7 +250080,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
249662
250080
  */
249663
250081
  function tlsConnect(options) {
249664
250082
  options.path = void 0;
249665
- if (!options.servername && options.servername !== "") options.servername = net$1.isIP(options.host) ? "" : options.host;
250083
+ if (!options.servername && options.servername !== "") options.servername = net$2.isIP(options.host) ? "" : options.host;
249666
250084
  return tls.connect(options);
249667
250085
  }
249668
250086
  /**
@@ -250445,6 +250863,610 @@ var import_websocket = /* @__PURE__ */ __toESM$2(require_websocket(), 1);
250445
250863
  var import_websocket_server = /* @__PURE__ */ __toESM$2(require_websocket_server(), 1);
250446
250864
  var wrapper_default = import_websocket.default;
250447
250865
 
250866
+ //#endregion
250867
+ //#region ../../packages/device-gateway-client/src/loopback.ts
250868
+ /**
250869
+ * Which loopback address actually answers on a port.
250870
+ *
250871
+ * The gateway pins tunnel targets to `127.0.0.1`, but plenty of dev servers
250872
+ * bind `localhost`, which newer Node resolves to `::1` only — `vite` with no
250873
+ * `--host` does exactly that on macOS. Dialing `127.0.0.1` then gets
250874
+ * ECONNREFUSED for a server that is plainly running. So before a tunnel dials
250875
+ * out, probe the port and fall back to `::1` when IPv4 refuses. Both are
250876
+ * loopback, so the security boundary is unchanged.
250877
+ *
250878
+ * Probing is a TCP connect-and-close, done before the real request so the
250879
+ * request itself (and any streamed body) is only ever sent once. Results are
250880
+ * cached briefly per port; a stale answer costs one failed request, after
250881
+ * which the cache entry is dropped.
250882
+ */
250883
+ const PROBE_TIMEOUT_MS = 1e3;
250884
+ /** Every spelling of loopback a proxy exclusion list might need. */
250885
+ const LOOPBACK_NO_PROXY = [
250886
+ "localhost",
250887
+ "127.0.0.1",
250888
+ "::1",
250889
+ "[::1]"
250890
+ ];
250891
+ /**
250892
+ * Make sure loopback traffic never goes to an HTTP proxy.
250893
+ *
250894
+ * A tunnel only ever dials this machine, but with `HTTP_PROXY` set, Node's
250895
+ * env-proxy mode (`NODE_USE_ENV_PROXY=1`) sends requests to the proxy unless
250896
+ * `NO_PROXY` matches the host — and the common `NO_PROXY=...,::1` does not
250897
+ * match the bracketed `[::1]` a URL carries, so the `::1` fallback below
250898
+ * would land on the proxy and come back as its 502. Adding every loopback
250899
+ * spelling is correct for any process: nothing should proxy loopback.
250900
+ *
250901
+ * Node reads these variables per request, so this takes effect immediately.
250902
+ * Bun snapshots them at startup, so a source run under `bun` with a proxy set
250903
+ * still needs `[::1]` in `NO_PROXY` from the environment.
250904
+ */
250905
+ const ensureLoopbackBypassesProxy = (env = process.env) => {
250906
+ if (![
250907
+ "HTTP_PROXY",
250908
+ "HTTPS_PROXY",
250909
+ "http_proxy",
250910
+ "https_proxy",
250911
+ "ALL_PROXY",
250912
+ "all_proxy"
250913
+ ].some((name) => !!env[name])) return;
250914
+ const entries = [];
250915
+ for (const name of ["NO_PROXY", "no_proxy"]) for (const entry of (env[name] ?? "").split(",")) {
250916
+ const host = entry.trim();
250917
+ if (host && !entries.includes(host)) entries.push(host);
250918
+ }
250919
+ for (const host of LOOPBACK_NO_PROXY) if (!entries.includes(host)) entries.push(host);
250920
+ const merged = entries.join(",");
250921
+ for (const name of ["NO_PROXY", "no_proxy"]) if (env[name] !== merged) env[name] = merged;
250922
+ };
250923
+ const CACHE_TTL_MS = 3e4;
250924
+ const tcpProbe = (host, port) => new Promise((resolve) => {
250925
+ const socket = net.connect({
250926
+ host,
250927
+ port
250928
+ });
250929
+ const done = (ok) => {
250930
+ socket.destroy();
250931
+ resolve(ok);
250932
+ };
250933
+ socket.setTimeout(PROBE_TIMEOUT_MS, () => done(false));
250934
+ socket.once("connect", () => done(true));
250935
+ socket.once("error", () => done(false));
250936
+ });
250937
+ var LoopbackResolver = class {
250938
+ cache = /* @__PURE__ */ new Map();
250939
+ constructor(probe = tcpProbe) {
250940
+ this.probe = probe;
250941
+ }
250942
+ /**
250943
+ * The host to dial for `target`. Anything other than `127.0.0.1` passes
250944
+ * through untouched; for `127.0.0.1` the answer is `::1` only when IPv4
250945
+ * refuses and IPv6 accepts. If neither answers, `127.0.0.1` is returned so
250946
+ * the real request produces the real error.
250947
+ */
250948
+ async resolve(target) {
250949
+ if (target.host !== "127.0.0.1") return target.host;
250950
+ const cached = this.cache.get(target.port);
250951
+ if (cached && cached.expires > Date.now()) return cached.host;
250952
+ let host = "127.0.0.1";
250953
+ if (!await this.probe("127.0.0.1", target.port) && await this.probe("::1", target.port)) host = "::1";
250954
+ this.cache.set(target.port, {
250955
+ expires: Date.now() + CACHE_TTL_MS,
250956
+ host
250957
+ });
250958
+ return host;
250959
+ }
250960
+ /** Forget a port's answer, e.g. after a request to it failed to connect. */
250961
+ forget(port) {
250962
+ this.cache.delete(port);
250963
+ }
250964
+ };
250965
+
250966
+ //#endregion
250967
+ //#region ../../packages/device-gateway-client/src/types.ts
250968
+ /** Bytes per `tunnel_data` frame. */
250969
+ const TUNNEL_CHUNK_SIZE = 64 * 1024;
250970
+ /** Unacked bytes one side may have in flight before it must wait. */
250971
+ const TUNNEL_FLOW_WINDOW = 512 * 1024;
250972
+ /** Largest WebSocket message relayed either way; bigger closes with 1009. */
250973
+ const TUNNEL_WS_MAX_MESSAGE = 1024 * 1024;
250974
+
250975
+ //#endregion
250976
+ //#region ../../packages/device-gateway-client/src/wsTunnel.ts
250977
+ const defaultFactory = (url, protocols) => new wrapper_default(url, protocols, {
250978
+ handshakeTimeout: 1e4,
250979
+ maxPayload: TUNNEL_WS_MAX_MESSAGE
250980
+ });
250981
+ /** Close codes a socket may legally send; anything else becomes 1000/1011. */
250982
+ const sendableCode = (code, fallback = 1e3) => code && (code >= 1e3 && code <= 1003 || code >= 1007 && code <= 1014 || code >= 3e3 && code <= 4999) ? code : fallback;
250983
+ /**
250984
+ * Relay backpressure. Frames to the gateway go through a socket whose `send`
250985
+ * never blocks, so a local app emitting faster than the uplink drains would
250986
+ * grow this process's memory without bound. Above the high mark the upstream
250987
+ * is paused (its TCP window then pushes back on the app) until the queue
250988
+ * drains below the low mark. The hard cap is a backstop for a queue that
250989
+ * keeps growing anyway — it closes the tunnel with 1013 "try again later".
250990
+ */
250991
+ const WS_RELAY_HIGH_WATER = 4 * 1024 * 1024;
250992
+ const WS_RELAY_LOW_WATER = 1024 * 1024;
250993
+ const WS_RELAY_HARD_LIMIT = 32 * 1024 * 1024;
250994
+ const WS_RELAY_DRAIN_POLL_MS = 50;
250995
+ var DeviceWsTunnelHost = class {
250996
+ tunnels = /* @__PURE__ */ new Map();
250997
+ constructor(options) {
250998
+ this.options = options;
250999
+ }
251000
+ get activeCount() {
251001
+ return this.tunnels.size;
251002
+ }
251003
+ open(frame) {
251004
+ const { connId } = frame;
251005
+ if (this.tunnels.has(connId)) return;
251006
+ const tunnel = {
251007
+ closed: false,
251008
+ connId,
251009
+ opened: false
251010
+ };
251011
+ this.tunnels.set(connId, tunnel);
251012
+ this.dial(tunnel, frame);
251013
+ }
251014
+ async dial(tunnel, frame) {
251015
+ const { connId, head, target } = frame;
251016
+ const host = this.options.loopback ? await this.options.loopback.resolve(target) : target.host;
251017
+ if (tunnel.closed) return;
251018
+ const authority = host.includes(":") ? `[${host}]` : host;
251019
+ let socket;
251020
+ try {
251021
+ socket = (this.options.createSocket ?? defaultFactory)(`ws://${authority}:${target.port}${head.path}`, head.protocols);
251022
+ } catch (error) {
251023
+ this.release(tunnel);
251024
+ this.fail(connId, error);
251025
+ return;
251026
+ }
251027
+ tunnel.socket = socket;
251028
+ socket.on("open", () => {
251029
+ tunnel.opened = true;
251030
+ this.options.send({
251031
+ connId,
251032
+ ok: true,
251033
+ ...socket.protocol ? { protocol: socket.protocol } : {},
251034
+ type: "tunnel_ws_open_ack"
251035
+ });
251036
+ });
251037
+ socket.on("message", (data, isBinary) => {
251038
+ if (tunnel.closed) return;
251039
+ this.options.send({
251040
+ ...isBinary ? { binary: true } : {},
251041
+ connId,
251042
+ data: isBinary ? data.toString("base64") : data.toString("utf8"),
251043
+ type: "tunnel_ws_message"
251044
+ });
251045
+ this.applyBackpressure(tunnel);
251046
+ });
251047
+ const forgetHost = () => this.options.loopback?.forget(target.port);
251048
+ socket.on("close", (code, reason) => {
251049
+ if (tunnel.closed) return;
251050
+ this.release(tunnel);
251051
+ if (!tunnel.opened) {
251052
+ forgetHost();
251053
+ this.options.send({
251054
+ connId,
251055
+ error: "UPSTREAM_CLOSED",
251056
+ ok: false,
251057
+ type: "tunnel_ws_open_ack"
251058
+ });
251059
+ return;
251060
+ }
251061
+ this.options.send({
251062
+ code,
251063
+ connId,
251064
+ reason: reason.toString("utf8"),
251065
+ type: "tunnel_ws_close"
251066
+ });
251067
+ });
251068
+ socket.on("error", (error) => {
251069
+ if (tunnel.closed) return;
251070
+ if (!tunnel.opened) {
251071
+ forgetHost();
251072
+ this.release(tunnel);
251073
+ this.fail(connId, error);
251074
+ return;
251075
+ }
251076
+ this.options.logger.warn(`[tunnel] ws ${connId} upstream error: ${error.message}`);
251077
+ });
251078
+ }
251079
+ /** A browser message for the upstream socket. */
251080
+ message(frame) {
251081
+ const tunnel = this.tunnels.get(frame.connId);
251082
+ if (!tunnel?.socket || tunnel.closed || !tunnel.opened) return;
251083
+ tunnel.socket.send(frame.binary ? Buffer$1.from(frame.data, "base64") : frame.data);
251084
+ }
251085
+ /** The browser side closed; close upstream to match. */
251086
+ close(frame) {
251087
+ const tunnel = this.tunnels.get(frame.connId);
251088
+ if (!tunnel || tunnel.closed) return;
251089
+ this.release(tunnel);
251090
+ try {
251091
+ tunnel.socket?.close(sendableCode(frame.code), (frame.reason ?? "").slice(0, 100));
251092
+ } catch {
251093
+ tunnel.socket?.terminate();
251094
+ }
251095
+ }
251096
+ /** Drop every upstream socket, e.g. when the device socket goes away. */
251097
+ closeAll() {
251098
+ for (const tunnel of this.tunnels.values()) {
251099
+ this.release(tunnel);
251100
+ tunnel.socket?.terminate();
251101
+ }
251102
+ }
251103
+ applyBackpressure(tunnel) {
251104
+ const backlog = this.options.backlog?.() ?? 0;
251105
+ if (backlog > 33554432) {
251106
+ this.options.logger.warn(`[tunnel] ws ${tunnel.connId} relay backlog ${backlog}B, closing`);
251107
+ this.release(tunnel);
251108
+ tunnel.socket?.close(1013, "RELAY_BACKLOG");
251109
+ this.options.send({
251110
+ code: 1013,
251111
+ connId: tunnel.connId,
251112
+ reason: "RELAY_BACKLOG",
251113
+ type: "tunnel_ws_close"
251114
+ });
251115
+ return;
251116
+ }
251117
+ if (backlog <= 4194304 || tunnel.drainTimer) return;
251118
+ tunnel.socket?.pause();
251119
+ tunnel.drainTimer = setInterval(() => {
251120
+ if (tunnel.closed) return;
251121
+ if ((this.options.backlog?.() ?? 0) > 1048576) return;
251122
+ clearInterval(tunnel.drainTimer);
251123
+ tunnel.drainTimer = void 0;
251124
+ tunnel.socket?.resume();
251125
+ }, WS_RELAY_DRAIN_POLL_MS);
251126
+ }
251127
+ /** Mark closed, forget it, and stop any drain polling. */
251128
+ release(tunnel) {
251129
+ tunnel.closed = true;
251130
+ this.tunnels.delete(tunnel.connId);
251131
+ if (tunnel.drainTimer) {
251132
+ clearInterval(tunnel.drainTimer);
251133
+ tunnel.drainTimer = void 0;
251134
+ }
251135
+ }
251136
+ fail(connId, error) {
251137
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
251138
+ this.options.send({
251139
+ connId,
251140
+ error: code ?? (error instanceof Error ? error.message : "TUNNEL_WS_OPEN_FAILED"),
251141
+ ok: false,
251142
+ type: "tunnel_ws_open_ack"
251143
+ });
251144
+ }
251145
+ };
251146
+
251147
+ //#endregion
251148
+ //#region ../../packages/device-gateway-client/src/tunnel.ts
251149
+ /**
251150
+ * Device half of the HTTP tunnel: turns the gateway's tunnel frames into a real
251151
+ * HTTP request against a loopback port on this machine and streams the response
251152
+ * back frame by frame.
251153
+ *
251154
+ * The gateway owns the browser-facing HTTP semantics (auth, ingress URLs,
251155
+ * header sanitising) and pins the target host, so this side only has to be a
251156
+ * faithful, well-paced HTTP client.
251157
+ */
251158
+ /** Only loopback targets are servable — defence in depth behind the gateway's pin. */
251159
+ const LOOPBACK_HOSTS$1 = new Set([
251160
+ "127.0.0.1",
251161
+ "::1",
251162
+ "localhost"
251163
+ ]);
251164
+ /**
251165
+ * Response headers that describe the *upstream* transfer and no longer hold
251166
+ * once the body has been decoded and re-framed. `content-encoding` matters
251167
+ * most: `fetch` transparently gunzips, so forwarding it would hand the browser
251168
+ * decoded bytes labelled as compressed.
251169
+ */
251170
+ const RESPONSE_HEADER_DENYLIST = new Set([
251171
+ "connection",
251172
+ "content-encoding",
251173
+ "content-length",
251174
+ "keep-alive",
251175
+ "trailer",
251176
+ "transfer-encoding",
251177
+ "upgrade"
251178
+ ]);
251179
+ const noopLogger$1 = {
251180
+ debug: () => {},
251181
+ error: () => {},
251182
+ info: () => {},
251183
+ warn: () => {}
251184
+ };
251185
+ const describeError = (error) => {
251186
+ if (error && typeof error === "object") {
251187
+ const { cause, code, message } = error;
251188
+ const causeCode = cause && typeof cause === "object" ? cause.code : void 0;
251189
+ return code ?? causeCode ?? message ?? "TUNNEL_ERROR";
251190
+ }
251191
+ return String(error ?? "TUNNEL_ERROR");
251192
+ };
251193
+ var DeviceTunnelHost = class {
251194
+ connections = /* @__PURE__ */ new Map();
251195
+ fetchImpl;
251196
+ logger;
251197
+ maxConcurrent;
251198
+ send;
251199
+ loopback;
251200
+ sockets;
251201
+ constructor(options) {
251202
+ this.fetchImpl = options.fetchImpl ?? ((url, init) => globalThis.fetch(url, init));
251203
+ this.logger = options.logger ?? noopLogger$1;
251204
+ this.maxConcurrent = options.maxConcurrent ?? 32;
251205
+ this.send = options.send;
251206
+ this.loopback = options.loopback ?? new LoopbackResolver();
251207
+ ensureLoopbackBypassesProxy();
251208
+ this.sockets = new DeviceWsTunnelHost({
251209
+ loopback: this.loopback,
251210
+ backlog: options.backlog,
251211
+ createSocket: options.createUpstreamSocket,
251212
+ logger: this.logger,
251213
+ send: options.send
251214
+ });
251215
+ }
251216
+ /** HTTP requests and WebSockets in flight; both count against the limit. */
251217
+ get activeCount() {
251218
+ return this.connections.size + this.sockets.activeCount;
251219
+ }
251220
+ handleFrame(frame) {
251221
+ switch (frame.type) {
251222
+ case "tunnel_open":
251223
+ this.handleOpen(frame);
251224
+ return;
251225
+ case "tunnel_data": {
251226
+ const conn = this.connections.get(frame.connId);
251227
+ if (!conn || conn.closed) return;
251228
+ if (frame.data) {
251229
+ const bytes = Buffer$1.from(frame.data, "base64");
251230
+ if (conn.bodyless) this.send({
251231
+ bytes: bytes.byteLength,
251232
+ connId: frame.connId,
251233
+ type: "tunnel_ack"
251234
+ });
251235
+ else conn.bodyQueue.push(new Uint8Array(bytes));
251236
+ }
251237
+ if (frame.fin) conn.bodyFin = true;
251238
+ this.wakeBody(conn);
251239
+ return;
251240
+ }
251241
+ case "tunnel_ack":
251242
+ this.ack(frame.connId, frame.bytes);
251243
+ return;
251244
+ case "tunnel_close":
251245
+ this.closeConnection(frame.connId, frame.reason ?? "PEER_CLOSED", false);
251246
+ return;
251247
+ case "tunnel_ws_open": {
251248
+ const refusal = this.refuse(frame.target.host);
251249
+ if (refusal) {
251250
+ this.send({
251251
+ connId: frame.connId,
251252
+ error: refusal,
251253
+ ok: false,
251254
+ type: "tunnel_ws_open_ack"
251255
+ });
251256
+ return;
251257
+ }
251258
+ this.sockets.open(frame);
251259
+ return;
251260
+ }
251261
+ case "tunnel_ws_message":
251262
+ this.sockets.message(frame);
251263
+ return;
251264
+ case "tunnel_ws_close":
251265
+ this.sockets.close(frame);
251266
+ return;
251267
+ default: return;
251268
+ }
251269
+ }
251270
+ /** Abort every in-flight tunnel, e.g. when the device socket drops. */
251271
+ closeAll(reason) {
251272
+ for (const connId of this.connections.keys()) this.closeConnection(connId, reason, false);
251273
+ this.sockets.closeAll();
251274
+ }
251275
+ /** Why an open must be refused, or undefined when it may proceed. */
251276
+ refuse(host) {
251277
+ if (!LOOPBACK_HOSTS$1.has(host)) return "TUNNEL_TARGET_NOT_LOOPBACK";
251278
+ if (this.activeCount >= this.maxConcurrent) return "TUNNEL_LIMIT_REACHED";
251279
+ }
251280
+ handleOpen(frame) {
251281
+ const { connId, head, target } = frame;
251282
+ if (this.connections.has(connId)) return;
251283
+ const refusal = this.refuse(target.host);
251284
+ if (refusal) {
251285
+ this.send({
251286
+ connId,
251287
+ error: refusal,
251288
+ ok: false,
251289
+ type: "tunnel_open_ack"
251290
+ });
251291
+ return;
251292
+ }
251293
+ const method = head.method.toUpperCase();
251294
+ const conn = {
251295
+ abort: new AbortController(),
251296
+ bodyFin: false,
251297
+ bodyless: method === "GET" || method === "HEAD",
251298
+ bodyQueue: [],
251299
+ bodyWaiters: [],
251300
+ closed: false,
251301
+ connId,
251302
+ headSent: false,
251303
+ seq: 0,
251304
+ unacked: 0,
251305
+ waiters: []
251306
+ };
251307
+ this.connections.set(connId, conn);
251308
+ this.serve(conn, head, target).catch((error) => {
251309
+ this.logger.warn(`[tunnel] ${connId} failed: ${describeError(error)}`);
251310
+ });
251311
+ }
251312
+ async serve(conn, head, target) {
251313
+ const body = conn.bodyless ? void 0 : new ReadableStream({
251314
+ cancel: () => {
251315
+ conn.bodyQueue.length = 0;
251316
+ this.wakeBody(conn);
251317
+ },
251318
+ pull: async (controller) => {
251319
+ while (conn.bodyQueue.length === 0 && !conn.bodyFin && !conn.closed) await new Promise((resolve) => conn.bodyWaiters.push(resolve));
251320
+ if (conn.closed) throw new Error("TUNNEL_CLOSED");
251321
+ const chunk = conn.bodyQueue.shift();
251322
+ if (!chunk) {
251323
+ controller.close();
251324
+ return;
251325
+ }
251326
+ controller.enqueue(chunk);
251327
+ this.send({
251328
+ bytes: chunk.byteLength,
251329
+ connId: conn.connId,
251330
+ type: "tunnel_ack"
251331
+ });
251332
+ }
251333
+ }, { highWaterMark: 0 });
251334
+ try {
251335
+ const host = await this.loopback.resolve(target);
251336
+ const authority = host.includes(":") ? `[${host}]` : host;
251337
+ const response = await this.fetchImpl(`http://${authority}:${target.port}${head.path}`, {
251338
+ body,
251339
+ ...body ? { duplex: "half" } : {},
251340
+ headers: head.headers,
251341
+ method: head.method.toUpperCase(),
251342
+ redirect: "manual",
251343
+ signal: conn.abort.signal
251344
+ });
251345
+ if (conn.closed) return;
251346
+ this.send({
251347
+ connId: conn.connId,
251348
+ head: {
251349
+ headers: collectResponseHeaders(response.headers),
251350
+ status: response.status
251351
+ },
251352
+ ok: true,
251353
+ type: "tunnel_open_ack"
251354
+ });
251355
+ conn.headSent = true;
251356
+ if (response.body) {
251357
+ const reader = response.body.getReader();
251358
+ try {
251359
+ for (;;) {
251360
+ const { done, value } = await reader.read();
251361
+ if (done) break;
251362
+ if (conn.closed) return;
251363
+ await this.sendBodyChunk(conn, value);
251364
+ }
251365
+ } finally {
251366
+ reader.releaseLock();
251367
+ }
251368
+ }
251369
+ if (conn.closed) return;
251370
+ conn.seq += 1;
251371
+ this.send({
251372
+ connId: conn.connId,
251373
+ data: "",
251374
+ fin: true,
251375
+ seq: conn.seq,
251376
+ type: "tunnel_data"
251377
+ });
251378
+ this.dropConnection(conn.connId);
251379
+ } catch (error) {
251380
+ if (conn.closed) return;
251381
+ const reason = describeError(error);
251382
+ if (!conn.headSent) this.loopback.forget(target.port);
251383
+ if (conn.headSent) this.send({
251384
+ connId: conn.connId,
251385
+ reason,
251386
+ type: "tunnel_close"
251387
+ });
251388
+ else this.send({
251389
+ connId: conn.connId,
251390
+ error: reason,
251391
+ ok: false,
251392
+ type: "tunnel_open_ack"
251393
+ });
251394
+ this.dropConnection(conn.connId);
251395
+ }
251396
+ }
251397
+ /** Split into protocol-sized frames and pace them against the flow window. */
251398
+ async sendBodyChunk(conn, chunk) {
251399
+ for (let offset = 0; offset < chunk.byteLength; offset += TUNNEL_CHUNK_SIZE) {
251400
+ const slice = chunk.subarray(offset, offset + TUNNEL_CHUNK_SIZE);
251401
+ while (conn.unacked >= 524288 && !conn.closed) await new Promise((resolve) => conn.waiters.push(resolve));
251402
+ if (conn.closed) return;
251403
+ conn.seq += 1;
251404
+ conn.unacked += slice.byteLength;
251405
+ this.send({
251406
+ connId: conn.connId,
251407
+ data: Buffer$1.from(slice.buffer, slice.byteOffset, slice.byteLength).toString("base64"),
251408
+ seq: conn.seq,
251409
+ type: "tunnel_data"
251410
+ });
251411
+ }
251412
+ }
251413
+ /** Gateway acked delivered bytes — refill the window and wake parked senders. */
251414
+ ack(connId, bytes) {
251415
+ const conn = this.connections.get(connId);
251416
+ if (!conn) return;
251417
+ conn.unacked = Math.max(0, conn.unacked - bytes);
251418
+ this.wake(conn);
251419
+ }
251420
+ closeConnection(connId, reason, notify) {
251421
+ const conn = this.connections.get(connId);
251422
+ if (!conn) return;
251423
+ conn.closed = true;
251424
+ conn.abort.abort();
251425
+ conn.bodyQueue.length = 0;
251426
+ this.wakeBody(conn);
251427
+ this.wake(conn);
251428
+ this.connections.delete(connId);
251429
+ if (notify) this.send({
251430
+ connId,
251431
+ reason,
251432
+ type: "tunnel_close"
251433
+ });
251434
+ }
251435
+ dropConnection(connId) {
251436
+ const conn = this.connections.get(connId);
251437
+ if (!conn) return;
251438
+ conn.closed = true;
251439
+ this.wake(conn);
251440
+ this.wakeBody(conn);
251441
+ this.connections.delete(connId);
251442
+ }
251443
+ wake(conn) {
251444
+ const waiters = conn.waiters;
251445
+ conn.waiters = [];
251446
+ for (const waiter of waiters) waiter();
251447
+ }
251448
+ wakeBody(conn) {
251449
+ const waiters = conn.bodyWaiters;
251450
+ conn.bodyWaiters = [];
251451
+ for (const waiter of waiters) waiter();
251452
+ }
251453
+ };
251454
+ /**
251455
+ * Flatten response headers, keeping every `set-cookie` separate (the Headers
251456
+ * API folds them into one comma-joined value, which breaks cookie parsing).
251457
+ */
251458
+ const collectResponseHeaders = (headers) => {
251459
+ const out = [];
251460
+ headers.forEach((value, name) => {
251461
+ const lower = name.toLowerCase();
251462
+ if (RESPONSE_HEADER_DENYLIST.has(lower) || lower === "set-cookie") return;
251463
+ out.push([name, value]);
251464
+ });
251465
+ const setCookies = headers.getSetCookie?.() ?? [];
251466
+ for (const cookie of setCookies) out.push(["set-cookie", cookie]);
251467
+ return out;
251468
+ };
251469
+
250448
251470
  //#endregion
250449
251471
  //#region ../../packages/device-gateway-client/src/client.ts
250450
251472
  const DEFAULT_GATEWAY_URL = "https://device-gateway.lobehub.com";
@@ -250467,6 +251489,7 @@ const noopLogger = {
250467
251489
  };
250468
251490
  var GatewayClient = class extends EventEmitter {
250469
251491
  ws = null;
251492
+ tunnelHost = null;
250470
251493
  heartbeatTimer = null;
250471
251494
  reconnectTimer = null;
250472
251495
  connectWatchdogTimer = null;
@@ -250502,6 +251525,11 @@ var GatewayClient = class extends EventEmitter {
250502
251525
  this.logger = options.logger || noopLogger;
250503
251526
  this.autoReconnect = options.autoReconnect ?? true;
250504
251527
  this.connectTimeoutMs = options.connectTimeoutMs ?? CONNECT_TIMEOUT;
251528
+ if (options.tunnel !== false) this.tunnelHost = new DeviceTunnelHost({
251529
+ backlog: () => this.ws?.bufferedAmount ?? 0,
251530
+ logger: this.logger,
251531
+ send: (frame) => this.sendMessage(frame)
251532
+ });
250505
251533
  }
250506
251534
  get connectionStatus() {
250507
251535
  return this.status;
@@ -250682,6 +251710,21 @@ var GatewayClient = class extends EventEmitter {
250682
251710
  this.logger.warn("Received auth_expired from gateway");
250683
251711
  this.emit("auth_expired");
250684
251712
  break;
251713
+ case "tunnel_open":
251714
+ case "tunnel_data":
251715
+ case "tunnel_ack":
251716
+ case "tunnel_close":
251717
+ case "tunnel_ws_open":
251718
+ case "tunnel_ws_message":
251719
+ case "tunnel_ws_close":
251720
+ if (this.tunnelHost) this.tunnelHost.handleFrame(message);
251721
+ else if (message.type === "tunnel_open" || message.type === "tunnel_ws_open") this.sendMessage({
251722
+ connId: message.connId,
251723
+ error: "TUNNEL_DISABLED",
251724
+ ok: false,
251725
+ type: message.type === "tunnel_open" ? "tunnel_open_ack" : "tunnel_ws_open_ack"
251726
+ });
251727
+ break;
250685
251728
  default: this.logger.warn("Unknown message type:", message.type);
250686
251729
  }
250687
251730
  } catch (error) {
@@ -250691,6 +251734,7 @@ var GatewayClient = class extends EventEmitter {
250691
251734
  handleClose = (code, reason) => {
250692
251735
  this.logger.info(`WebSocket closed: code=${code} reason=${reason.toString()}`);
250693
251736
  this.stopHeartbeat();
251737
+ this.tunnelHost?.closeAll("DEVICE_DISCONNECTED");
250694
251738
  this.clearConnectWatchdog();
250695
251739
  this.ws = null;
250696
251740
  if (!this.intentionalDisconnect && this.autoReconnect) {
@@ -250781,6 +251825,7 @@ var GatewayClient = class extends EventEmitter {
250781
251825
  if (this.ws?.readyState === wrapper_default.OPEN) this.ws.send(JSON.stringify(data));
250782
251826
  }
250783
251827
  closeWebSocket() {
251828
+ this.tunnelHost?.closeAll("DEVICE_DISCONNECTED");
250784
251829
  if (!this.ws) return;
250785
251830
  const ws = this.ws;
250786
251831
  const suppressCloseError = (error) => {
@@ -250969,6 +252014,31 @@ const parsePiModelCatalog = (stdout) => {
250969
252014
  return models;
250970
252015
  };
250971
252016
  /**
252017
+ * Parse the JSON emitted by `kimi provider list --json`. The `-m` flag accepts
252018
+ * the model alias (the `models` table key), so the alias is the catalog id.
252019
+ */
252020
+ const parseKimiCodeModelCatalog = (stdout) => {
252021
+ let result;
252022
+ try {
252023
+ result = JSON.parse(stdout);
252024
+ } catch {
252025
+ return [];
252026
+ }
252027
+ if (!isRecord$6(result) || !isRecord$6(result.models)) return [];
252028
+ const models = [];
252029
+ for (const [alias, entry] of Object.entries(result.models)) {
252030
+ if (!alias || !isRecord$6(entry) || typeof entry.model !== "string" || !entry.model) continue;
252031
+ const separatorIndex = alias.indexOf("/");
252032
+ models.push({
252033
+ id: alias,
252034
+ ...typeof entry.displayName === "string" && entry.displayName ? { label: entry.displayName } : void 0,
252035
+ modelId: entry.model,
252036
+ providerId: separatorIndex > 0 ? alias.slice(0, separatorIndex) : alias
252037
+ });
252038
+ }
252039
+ return models;
252040
+ };
252041
+ /**
250972
252042
  * Parse the one-column table emitted by `qodercli --list-models`.
250973
252043
  * Built-in models are selected by name; custom models append their modelID in
250974
252044
  * parentheses and must be selected by that id.
@@ -251050,7 +252120,11 @@ const listHeterogeneousAgentModels = async (params) => {
251050
252120
  "list",
251051
252121
  "--format",
251052
252122
  "json"
251053
- ] : params.type === "grok-build" || params.type === "opencode" ? ["models"] : ["--list-models"];
252123
+ ] : params.type === "grok-build" || params.type === "opencode" ? ["models"] : params.type === "kimi-code" ? [
252124
+ "provider",
252125
+ "list",
252126
+ "--json"
252127
+ ] : ["--list-models"];
251054
252128
  const spawnPlan = await resolveCliSpawnPlan(resolved.command, args);
251055
252129
  const { stderr, stdout } = await execFilePromise(spawnPlan.command, spawnPlan.args, {
251056
252130
  cwd: params.cwd,
@@ -251077,7 +252151,7 @@ const listHeterogeneousAgentModels = async (params) => {
251077
252151
  };
251078
252152
  }
251079
252153
  return {
251080
- models: params.type === "cursor" ? parseCursorModelCatalog(String(stdout)) : params.type === "devin" ? parseDevinModelCatalog(String(stdout)) : params.type === "grok-build" ? parseGrokBuildModelCatalog(String(stdout)) : params.type === "pi" ? parsePiModelCatalog(String(stdout)) : params.type === "qoder" ? parseQoderModelCatalog(String(stdout)) : parseOpenCodeModelCatalog(String(stdout)),
252154
+ models: params.type === "cursor" ? parseCursorModelCatalog(String(stdout)) : params.type === "devin" ? parseDevinModelCatalog(String(stdout)) : params.type === "grok-build" ? parseGrokBuildModelCatalog(String(stdout)) : params.type === "kimi-code" ? parseKimiCodeModelCatalog(String(stdout)) : params.type === "pi" ? parsePiModelCatalog(String(stdout)) : params.type === "qoder" ? parseQoderModelCatalog(String(stdout)) : parseOpenCodeModelCatalog(String(stdout)),
251081
252155
  status: "success",
251082
252156
  updatedAt
251083
252157
  };
@@ -286195,7 +287269,7 @@ async function verifyWindowsWfpEgress(opts = {}) {
286195
287269
  if (!target) {
286196
287270
  const [lo, hi] = opts.proxyPortRange ?? DEFAULT_WINDOWS_PROXY_PORT_RANGE;
286197
287271
  for (let i = 0; i < 5; i++) {
286198
- const s = net.createServer();
287272
+ const s = net$1.createServer();
286199
287273
  s.listen(0, "127.0.0.1");
286200
287274
  await once(s, "listening");
286201
287275
  const p = s.address().port;
@@ -292510,7 +293584,7 @@ async function resolveToken(options) {
292510
293584
  }
292511
293585
  }
292512
293586
  const result = await getValidToken();
292513
- if (result) {
293587
+ if (result.status === "ok") {
292514
293588
  log$12.debug("Using stored credentials");
292515
293589
  const { credentials } = result;
292516
293590
  const serverUrl = resolveServerUrl();
@@ -292526,7 +293600,8 @@ async function resolveToken(options) {
292526
293600
  userId
292527
293601
  };
292528
293602
  }
292529
- log$12.error(`No authentication found. Run '${"lh"} login' first, or set ${CLI_API_KEY_ENV}, or provide --token.`);
293603
+ const report = describeTokenLookup(result);
293604
+ log$12.error(report ? `${report.detail} ${report.fix}` : `No authentication found. Run '${"lh"} login' first, or set ${CLI_API_KEY_ENV}, or provide --token.`);
292530
293605
  process.exit(1);
292531
293606
  }
292532
293607
 
@@ -506431,10 +507506,14 @@ async function probeCredential(ctx) {
506431
507506
  }
506432
507507
  if (source.kind === "stored") {
506433
507508
  const refreshed = await getValidToken();
506434
- if (!refreshed) return {
506435
- ...base,
506436
- error: source.token ? "the stored token has expired and could not be refreshed" : "no stored login on this machine"
506437
- };
507509
+ if (refreshed.status !== "ok") {
507510
+ const report = describeTokenLookup(refreshed);
507511
+ return {
507512
+ ...base,
507513
+ error: report?.detail ?? (source.token ? "the stored token has expired and could not be refreshed" : "no stored login on this machine"),
507514
+ fix: report?.fix
507515
+ };
507516
+ }
506438
507517
  const payload = parseJwtPayload(refreshed.credentials.accessToken);
506439
507518
  return {
506440
507519
  ...base,
@@ -506545,9 +507624,9 @@ const credentialValidity = {
506545
507624
  userId: credential.userId
506546
507625
  };
506547
507626
  if (credential.error) return {
506548
- detail: `${credential.origin} cannot be used: ${credential.error}.`,
507627
+ detail: `${credential.origin} cannot be used: ${credential.error.replace(/\.$/, "")}.`,
506549
507628
  evidence,
506550
- fix: credential.kind === "env-api-key" ? `Issue a fresh key and re-export ${CLI_API_KEY_ENV}.` : `Run '${"lh"} login' again.`,
507629
+ fix: credential.fix ?? (credential.kind === "env-api-key" ? `Issue a fresh key and re-export ${CLI_API_KEY_ENV}.` : `Run '${"lh"} login' again.`),
506551
507630
  status: "fail"
506552
507631
  };
506553
507632
  if (!credential.userId) return {
@@ -506978,6 +508057,7 @@ const gatewayHandshake = {
506978
508057
  autoReconnect: false,
506979
508058
  gatewayUrl,
506980
508059
  serverUrl: credential.serverUrl,
508060
+ tunnel: false,
506981
508061
  token: credential.token,
506982
508062
  tokenType: credential.tokenType,
506983
508063
  userId: credential.userId
@@ -526172,7 +527252,8 @@ function registerLoginCommand(program) {
526172
527252
  if (apiKey) try {
526173
527253
  await getUserIdFromApiKey(apiKey, serverUrl);
526174
527254
  const existingSettings = loadSettings();
526175
- saveSettings(existingSettings?.serverUrl === serverUrl ? {
527255
+ const existingServerUrl = existingSettings?.serverUrl || OFFICIAL_SERVER_URL;
527256
+ saveSettings(!!existingSettings && existingServerUrl === serverUrl ? {
526176
527257
  gatewayUrl: existingSettings.gatewayUrl,
526177
527258
  serverUrl
526178
527259
  } : { serverUrl });
@@ -526257,7 +527338,8 @@ function registerLoginCommand(program) {
526257
527338
  refreshToken: body.refresh_token
526258
527339
  });
526259
527340
  const existingSettings = loadSettings();
526260
- saveSettings(existingSettings?.serverUrl === serverUrl ? {
527341
+ const existingServerUrl = existingSettings?.serverUrl || OFFICIAL_SERVER_URL;
527342
+ saveSettings(!!existingSettings && existingServerUrl === serverUrl ? {
526261
527343
  gatewayUrl: existingSettings.gatewayUrl,
526262
527344
  serverUrl
526263
527345
  } : { serverUrl });
@@ -526489,11 +527571,7 @@ function registerMemoryCommand(program) {
526489
527571
  const client = await getTrpcClient();
526490
527572
  const categoriesToFetch = category ? [category] : [...CATEGORIES];
526491
527573
  const allResults = {};
526492
- for (const cat of categoriesToFetch) try {
526493
- allResults[cat] = await fetchCategory(client, cat);
526494
- } catch {
526495
- allResults[cat] = [];
526496
- }
527574
+ for (const cat of categoriesToFetch) allResults[cat] = await fetchCategory(client, cat);
526497
527575
  if (options.json !== void 0) {
526498
527576
  const fields = typeof options.json === "string" ? options.json : void 0;
526499
527577
  outputJson(category ? allResults[category] : allResults, fields);
@@ -528564,6 +529642,7 @@ function registerStatusCommand(program) {
528564
529642
  gatewayUrl: gatewayUrl || OFFICIAL_GATEWAY_URL,
528565
529643
  logger: log$12,
528566
529644
  serverUrl: auth.serverUrl,
529645
+ tunnel: false,
528567
529646
  token: auth.token,
528568
529647
  tokenType: auth.tokenType,
528569
529648
  userId: auth.userId
@@ -530938,8 +532017,70 @@ function ensureAcceptanceDirIgnoredFor(roundDir) {
530938
532017
  //#endregion
530939
532018
  //#region src/utils/skillWiring.ts
530940
532019
  const AGENTS_SKILLS_DIR = path.join(".agents", "skills");
530941
- function detectClaudeHarness(baseDir) {
530942
- return existsSync(path.join(baseDir, "CLAUDE.md")) || existsSync(path.join(baseDir, ".claude"));
532020
+ /**
532021
+ * Harness dirs wired onto `.agents/skills`. Two reasons a dir is listed:
532022
+ *
532023
+ * - the harness does not scan `.agents/skills` natively (Claude Code's
532024
+ * `.claude/skills`, OpenCode's `.opencode/skill`, Windsurf's
532025
+ * `.windsurf/skills`, Roo Code's `.roo/skills`), or
532026
+ * - another harness compatibility-reads it (Cursor reads `.codex/skills` and
532027
+ * `.claude/skills`, so a repo already carrying `.codex` config gets wired).
532028
+ *
532029
+ * Codex, Cursor, Gemini CLI, Copilot/VS Code and Amp all read `.agents/skills`
532030
+ * directly (Cursor additionally reads its own `.cursor/skills`, wired above).
532031
+ * `.github` is deliberately absent: it exists in almost every repo (workflows)
532032
+ * and Copilot already reads `.agents/skills`, so wiring it would be pure noise.
532033
+ */
532034
+ const HARNESSES = [
532035
+ {
532036
+ dir: ".claude",
532037
+ name: "claude",
532038
+ signals: ["CLAUDE.md", ".claude"],
532039
+ skillsSubdir: "skills"
532040
+ },
532041
+ {
532042
+ dir: ".codex",
532043
+ name: "codex",
532044
+ signals: [".codex"],
532045
+ skillsSubdir: "skills"
532046
+ },
532047
+ {
532048
+ dir: ".cursor",
532049
+ name: "cursor",
532050
+ signals: [".cursor"],
532051
+ skillsSubdir: "skills"
532052
+ },
532053
+ {
532054
+ dir: ".gemini",
532055
+ name: "gemini",
532056
+ signals: [".gemini", "GEMINI.md"],
532057
+ skillsSubdir: "skills"
532058
+ },
532059
+ {
532060
+ dir: ".opencode",
532061
+ name: "opencode",
532062
+ signals: [
532063
+ ".opencode",
532064
+ "opencode.json",
532065
+ "opencode.jsonc"
532066
+ ],
532067
+ skillsSubdir: "skill"
532068
+ },
532069
+ {
532070
+ dir: ".roo",
532071
+ name: "roo",
532072
+ signals: [".roo"],
532073
+ skillsSubdir: "skills"
532074
+ },
532075
+ {
532076
+ dir: ".windsurf",
532077
+ name: "windsurf",
532078
+ signals: [".windsurf"],
532079
+ skillsSubdir: "skills"
532080
+ }
532081
+ ];
532082
+ function detectHarness(baseDir, harness) {
532083
+ return harness.signals.some((signal) => existsSync(path.join(baseDir, signal)));
530943
532084
  }
530944
532085
  function isSymlink(target) {
530945
532086
  try {
@@ -530948,18 +532089,13 @@ function isSymlink(target) {
530948
532089
  return false;
530949
532090
  }
530950
532091
  }
530951
- /**
530952
- * `.agents/skills` is the single materialized copy; every other harness dir is a
530953
- * symlink onto it, so one install/update reaches all of them.
530954
- */
530955
- function linkHarnessSkills(baseDir, skillId) {
530956
- if (!detectClaudeHarness(baseDir)) return { kind: "none" };
530957
- const claudeDir = path.join(baseDir, ".claude");
530958
- const link = path.join(claudeDir, "skills");
530959
- const rel = path.join(".claude", "skills");
532092
+ function linkOneHarness(baseDir, skillId, harness) {
532093
+ const harnessDir = path.join(baseDir, harness.dir);
532094
+ const link = path.join(harnessDir, harness.skillsSubdir);
532095
+ const rel = path.join(harness.dir, harness.skillsSubdir);
530960
532096
  if (isSymlink(link)) {
530961
532097
  const current = readlinkSync(link);
530962
- if (path.resolve(claudeDir, current) === path.join(baseDir, AGENTS_SKILLS_DIR)) return {
532098
+ if (path.resolve(harnessDir, current) === path.join(baseDir, AGENTS_SKILLS_DIR)) return {
530963
532099
  kind: "already",
530964
532100
  link: rel
530965
532101
  };
@@ -530969,7 +532105,7 @@ function linkHarnessSkills(baseDir, skillId) {
530969
532105
  reason: `already a symlink to ${current} — leaving it alone`
530970
532106
  };
530971
532107
  }
530972
- mkdirSync(claudeDir, { recursive: true });
532108
+ mkdirSync(harnessDir, { recursive: true });
530973
532109
  if (existsSync(link)) {
530974
532110
  const single = path.join(link, skillId);
530975
532111
  if (isSymlink(single) || existsSync(single)) return {
@@ -531008,6 +532144,15 @@ function linkHarnessSkills(baseDir, skillId) {
531008
532144
  target
531009
532145
  };
531010
532146
  }
532147
+ /**
532148
+ * `.agents/skills` is the single materialized copy; every detected harness dir
532149
+ * is a symlink onto it, so one install/update reaches all of them.
532150
+ */
532151
+ function linkHarnessSkills(baseDir, skillId) {
532152
+ const detected = HARNESSES.filter((harness) => detectHarness(baseDir, harness));
532153
+ if (detected.length === 0) return [{ kind: "none" }];
532154
+ return detected.map((harness) => linkOneHarness(baseDir, skillId, harness));
532155
+ }
531011
532156
 
531012
532157
  //#endregion
531013
532158
  //#region ../../packages/utils/src/verify/interactionCost.ts
@@ -531832,6 +532977,84 @@ function evidenceDescriptionForFile(description, file) {
531832
532977
  return description?.trim() || (file ? path.basename(file) : void 0);
531833
532978
  }
531834
532979
 
532980
+ //#endregion
532981
+ //#region src/commands/acceptanceEvidence.ts
532982
+ /** Upload and attach independently: a failed artifact must not discard the report. */
532983
+ async function uploadReportEvidence(client, params) {
532984
+ const failedEvidence = [];
532985
+ const types = /* @__PURE__ */ new Set();
532986
+ let count = 0;
532987
+ let inlined = 0;
532988
+ for (const input of reportEvidence(params.evidence)) {
532989
+ const absolutePath = path.resolve(params.dir, input.path);
532990
+ const type = evidenceTypeForFile(absolutePath);
532991
+ const description = evidenceDescriptionForFile(input.description, absolutePath);
532992
+ const metadata = input.comparison ? { comparison: input.comparison } : void 0;
532993
+ let fileId;
532994
+ let missing = false;
532995
+ try {
532996
+ try {
532997
+ await access(absolutePath);
532998
+ } catch (error) {
532999
+ missing = error.code === "ENOENT";
533000
+ throw error;
533001
+ }
533002
+ const content = inlineTextEvidenceForFile(absolutePath, type);
533003
+ if (content === void 0) fileId = (await uploadLocalFile(client, absolutePath)).id;
533004
+ await client.verify.uploadEvidence.mutate({
533005
+ capturedBy: "cli",
533006
+ checkResultId: params.checkResultId,
533007
+ content,
533008
+ description,
533009
+ fileId,
533010
+ metadata,
533011
+ type
533012
+ });
533013
+ types.add(type);
533014
+ count += 1;
533015
+ if (content !== void 0) inlined += 1;
533016
+ } catch (error) {
533017
+ const message = error instanceof Error ? error.message : String(error);
533018
+ const reason = missing ? "file_missing" : message.includes("storage_block:") ? "storage_quota" : "upload_failed";
533019
+ const args = [
533020
+ "--check",
533021
+ params.checkResultId,
533022
+ "--type",
533023
+ type,
533024
+ ...fileId ? ["--file-id", fileId] : ["--file", absolutePath],
533025
+ "--desc",
533026
+ description,
533027
+ ...metadata ? ["--metadata", JSON.stringify(metadata)] : []
533028
+ ];
533029
+ const retryCommand = `lh acceptance run evidence upload ${args.map((arg) => `'${arg.replaceAll("'", "'\\''")}'`).join(" ")}`;
533030
+ failedEvidence.push({
533031
+ checkResultId: params.checkResultId,
533032
+ error: message,
533033
+ fileId,
533034
+ path: absolutePath,
533035
+ reason,
533036
+ retryArgs: [
533037
+ "acceptance",
533038
+ "run",
533039
+ "evidence",
533040
+ "upload",
533041
+ ...args
533042
+ ],
533043
+ retryCommand,
533044
+ retryCommandShell: "posix",
533045
+ type
533046
+ });
533047
+ log$12.warn(`evidence not published: ${path.basename(absolutePath)}: ${message}`);
533048
+ }
533049
+ }
533050
+ return {
533051
+ count,
533052
+ failedEvidence,
533053
+ inlined,
533054
+ types
533055
+ };
533056
+ }
533057
+
531835
533058
  //#endregion
531836
533059
  //#region src/commands/acceptanceRun.ts
531837
533060
  const listMaterializedFiles = (directory) => {
@@ -531874,11 +533097,13 @@ async function installAction(options) {
531874
533097
  removed.push(relativePath.split(path.sep).join("/"));
531875
533098
  }
531876
533099
  }
531877
- const link = linkHarnessSkills(baseDir, bundle.identifier);
533100
+ const links = linkHarnessSkills(baseDir, bundle.identifier);
533101
+ const link = links.find((l) => "link" in l && l.link.startsWith(".claude")) ?? { kind: "none" };
531878
533102
  const result = {
531879
533103
  dir: skillDir,
531880
533104
  ignored: ensureAcceptanceDirIgnored(baseDir),
531881
533105
  link,
533106
+ links,
531882
533107
  removed,
531883
533108
  skill: bundle.identifier,
531884
533109
  skipped,
@@ -531894,11 +533119,11 @@ async function installAction(options) {
531894
533119
  console.log(`${import_picocolors.default.green("✓")} ${import_picocolors.default.bold(bundle.name)}${versionLabel} skill → ${import_picocolors.default.dim(path.relative(process.cwd(), skillDir) || skillDir)}`);
531895
533120
  console.log(` ${written.length} written${skipped.length ? `, ${skipped.length} skipped` : ""}${removed.length ? `, ${removed.length} stale removed` : ""}`);
531896
533121
  if (skipped.length > 0) console.log(import_picocolors.default.dim(` (skipped existing — pass --force to overwrite)`));
531897
- printWiring(link);
533122
+ printWiring(links);
531898
533123
  }
531899
- function printWiring(link) {
533124
+ function printWiring(links) {
531900
533125
  const arrow = import_picocolors.default.dim(" ↳");
531901
- switch (link.kind) {
533126
+ for (const link of links) switch (link.kind) {
531902
533127
  case "linked":
531903
533128
  case "linked-single":
531904
533129
  console.log(`${arrow} linked ${link.link} → ${import_picocolors.default.dim(link.target)}`);
@@ -531909,7 +533134,9 @@ function printWiring(link) {
531909
533134
  case "skipped":
531910
533135
  console.log(`${arrow} ${import_picocolors.default.yellow(`skipped ${link.link}: ${link.reason}`)}`);
531911
533136
  break;
531912
- default: break;
533137
+ default:
533138
+ console.log(`${arrow} ${import_picocolors.default.dim("no harness dirs detected — agents that read .agents/skills pick it up automatically")}`);
533139
+ break;
531913
533140
  }
531914
533141
  }
531915
533142
  async function runCreateAction(options) {
@@ -532070,12 +533297,17 @@ async function decisionAction(resultId, decision) {
532070
533297
  console.log(`${import_picocolors.default.green("✓")} Recorded ${import_picocolors.default.bold(decision)} on result ${import_picocolors.default.bold(resultId)}`);
532071
533298
  }
532072
533299
  async function evidenceUploadAction(options) {
532073
- if (Boolean(options.file) === Boolean(options.content)) {
532074
- log$12.error("Provide exactly one of --file or --content");
533300
+ if ([
533301
+ options.file,
533302
+ options.content,
533303
+ options.fileId
533304
+ ].filter(Boolean).length !== 1) {
533305
+ log$12.error("Provide exactly one of --file, --file-id or --content");
532075
533306
  process.exit(1);
532076
533307
  }
533308
+ const metadata = options.metadata ? JSON.parse(options.metadata) : void 0;
532077
533309
  const client = await getTrpcClient();
532078
- let fileId;
533310
+ let fileId = options.fileId;
532079
533311
  let inlineContent = options.content;
532080
533312
  if (options.file) {
532081
533313
  inlineContent = inlineTextEvidenceForFile(options.file, options.type);
@@ -532087,6 +533319,7 @@ async function evidenceUploadAction(options) {
532087
533319
  content: inlineContent,
532088
533320
  description: evidenceDescriptionForFile(options.desc, options.file),
532089
533321
  fileId,
533322
+ metadata,
532090
533323
  type: options.type
532091
533324
  });
532092
533325
  if (options.json !== void 0) {
@@ -532243,6 +533476,17 @@ async function ingestReportAction(reportDir, options) {
532243
533476
  if (requestedAcceptanceId) {
532244
533477
  const bundle = await client.acceptance.getBundle.query({ id: requestedAcceptanceId });
532245
533478
  acceptance = bundle.acceptance;
533479
+ const currentWorkspaceId = resolveWorkspaceId();
533480
+ const targetWorkspaceId = acceptance.workspaceId ?? void 0;
533481
+ if (currentWorkspaceId !== targetWorkspaceId) {
533482
+ const current = currentWorkspaceId ? `workspace "${currentWorkspaceId}"` : "personal space";
533483
+ const target = targetWorkspaceId ? `workspace "${targetWorkspaceId}"` : "personal space";
533484
+ const hint = targetWorkspaceId ? `Set LOBEHUB_WORKSPACE_ID=${targetWorkspaceId} for this command and retry.` : "Unset LOBEHUB_WORKSPACE_ID and run 'lh workspace use --personal', then retry.";
533485
+ throw new Error(`Acceptance "${acceptance.id}" belongs to ${target}, but the CLI is using ${current}. ${hint} No run was created.`);
533486
+ }
533487
+ if (targetWorkspaceId) {
533488
+ if ((await client.workspace.getById.query())?.id !== targetWorkspaceId) throw new Error(`The server did not resolve workspace "${targetWorkspaceId}" for this account. Check your access with 'lh workspace list' before retrying. No run was created.`);
533489
+ }
532246
533490
  plan = plan?.map((item) => ({
532247
533491
  ...item,
532248
533492
  sourceCriterionId: item.sourceCriterionId ?? bundle.checks?.find((check) => check.id === item.id || check.planItem?.id === item.id)?.planItem?.sourceCriterionId ?? void 0
@@ -532280,6 +533524,7 @@ async function ingestReportAction(reportDir, options) {
532280
533524
  acceptanceId,
532281
533525
  verifyRunId: run.id
532282
533526
  });
533527
+ plan = attached?.plan ?? plan;
532283
533528
  const runId = attached?.id ?? run.id;
532284
533529
  if (runId !== run.id) console.log(import_picocolors.default.dim(`Folded into the acceptance's draft round ${attached.roundIndex ?? ""}`));
532285
533530
  const roundIndex = attached?.roundIndex ?? null;
@@ -532288,11 +533533,14 @@ async function ingestReportAction(reportDir, options) {
532288
533533
  const seenCheckItemIds = /* @__PURE__ */ new Set();
532289
533534
  let evidenceCount = 0;
532290
533535
  let inlined = 0;
533536
+ const failedEvidence = [];
533537
+ const missingEvidence = [];
533538
+ const publishedVerdicts = [];
532291
533539
  for (const [index, { case: c, checkItemId }] of cases.entries()) {
532292
533540
  seenCheckItemIds.add(checkItemId);
532293
533541
  const verdict = toVerdict(c.result ?? c.status ?? c.verdict);
532294
533542
  const observation = c.keyObservation ?? c.observation ?? c.note;
532295
- const checkResult = await client.verify.ingestResult.mutate({
533543
+ const checkInput = {
532296
533544
  checkItemId,
532297
533545
  checkItemIndex: index,
532298
533546
  checkItemTitle: c.name ?? c.case ?? c.title ?? checkItemId,
@@ -532303,48 +533551,66 @@ async function ingestReportAction(reportDir, options) {
532303
533551
  verdict,
532304
533552
  verifierType: "agent",
532305
533553
  verifyRunId: runId
533554
+ };
533555
+ const uploaded = await uploadReportEvidence(client, {
533556
+ checkResultId: (await client.verify.ingestResult.mutate(checkInput)).id,
533557
+ dir,
533558
+ evidence: c.evidence
532306
533559
  });
532307
- for (const evidenceInput of reportEvidence(c.evidence)) {
532308
- const rel = evidenceInput.path;
532309
- const abs = path.isAbsolute(rel) ? rel : path.join(dir, rel);
532310
- if (!existsSync(abs)) {
532311
- log$12.warn(`evidence not found, skipping: ${rel}`);
532312
- continue;
532313
- }
532314
- try {
532315
- const type = evidenceTypeForFile(abs);
532316
- const inlineContent = inlineTextEvidenceForFile(abs, type);
532317
- const file = inlineContent === void 0 ? await uploadLocalFile(client, abs) : void 0;
532318
- await client.verify.uploadEvidence.mutate({
532319
- capturedBy: "cli",
532320
- checkResultId: checkResult.id,
532321
- content: inlineContent,
532322
- description: evidenceDescriptionForFile(evidenceInput.description, abs),
532323
- fileId: file?.id,
532324
- metadata: evidenceInput.comparison ? { comparison: evidenceInput.comparison } : void 0,
532325
- type
532326
- });
532327
- evidenceCount += 1;
532328
- if (inlineContent !== void 0) inlined += 1;
532329
- } catch (e) {
532330
- log$12.warn(`evidence upload failed, skipping ${path.basename(abs)}: ${String(e)}`);
532331
- }
533560
+ evidenceCount += uploaded.count;
533561
+ inlined += uploaded.inlined;
533562
+ failedEvidence.push(...uploaded.failedEvidence.map((failure) => ({
533563
+ ...failure,
533564
+ checkItemId
533565
+ })));
533566
+ const required = (plan?.find((item) => item.id === checkItemId)?.verifierConfig)?.requiredEvidence;
533567
+ const gaps = [...new Set(required?.map((spec) => spec.type) ?? [])].filter((type) => !uploaded.types.has(type));
533568
+ const publishedVerdict = gaps.length > 0 && verdict === "passed" ? "uncertain" : verdict;
533569
+ publishedVerdicts.push(publishedVerdict);
533570
+ if (gaps.length > 0) {
533571
+ missingEvidence.push({
533572
+ checkItemId,
533573
+ types: gaps
533574
+ });
533575
+ const limitation = `Required evidence not published: ${gaps.join(", ")}.`;
533576
+ log$12.warn(`${checkItemId}: ${limitation}`);
533577
+ await client.verify.ingestResult.mutate({
533578
+ ...checkInput,
533579
+ toulmin: {
533580
+ ...checkInput.toulmin,
533581
+ limitation
533582
+ },
533583
+ verdict: publishedVerdict
533584
+ });
532332
533585
  }
532333
533586
  }
533587
+ const unexecuted = plan?.filter((item) => !seenCheckItemIds.has(item.id)) ?? [];
533588
+ for (const item of unexecuted) {
533589
+ const config = item.verifierConfig;
533590
+ const types = [...new Set(config.requiredEvidence?.map((spec) => spec.type) ?? [])];
533591
+ if (types.length === 0) continue;
533592
+ missingEvidence.push({
533593
+ checkItemId: item.id,
533594
+ types
533595
+ });
533596
+ publishedVerdicts.push("uncertain");
533597
+ log$12.warn(`${item.id}: not executed; required evidence not published: ${types.join(", ")}.`);
533598
+ }
532334
533599
  const conclusion = typeof summary.conclusion === "string" ? summary.conclusion : typeof summary.note === "string" ? summary.note : void 0;
532335
533600
  const score = typeof summary.score === "number" ? Math.max(0, Math.min(1, summary.score / 100)) : void 0;
532336
- const recount = cases.length !== allCases.length;
532337
- const verdicts = cases.map(({ case: c }) => toVerdict(c.result ?? c.status ?? c.verdict));
532338
- const counted = (verdict) => verdicts.filter((v) => v === verdict).length;
533601
+ const recount = cases.length !== allCases.length || missingEvidence.length > 0;
533602
+ const counted = (verdict) => publishedVerdicts.filter((v) => v === verdict).length;
533603
+ const derivedVerdict = deriveReportVerdict(publishedVerdicts.map((verdict) => ({ verdict })));
533604
+ const reportVerdict = summary.verdict && cases.length === allCases.length ? toVerdict(summary.verdict) : derivedVerdict;
532339
533605
  await client.verify.upsertReport.mutate({
532340
533606
  content,
532341
533607
  failedChecks: recount ? counted("failed") : summary.failed,
532342
533608
  overallConfidence: score,
532343
533609
  passedChecks: recount ? counted("passed") : summary.passed,
532344
533610
  summary: conclusion,
532345
- totalChecks: recount ? cases.length : summary.total ?? cases.length,
533611
+ totalChecks: recount ? publishedVerdicts.length : summary.total ?? cases.length,
532346
533612
  uncertainChecks: recount ? counted("uncertain") || void 0 : (summary.blocked ?? 0) + (summary.uncertain ?? 0) || void 0,
532347
- verdict: summary.verdict && !recount ? toVerdict(summary.verdict) : deriveReportVerdict(cases.map(({ case: c }) => c)),
533613
+ verdict: missingEvidence.length > 0 ? reportVerdict === "failed" || derivedVerdict === "failed" ? "failed" : "uncertain" : reportVerdict,
532348
533614
  verifyRunId: runId
532349
533615
  });
532350
533616
  let proposalPosted = false;
@@ -532362,6 +533628,13 @@ async function ingestReportAction(reportDir, options) {
532362
533628
  log$12.warn(`proposal not posted to the discussion: ${String(e)}`);
532363
533629
  }
532364
533630
  const unplanned = plan?.length ? [...seenCheckItemIds].filter((id) => !plan.some((item) => item.id === id)) : [];
533631
+ const partial = failedEvidence.length > 0 || missingEvidence.length > 0;
533632
+ if (partial) {
533633
+ process.exitCode = 1;
533634
+ log$12.warn("Report saved, but evidence publication is incomplete. Keep the local artifacts; retry only the missing evidence, not the whole ingest. Supplementing evidence does not change recorded verdicts.");
533635
+ if (missingEvidence.some(({ checkItemId }) => !seenCheckItemIds.has(checkItemId))) log$12.warn("Unexecuted checks have no result to attach evidence to. Execute them and publish a new round on the same acceptance; do not re-ingest this unchanged report.");
533636
+ if (failedEvidence.some((failure) => failure.reason === "storage_quota")) log$12.warn("Acceptance evidence uses your personal file storage quota. Free space or upgrade your storage plan, then retry the failed artifacts.");
533637
+ }
532365
533638
  if (options.json !== void 0) {
532366
533639
  outputJson({
532367
533640
  acceptanceId,
@@ -532369,32 +533642,37 @@ async function ingestReportAction(reportDir, options) {
532369
533642
  cases: cases.length,
532370
533643
  droppedProgrammaticChecks: droppedLabels,
532371
533644
  evidence: evidenceCount,
533645
+ failedEvidence,
532372
533646
  inlined,
533647
+ missingEvidence,
532373
533648
  origin,
532374
533649
  planItems: plan?.length ?? 0,
532375
533650
  proposalPosted,
533651
+ publicationStatus: partial ? "partial" : "complete",
532376
533652
  pullRequest,
532377
533653
  roundIndex,
532378
533654
  roundUrl,
532379
533655
  scenario,
532380
533656
  subject: subject.ref,
533657
+ unexecuted: unexecuted.map((item) => item.id),
532381
533658
  unplanned,
532382
533659
  verifyRunId: runId
532383
533660
  }, typeof options.json === "string" ? options.json : void 0);
532384
533661
  return;
532385
533662
  }
532386
- console.log(`${import_picocolors.default.green("✓")} Ingested ${import_picocolors.default.bold(String(cases.length))} case(s), ${import_picocolors.default.bold(String(evidenceCount))} evidence artifact(s)${inlined > 0 ? `, ${import_picocolors.default.bold(String(inlined))} inline` : ""}${droppedLabels.length > 0 ? import_picocolors.default.yellow(` — ${droppedLabels.length} programmatic-test check(s) dropped`) : ""}`);
532387
- if (plan?.length) {
532388
- const unexecuted = plan.filter((item) => !seenCheckItemIds.has(item.id));
532389
- console.log(`${import_picocolors.default.bold("plan")}: ${plan.length} item(s)${unexecuted.length > 0 ? import_picocolors.default.yellow(` — ${unexecuted.length} planned but not executed`) : ""}${unplanned.length > 0 ? import_picocolors.default.dim(` — ${unplanned.length} unplanned case(s)`) : ""}`);
533663
+ console.log(`${partial ? import_picocolors.default.yellow("⚠ Partially published") : import_picocolors.default.green("✓ Ingested")} ${import_picocolors.default.bold(String(cases.length))} case(s), ${import_picocolors.default.bold(String(evidenceCount))} evidence artifact(s)${inlined > 0 ? `, ${import_picocolors.default.bold(String(inlined))} inline` : ""}${droppedLabels.length > 0 ? import_picocolors.default.yellow(` — ${droppedLabels.length} programmatic-test check(s) dropped`) : ""}`);
533664
+ for (const failure of failedEvidence) {
533665
+ console.log(`${import_picocolors.default.yellow("retry (POSIX shell)")}: ${failure.retryCommand}`);
533666
+ console.log(`${import_picocolors.default.dim("retryArgs (lh, shell disabled)")}: ${JSON.stringify(failure.retryArgs)}`);
532390
533667
  }
533668
+ if (plan?.length) console.log(`${import_picocolors.default.bold("plan")}: ${plan.length} item(s)${unexecuted.length > 0 ? import_picocolors.default.yellow(` — ${unexecuted.length} planned but not executed`) : ""}${unplanned.length > 0 ? import_picocolors.default.dim(` — ${unplanned.length} unplanned case(s)`) : ""}`);
532391
533669
  if (proposalPosted) console.log(`${import_picocolors.default.bold("proposal")}: posted to the discussion`);
532392
533670
  if (pullRequest?.url) console.log(`${import_picocolors.default.bold("pr")}: ${pullRequest.url}`);
532393
533671
  if (origin?.topicId) console.log(`${import_picocolors.default.bold("origin topic")}: ${origin.topicId}`);
532394
533672
  console.log(`${import_picocolors.default.bold("verifyRunId")}: ${runId} ${import_picocolors.default.dim("(immutable snapshot)")}`);
532395
533673
  const subjectLabel = subject.ref.subjectType === "standalone" ? "standalone" : `${subject.ref.subjectType}:${subject.ref.subjectId}`;
532396
533674
  console.log(`${import_picocolors.default.bold("acceptance")}: ${acceptanceId} ${import_picocolors.default.dim(`(${subjectLabel})`)}`);
532397
- if (options.open) {
533675
+ if (options.open || partial) {
532398
533676
  console.log(`${import_picocolors.default.bold("open acceptance")}: ${acceptanceUrl}`);
532399
533677
  if (roundUrl) console.log(`${import_picocolors.default.bold("round snapshot")}: ${roundUrl}`);
532400
533678
  }
@@ -532418,7 +533696,7 @@ function withSubmitOptions(cmd) {
532418
533696
  return cmd.option("--run <verifyRunId>", "Target verification session (or use --operation)").option("--operation <operationId>", "Resolve the session from an Agent Run operation id").requiredOption("--item <checkItemId>", "Plan item id (checkItemId)").option("--type <type>", "screenshot|gif|video|text|dom_snapshot|transcript").option("--file <path>", "Local file to upload as the evidence artifact").option("--content <text>", "Inline text payload (instead of a file)").option("--verdict <verdict>", "passed|failed|uncertain").option("--title <text>", "Check item title snapshot").option("--by <capturedBy>", "agent-browser|cdp|cli|program|llm_judge", "cli").option("--desc <text>", "Human-readable caption for the evidence").option("--json [fields]", "Output JSON");
532419
533697
  }
532420
533698
  function withEvidenceUploadOptions(cmd) {
532421
- return cmd.requiredOption("--check <checkResultId>", "Target check result id").requiredOption("--type <type>", "screenshot|gif|video|text|dom_snapshot|transcript").option("--file <path>", "Local file to upload as the artifact").option("--content <text>", "Inline text payload (instead of a file)").option("--by <capturedBy>", "agent-browser|cdp|cli|program|llm_judge", "cli").option("--desc <text>", "Human-readable caption").option("--json [fields]", "Output JSON");
533699
+ return cmd.requiredOption("--check <checkResultId>", "Target check result id").requiredOption("--type <type>", "screenshot|gif|video|text|dom_snapshot|transcript").option("--file <path>", "Local file to upload as the artifact").option("--file-id <id>", "Attach an already-uploaded file without uploading it again").option("--content <text>", "Inline text payload (instead of a file)").option("--metadata <json>", "Evidence metadata, preserved when retrying an attachment").option("--by <capturedBy>", "agent-browser|cdp|cli|program|llm_judge", "cli").option("--desc <text>", "Human-readable caption").option("--json [fields]", "Output JSON");
532422
533700
  }
532423
533701
  function withReportUpsertOptions(cmd) {
532424
533702
  return cmd.requiredOption("--run <verifyRunId>", "Target session id").option("--verdict <verdict>", "passed|failed|uncertain").option("--summary <text>", "Short summary").option("--content <markdown>", "Full markdown body").option("--total <n>", "Total checks").option("--passed <n>", "Passed checks").option("--failed <n>", "Failed checks").option("--uncertain <n>", "Uncertain checks").option("--json [fields]", "Output JSON");
@@ -532491,6 +533769,45 @@ function attachDeprecatedVerifyRunAliases(verify) {
532491
533769
  report.command("get <runId>").description("Deprecated — use `lh acceptance run report get`").option("--json [fields]", "Output JSON").hook("preAction", () => log$12.warn("`lh verify report get` is deprecated — use `lh acceptance run report get`")).action(reportGetAction);
532492
533770
  }
532493
533771
 
533772
+ //#endregion
533773
+ //#region src/commands/acceptanceCommentFeedback.ts
533774
+ /** Discussion lives outside getBundle; its threads are resolved explicitly, not by a new round. */
533775
+ const collectCommentFeedback = (items, checks, evidenceLabels) => {
533776
+ const commentsById = new Map(items.map((item) => [item.id, item]));
533777
+ const checksById = new Map(checks.map((check) => [check.id, check]));
533778
+ return items.flatMap((item) => {
533779
+ if (item.kind !== "comment" || item.deletedAt) return [];
533780
+ const root = item.parentCommentId ? commentsById.get(item.parentCommentId) : item;
533781
+ if (!root) return [];
533782
+ const check = root.checkItemId ? checksById.get(root.checkItemId) : void 0;
533783
+ const annotation = root.evidenceId && root.rect ? {
533784
+ comment: item.content,
533785
+ evidenceId: root.evidenceId,
533786
+ rect: root.rect
533787
+ } : void 0;
533788
+ return [{
533789
+ actionable: !root.resolvedAt,
533790
+ annotations: annotation ? [{
533791
+ ...annotation,
533792
+ region: formatAnnotationRegion(annotation, evidenceLabels)
533793
+ }] : void 0,
533794
+ attachments: item.attachments,
533795
+ author: item.author,
533796
+ checkId: root.checkItemId ?? void 0,
533797
+ checkSeq: check?.seq,
533798
+ comment: item.content,
533799
+ commentId: item.id,
533800
+ createdAt: item.createdAt.toISOString(),
533801
+ fileIds: item.attachments.map((attachment) => attachment.id),
533802
+ kind: "comment",
533803
+ parentCommentId: item.parentCommentId ?? void 0,
533804
+ roundIndex: item.contextRoundIndex ?? root.contextRoundIndex ?? 0,
533805
+ threadId: root.id,
533806
+ title: check?.title
533807
+ }];
533808
+ });
533809
+ };
533810
+
532494
533811
  //#endregion
532495
533812
  //#region src/commands/acceptanceFlow.ts
532496
533813
  /** Agent entry point: author a graph before exercising the product, then append real visits. */
@@ -532720,9 +534037,10 @@ function registerAcceptanceCommands(parent, options) {
532720
534037
  "CREATED"
532721
534038
  ]);
532722
534039
  });
532723
- acceptance.command("feedback <idOrSubject>").description("The user's review feedback — check rejects (with region notes & attachments) plus group-scoped notes").option("--actionable", "Only standing feedback the next repair round must act on").option("--json [fields]", "Output JSON").action(async (idOrSubject, options) => {
534040
+ acceptance.command("feedback <idOrSubject>").description("The user's review feedback — unresolved comments, region notes, attachments and rejection decisions").option("--actionable", "Only standing feedback the next repair round must act on").option("--json [fields]", "Output JSON").action(async (idOrSubject, options) => {
532724
534041
  const id = await resolveAcceptanceId(idOrSubject);
532725
- const bundle = await (await getTrpcClient()).acceptance.getBundle.query({ id });
534042
+ const client = await getTrpcClient();
534043
+ const [bundle, comments] = await Promise.all([client.acceptance.getBundle.query({ id }), client.acceptanceComment.list.query({ acceptanceId: id })]);
532726
534044
  const currentRoundIndex = bundle.rounds.at(-1)?.run.roundIndex ?? 0;
532727
534045
  const evidenceLabels = /* @__PURE__ */ new Map();
532728
534046
  for (const check of bundle.checks) {
@@ -532771,9 +534089,17 @@ function registerAcceptanceCommands(parent, options) {
532771
534089
  });
532772
534090
  }
532773
534091
  }
532774
- for (const round of bundle.rounds) for (const entry of round.run.decisionDetail?.groupFeedback ?? []) {
534092
+ for (const round of bundle.rounds) {
534093
+ const { decisionDetail, userDecision } = round.run;
532775
534094
  const roundIndex = round.run.roundIndex ?? 0;
532776
- entries.push({
534095
+ if (userDecision === "reject" && decisionDetail?.comment?.trim()) entries.push({
534096
+ actionable: roundIndex >= currentRoundIndex,
534097
+ comment: decisionDetail.comment,
534098
+ createdAt: decisionDetail.decidedAt,
534099
+ kind: "decision",
534100
+ roundIndex
534101
+ });
534102
+ for (const entry of round.run.decisionDetail?.groupFeedback ?? []) entries.push({
532777
534103
  actionable: roundIndex >= currentRoundIndex,
532778
534104
  category: entry.category,
532779
534105
  comment: entry.comment,
@@ -532783,6 +534109,7 @@ function registerAcceptanceCommands(parent, options) {
532783
534109
  roundIndex
532784
534110
  });
532785
534111
  }
534112
+ entries.push(...collectCommentFeedback(comments.items, bundle.checks, evidenceLabels));
532786
534113
  entries.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
532787
534114
  const shown = options.actionable ? entries.filter((entry) => entry.actionable) : entries;
532788
534115
  if (options.json !== void 0) {
@@ -532799,14 +534126,15 @@ function registerAcceptanceCommands(parent, options) {
532799
534126
  }
532800
534127
  for (const entry of shown) {
532801
534128
  const marker = entry.actionable ? import_picocolors.default.red("▶ actionable") : import_picocolors.default.dim("· addressed");
532802
- const label = entry.kind === "check" ? `C${entry.checkSeq} ${truncate$2(entry.title ?? "", 60)}` : entry.kind === "flow" ? `flow · ${entry.title}` : `group · ${entry.category || "overall"}`;
534129
+ const label = entry.kind === "decision" ? "overall rejection" : entry.kind === "comment" ? `comment ${entry.commentId} · ${entry.checkSeq === void 0 ? "overall" : `C${entry.checkSeq} ${truncate$2(entry.title ?? "", 60)}`}` : entry.kind === "check" ? `C${entry.checkSeq} ${truncate$2(entry.title ?? "", 60)}` : entry.kind === "flow" ? `flow · ${entry.title}` : `group · ${entry.category || "overall"}`;
532803
534130
  console.log(`${marker} ${label} ${import_picocolors.default.dim(`(r${entry.roundIndex})`)}`);
532804
534131
  if (entry.comment) console.log(` ${entry.comment}`);
532805
534132
  for (const annotation of entry.annotations ?? []) {
532806
- if (annotation.comment) console.log(` ${import_picocolors.default.dim("region:")} ${annotation.comment}`);
534133
+ if (annotation.comment && annotation.comment !== entry.comment) console.log(` ${import_picocolors.default.dim("region:")} ${annotation.comment}`);
532807
534134
  if (annotation.region) console.log(` ${import_picocolors.default.dim(`└ ${annotation.region}`)}`);
532808
534135
  }
532809
534136
  if (entry.fileIds?.length) console.log(` ${import_picocolors.default.dim(`attachments: ${entry.fileIds.join(", ")}`)}`);
534137
+ for (const attachment of entry.attachments ?? []) if (attachment.url) console.log(` ${attachment.name ?? attachment.id}: ${attachment.url}`);
532810
534138
  }
532811
534139
  });
532812
534140
  acceptance.command("accept <idOrSubject>").description("Accept the delivery — the terminal user decision that closes the acceptance").option("--comment <text>", "Optional note recorded with the decision").option("--json [fields]", "Output JSON").action(async (idOrSubject, options) => {
@@ -532832,6 +534160,14 @@ function registerAcceptanceCommands(parent, options) {
532832
534160
  return;
532833
534161
  }
532834
534162
  console.log(`${import_picocolors.default.red("✗")} Delivery rejected (${result.id}) — next round re-opens it`);
534163
+ const dispatch = result.repairDispatch;
534164
+ if (dispatch?.dispatched) console.log(`${import_picocolors.default.green("↻")} Sent back to agent ${dispatch.agentId} in topic ${dispatch.topicId} (operation ${dispatch.operationId})`);
534165
+ else if (dispatch?.reason === "failed") console.log(`${import_picocolors.default.yellow("!")} Repair dispatch failed: ${dispatch.error ?? "unknown error"}`);
534166
+ else if (dispatch?.reason === "goal_coordinator") console.log(import_picocolors.default.dim("Goal task — its coordinator starts the next attempt."));
534167
+ else {
534168
+ const why = dispatch?.reason === "forbidden" ? "Not allowed to run the source agent" : "No source agent to send it back to";
534169
+ console.log(import_picocolors.default.dim(`${why} — hand the repair over with: lh acceptance feedback ${result.id} --actionable`));
534170
+ }
532835
534171
  });
532836
534172
  if (!options?.deprecated) attachAcceptanceRunCommands(acceptance);
532837
534173
  }