@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/README.md +32 -0
- package/dist/index.js +1498 -162
- package/man/man1/lh.1 +1 -1
- package/package.json +2 -1
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
|
|
20150
|
-
if (credentials.expiresAt && Date.now() / 1e3 < credentials.expiresAt - bufferSeconds) return {
|
|
20151
|
-
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
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 (
|
|
20381
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
20494
|
-
|
|
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
|
|
40427
|
-
|
|
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/
|
|
42920
|
-
|
|
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:
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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"] :
|
|
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
|
-
|
|
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 (
|
|
506435
|
-
|
|
506436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
530942
|
-
|
|
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
|
-
|
|
530953
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
533122
|
+
printWiring(links);
|
|
531898
533123
|
}
|
|
531899
|
-
function printWiring(
|
|
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:
|
|
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 (
|
|
532074
|
-
|
|
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
|
|
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
|
-
|
|
532308
|
-
|
|
532309
|
-
|
|
532310
|
-
|
|
532311
|
-
|
|
532312
|
-
|
|
532313
|
-
|
|
532314
|
-
|
|
532315
|
-
|
|
532316
|
-
|
|
532317
|
-
|
|
532318
|
-
|
|
532319
|
-
|
|
532320
|
-
|
|
532321
|
-
|
|
532322
|
-
|
|
532323
|
-
|
|
532324
|
-
|
|
532325
|
-
|
|
532326
|
-
|
|
532327
|
-
|
|
532328
|
-
|
|
532329
|
-
|
|
532330
|
-
|
|
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
|
|
532338
|
-
const
|
|
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 ?
|
|
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:
|
|
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("✓")}
|
|
532387
|
-
|
|
532388
|
-
|
|
532389
|
-
console.log(`${import_picocolors.default.
|
|
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 —
|
|
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
|
|
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)
|
|
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
|
}
|