@lobehub/cli 0.0.64 → 0.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +531 -39
- package/man/man1/lh.1 +1 -1
- package/package.json +28 -27
package/dist/index.js
CHANGED
|
@@ -292909,18 +292909,35 @@ async function runCommand$1({ command, cwd, description, env: extraEnv, run_in_b
|
|
|
292909
292909
|
|
|
292910
292910
|
//#endregion
|
|
292911
292911
|
//#region src/auth/apiKey.ts
|
|
292912
|
+
var ApiKeyValidationError = class extends Error {
|
|
292913
|
+
constructor(message, retryable) {
|
|
292914
|
+
super(message);
|
|
292915
|
+
this.retryable = retryable;
|
|
292916
|
+
this.name = "ApiKeyValidationError";
|
|
292917
|
+
}
|
|
292918
|
+
};
|
|
292912
292919
|
async function getUserIdFromApiKey(apiKey, serverUrl) {
|
|
292913
292920
|
const normalizedServerUrl = normalizeUrl(serverUrl) || resolveServerUrl();
|
|
292914
|
-
|
|
292921
|
+
let response;
|
|
292922
|
+
try {
|
|
292923
|
+
response = await fetch(`${normalizedServerUrl}/api/v1/users/me?includeCount=0`, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
292924
|
+
} catch (error) {
|
|
292925
|
+
throw new ApiKeyValidationError(error instanceof Error ? error.message : String(error), true);
|
|
292926
|
+
}
|
|
292927
|
+
const retryableStatus = response.status >= 500 || [
|
|
292928
|
+
408,
|
|
292929
|
+
425,
|
|
292930
|
+
429
|
|
292931
|
+
].includes(response.status);
|
|
292915
292932
|
let body;
|
|
292916
292933
|
try {
|
|
292917
292934
|
body = await response.json();
|
|
292918
292935
|
} catch {
|
|
292919
|
-
throw new
|
|
292936
|
+
throw new ApiKeyValidationError(`Failed to parse response from ${normalizedServerUrl}/api/v1/users/me?includeCount=0.`, response.ok || retryableStatus);
|
|
292920
292937
|
}
|
|
292921
|
-
if (!response.ok || body?.success === false) throw new
|
|
292938
|
+
if (!response.ok || body?.success === false) throw new ApiKeyValidationError(body?.error || body?.message || `Request failed with status ${response.status}.`, retryableStatus);
|
|
292922
292939
|
const userId = body?.data?.id || body?.data?.userId;
|
|
292923
|
-
if (!userId) throw new
|
|
292940
|
+
if (!userId) throw new ApiKeyValidationError("Current user response did not include a user id.", true);
|
|
292924
292941
|
return userId;
|
|
292925
292942
|
}
|
|
292926
292943
|
|
|
@@ -292998,7 +293015,7 @@ async function resolveToken(options) {
|
|
|
292998
293015
|
} catch (error) {
|
|
292999
293016
|
const message = error instanceof Error ? error.message : String(error);
|
|
293000
293017
|
log$13.error(`Failed to validate ${source.origin}: ${message}`);
|
|
293001
|
-
process.exit(1);
|
|
293018
|
+
process.exit(error instanceof ApiKeyValidationError && error.retryable ? options.unavailableExitCode ?? 1 : 1);
|
|
293002
293019
|
}
|
|
293003
293020
|
}
|
|
293004
293021
|
const result = await getValidToken();
|
|
@@ -293020,7 +293037,7 @@ async function resolveToken(options) {
|
|
|
293020
293037
|
}
|
|
293021
293038
|
const report = describeTokenLookup(result);
|
|
293022
293039
|
log$13.error(report ? `${report.detail} ${report.fix}` : `No authentication found. Run '${"lh"} login' first, or set ${CLI_API_KEY_ENV}, or provide --token.`);
|
|
293023
|
-
process.exit(1);
|
|
293040
|
+
process.exit(result.status === "unavailable" ? options.unavailableExitCode ?? 1 : 1);
|
|
293024
293041
|
}
|
|
293025
293042
|
|
|
293026
293043
|
//#endregion
|
|
@@ -296378,6 +296395,23 @@ var TerminalSessionManager = class {
|
|
|
296378
296395
|
const SERVICE_NAME = CLI_CONNECT_SERVICE_NAME;
|
|
296379
296396
|
const ENV_NAME_PATTERN = /^[A-Z_]\w*$/i;
|
|
296380
296397
|
const SERVICE_ENV_FILE_NAME = "connect-service.env";
|
|
296398
|
+
/**
|
|
296399
|
+
* Exit code for workspace failures worth retrying (EX_TEMPFAIL). It sits outside the
|
|
296400
|
+
* unit's `SuccessExitStatus`, so `Restart=on-failure` restarts the service child.
|
|
296401
|
+
*/
|
|
296402
|
+
const CONNECT_RETRYABLE_EXIT_CODE = 75;
|
|
296403
|
+
const PERMANENT_TRPC_ERROR_CODES = new Set([
|
|
296404
|
+
"BAD_REQUEST",
|
|
296405
|
+
"CONFLICT",
|
|
296406
|
+
"FORBIDDEN",
|
|
296407
|
+
"NOT_FOUND",
|
|
296408
|
+
"UNAUTHORIZED"
|
|
296409
|
+
]);
|
|
296410
|
+
/** Exit code for a failed workspace operation (token mint, refresh, device registration): 1 for refusals, 75 otherwise. */
|
|
296411
|
+
function getWorkspaceFailureExitCode(error) {
|
|
296412
|
+
const code = error?.data?.code;
|
|
296413
|
+
return code && PERMANENT_TRPC_ERROR_CODES.has(code) ? 1 : 75;
|
|
296414
|
+
}
|
|
296381
296415
|
function systemctl(args, stdio = "pipe") {
|
|
296382
296416
|
const output = execFileSync("systemctl", ["--user", ...args], {
|
|
296383
296417
|
encoding: "utf8",
|
|
@@ -296403,7 +296437,13 @@ function getExecErrorMessage(error) {
|
|
|
296403
296437
|
if (stderr) return stderr;
|
|
296404
296438
|
return error instanceof Error ? error.message : String(error);
|
|
296405
296439
|
}
|
|
296406
|
-
function
|
|
296440
|
+
function buildServiceConnectArgs(options) {
|
|
296441
|
+
const args = ["connect", "--service-child"];
|
|
296442
|
+
if (options.workspace) args.push("--workspace", options.workspace);
|
|
296443
|
+
if (options.public) args.push("--public");
|
|
296444
|
+
return args;
|
|
296445
|
+
}
|
|
296446
|
+
function renderUnit(options) {
|
|
296407
296447
|
const quoteSystemdValue = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
296408
296448
|
const escapeSystemdEnvValue = (value) => value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
296409
296449
|
const homeDir = os.homedir();
|
|
@@ -296414,16 +296454,14 @@ function renderUnit() {
|
|
|
296414
296454
|
const execStart = [
|
|
296415
296455
|
process.execPath,
|
|
296416
296456
|
scriptPath,
|
|
296417
|
-
|
|
296418
|
-
"--service-child"
|
|
296457
|
+
...buildServiceConnectArgs(options)
|
|
296419
296458
|
].map(quoteSystemdValue).join(" ");
|
|
296420
296459
|
return [
|
|
296421
296460
|
"[Unit]",
|
|
296422
296461
|
`Description=${CLI_DISPLAY_NAME} connect service`,
|
|
296423
296462
|
"After=network-online.target",
|
|
296424
296463
|
"Wants=network-online.target",
|
|
296425
|
-
"StartLimitIntervalSec=
|
|
296426
|
-
"StartLimitBurst=10",
|
|
296464
|
+
"StartLimitIntervalSec=0",
|
|
296427
296465
|
"",
|
|
296428
296466
|
"[Service]",
|
|
296429
296467
|
"Type=simple",
|
|
@@ -296436,7 +296474,7 @@ function renderUnit() {
|
|
|
296436
296474
|
`StandardError=append:${logPath}`,
|
|
296437
296475
|
"Restart=on-failure",
|
|
296438
296476
|
"SuccessExitStatus=0 1 2",
|
|
296439
|
-
"RestartSec=
|
|
296477
|
+
"RestartSec=5",
|
|
296440
296478
|
"TimeoutStopSec=20",
|
|
296441
296479
|
"",
|
|
296442
296480
|
"[Install]",
|
|
@@ -296493,7 +296531,8 @@ function assertConnectServiceAuthAvailable() {
|
|
|
296493
296531
|
if (process.env.LOBEHUB_JWT || readCliApiKeyEnv() || loadCredentials()) return;
|
|
296494
296532
|
throw new Error(`No authentication found. Run '${"lh"} login' first, or set ${CLI_API_KEY_ENV} before starting the connect service.`);
|
|
296495
296533
|
}
|
|
296496
|
-
function installConnectService() {
|
|
296534
|
+
function installConnectService(options = {}) {
|
|
296535
|
+
if (options.public && !options.workspace) throw new Error("--public requires --workspace.");
|
|
296497
296536
|
requireLinuxSystemd();
|
|
296498
296537
|
assertNoConnectDaemonRunning();
|
|
296499
296538
|
assertConnectServiceAuthAvailable();
|
|
@@ -296502,13 +296541,10 @@ function installConnectService() {
|
|
|
296502
296541
|
mode: 448,
|
|
296503
296542
|
recursive: true
|
|
296504
296543
|
});
|
|
296505
|
-
fs.writeFileSync(path.join(getUserServiceDir(), SERVICE_NAME), renderUnit(), { mode: 420 });
|
|
296544
|
+
fs.writeFileSync(path.join(getUserServiceDir(), SERVICE_NAME), renderUnit(options), { mode: 420 });
|
|
296506
296545
|
systemctl(["daemon-reload"], "inherit");
|
|
296507
|
-
systemctl([
|
|
296508
|
-
|
|
296509
|
-
"--now",
|
|
296510
|
-
SERVICE_NAME
|
|
296511
|
-
], "inherit");
|
|
296546
|
+
systemctl(["enable", SERVICE_NAME], "inherit");
|
|
296547
|
+
systemctl(["restart", SERVICE_NAME], "inherit");
|
|
296512
296548
|
}
|
|
296513
296549
|
function uninstallConnectService() {
|
|
296514
296550
|
const unitPath = path.join(getUserServiceDir(), SERVICE_NAME);
|
|
@@ -508017,7 +508053,7 @@ const sweepLocalTraces = async () => {
|
|
|
508017
508053
|
const SHUTDOWN_METRICS_FLUSH_MS = 3e3;
|
|
508018
508054
|
const CONNECT_SERVICE_NAME = CLI_CONNECT_SERVICE_NAME;
|
|
508019
508055
|
function registerConnectCommand(program) {
|
|
508020
|
-
const connectCmd = program.command("connect").description("Connect to the device gateway and listen for tool calls").option("--token <jwt>", "JWT access token").option("--gateway <url>", "Device gateway URL").option("--device-id <id>", "Device ID (auto-generated if not provided)").option("--workspace <id>", "Enroll as a device of this workspace (
|
|
508056
|
+
const connectCmd = program.command("connect").description("Connect to the device gateway and listen for tool calls").option("--token <jwt>", "JWT access token").option("--gateway <url>", "Device gateway URL").option("--device-id <id>", "Device ID (auto-generated if not provided)").option("--workspace <id>", "Enroll as a device of this workspace (workspace members)").option("--public", "With --workspace: enroll into the shared pool visible to every member (default: private, visible only to you)").option("-v, --verbose", "Enable verbose logging").option("-d, --daemon", "Run as a background daemon process").option("--daemon-child", "Internal: runs as the daemon child process").option("--service-child", "Internal: runs as the system service child process").action(async (options) => {
|
|
508021
508057
|
if (options.verbose) setVerbose(true);
|
|
508022
508058
|
if (options.daemon) return handleDaemonStart(options);
|
|
508023
508059
|
const isServiceChild = options.serviceChild || process.env.LOBEHUB_CONNECT_SERVICE === "1";
|
|
@@ -508063,9 +508099,12 @@ function registerConnectCommand(program) {
|
|
|
508063
508099
|
});
|
|
508064
508100
|
});
|
|
508065
508101
|
const serviceCmd = connectCmd.command("service").description("Manage the Linux user systemd connect service");
|
|
508066
|
-
serviceCmd.command("install").description("Install and start the Linux user systemd connect service").action(() => {
|
|
508067
|
-
installConnectService(
|
|
508068
|
-
|
|
508102
|
+
serviceCmd.command("install").description("Install and start the Linux user systemd connect service").option("--workspace <id>", "Enroll as a device of this workspace (workspace members)").option("--public", "With --workspace: enroll into the shared pool visible to every member (default: private, visible only to you)").action((options) => {
|
|
508103
|
+
installConnectService({
|
|
508104
|
+
public: options.public,
|
|
508105
|
+
workspace: options.workspace
|
|
508106
|
+
});
|
|
508107
|
+
log$13.info(options.workspace ? `Installed and started ${CONNECT_SERVICE_NAME} for workspace ${options.workspace}.` : `Installed and started ${CONNECT_SERVICE_NAME}.`);
|
|
508069
508108
|
log$13.info("Run 'lh connect service status' to inspect the service.");
|
|
508070
508109
|
});
|
|
508071
508110
|
serviceCmd.command("uninstall").description("Remove the Linux user systemd connect service").action(() => {
|
|
@@ -508136,7 +508175,11 @@ function buildDaemonArgs(options) {
|
|
|
508136
508175
|
return args;
|
|
508137
508176
|
}
|
|
508138
508177
|
async function runConnect(options, isDaemonChild) {
|
|
508139
|
-
|
|
508178
|
+
const unavailableExitCode = options.workspace ? 75 : void 0;
|
|
508179
|
+
let auth = await resolveToken({
|
|
508180
|
+
...options,
|
|
508181
|
+
unavailableExitCode
|
|
508182
|
+
});
|
|
508140
508183
|
const settings = loadSettings();
|
|
508141
508184
|
const gatewayUrl = normalizeUrl(options.gateway) || settings?.gatewayUrl;
|
|
508142
508185
|
if (!gatewayUrl && settings?.serverUrl) {
|
|
@@ -508154,11 +508197,17 @@ async function runConnect(options, isDaemonChild) {
|
|
|
508154
508197
|
let connectToken = auth.token;
|
|
508155
508198
|
let connectTokenType = auth.tokenType;
|
|
508156
508199
|
if (workspaceId) {
|
|
508157
|
-
|
|
508200
|
+
try {
|
|
508201
|
+
connectToken = (await mintWorkspaceConnectToken(auth, workspaceId)).token;
|
|
508202
|
+
} catch (err) {
|
|
508203
|
+
log$13.error(`Workspace connect token mint failed: ${err.message}`);
|
|
508204
|
+
process.exit(getWorkspaceFailureExitCode(err));
|
|
508205
|
+
throw new Error("process.exit", { cause: err });
|
|
508206
|
+
}
|
|
508158
508207
|
connectTokenType = "jwt";
|
|
508159
508208
|
}
|
|
508160
508209
|
const refreshConnectToken = async () => {
|
|
508161
|
-
const refreshed = await resolveToken({});
|
|
508210
|
+
const refreshed = await resolveToken({ unavailableExitCode });
|
|
508162
508211
|
if (!refreshed) return void 0;
|
|
508163
508212
|
auth = refreshed;
|
|
508164
508213
|
if (workspaceId) {
|
|
@@ -508397,6 +508446,7 @@ async function runConnect(options, isDaemonChild) {
|
|
|
508397
508446
|
let cancelRefreshTimer = startProactiveRefresh();
|
|
508398
508447
|
let authFailedRefreshAttempted = false;
|
|
508399
508448
|
client.on("auth_failed", async (reason) => {
|
|
508449
|
+
let exitCode = 1;
|
|
508400
508450
|
if (connectTokenType === "jwt" && !authFailedRefreshAttempted) {
|
|
508401
508451
|
authFailedRefreshAttempted = true;
|
|
508402
508452
|
info(`Authentication failed (${reason}). Attempting token refresh...`);
|
|
@@ -508411,16 +508461,20 @@ async function runConnect(options, isDaemonChild) {
|
|
|
508411
508461
|
await client.reconnect();
|
|
508412
508462
|
return;
|
|
508413
508463
|
}
|
|
508414
|
-
} catch {
|
|
508464
|
+
} catch (err) {
|
|
508465
|
+
error(`Token refresh failed: ${err.message}`);
|
|
508466
|
+
if (workspaceId) exitCode = getWorkspaceFailureExitCode(err);
|
|
508467
|
+
}
|
|
508415
508468
|
}
|
|
508416
508469
|
error(`Authentication failed: ${reason}`);
|
|
508417
508470
|
error(`Run '${"lh"} login', or set ${CLI_API_KEY_ENV} and run '${"lh"} login --server <url>' to configure API key authentication.`);
|
|
508418
508471
|
cleanup();
|
|
508419
|
-
process.exit(
|
|
508472
|
+
process.exit(exitCode);
|
|
508420
508473
|
});
|
|
508421
508474
|
client.on("auth_expired", async () => {
|
|
508422
508475
|
if (connectTokenType === "apiKey") return;
|
|
508423
508476
|
info("Authentication expired. Attempting to refresh token...");
|
|
508477
|
+
let exitCode = 1;
|
|
508424
508478
|
try {
|
|
508425
508479
|
const newToken = await refreshConnectToken();
|
|
508426
508480
|
if (newToken) {
|
|
@@ -508430,10 +508484,13 @@ async function runConnect(options, isDaemonChild) {
|
|
|
508430
508484
|
await client.reconnect();
|
|
508431
508485
|
return;
|
|
508432
508486
|
}
|
|
508433
|
-
} catch {
|
|
508487
|
+
} catch (err) {
|
|
508488
|
+
error(`Token refresh failed: ${err.message}`);
|
|
508489
|
+
if (workspaceId) exitCode = getWorkspaceFailureExitCode(err);
|
|
508490
|
+
}
|
|
508434
508491
|
error("Could not refresh token. Run 'lh login' to re-authenticate.");
|
|
508435
508492
|
cleanup();
|
|
508436
|
-
process.exit(
|
|
508493
|
+
process.exit(exitCode);
|
|
508437
508494
|
});
|
|
508438
508495
|
client.on("replaced", () => {
|
|
508439
508496
|
error(`Another '${"lh"} connect' on this machine took over this device's gateway connection. Only one can be connected at a time; stopping this one.`);
|
|
@@ -508468,7 +508525,7 @@ async function runConnect(options, isDaemonChild) {
|
|
|
508468
508525
|
if (workspaceId) {
|
|
508469
508526
|
error(`Workspace device registration failed: ${err.message}`);
|
|
508470
508527
|
cleanup();
|
|
508471
|
-
process.exit(
|
|
508528
|
+
process.exit(getWorkspaceFailureExitCode(err));
|
|
508472
508529
|
}
|
|
508473
508530
|
error(`Device registration failed (non-fatal): ${err.message}`);
|
|
508474
508531
|
}
|
|
@@ -522856,7 +522913,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
|
|
|
522856
522913
|
} };
|
|
522857
522914
|
|
|
522858
522915
|
//#endregion
|
|
522859
|
-
//#region node_modules/.pnpm/@hono+node-server@2.1.
|
|
522916
|
+
//#region node_modules/.pnpm/@hono+node-server@2.1.5_hono@4.13.13/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs
|
|
522860
522917
|
const X_ALREADY_SENT = "x-hono-already-sent";
|
|
522861
522918
|
|
|
522862
522919
|
//#endregion
|
|
@@ -522885,7 +522942,7 @@ const defineWebSocketHelper = (handler) => {
|
|
|
522885
522942
|
};
|
|
522886
522943
|
|
|
522887
522944
|
//#endregion
|
|
522888
|
-
//#region node_modules/.pnpm/@hono+node-server@2.1.
|
|
522945
|
+
//#region node_modules/.pnpm/@hono+node-server@2.1.5_hono@4.13.13/node_modules/@hono/node-server/dist/index.mjs
|
|
522889
522946
|
var RequestError = class extends Error {
|
|
522890
522947
|
constructor(message, options) {
|
|
522891
522948
|
super(message, options);
|
|
@@ -523707,7 +523764,7 @@ const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => {
|
|
|
523707
523764
|
if (incoming.errored) {
|
|
523708
523765
|
recordBodyBufferedBeforeDisconnect(incoming);
|
|
523709
523766
|
req[abortRequest](incoming.errored.toString());
|
|
523710
|
-
} else if (!outgoing.writableFinished) {
|
|
523767
|
+
} else if (!outgoing.writableFinished || incoming instanceof Http2ServerRequest && incoming.aborted) {
|
|
523711
523768
|
recordBodyBufferedBeforeDisconnect(incoming);
|
|
523712
523769
|
req[abortRequest]("Client connection prematurely closed.");
|
|
523713
523770
|
}
|
|
@@ -535153,6 +535210,345 @@ async function uploadReportEvidence(client, params) {
|
|
|
535153
535210
|
};
|
|
535154
535211
|
}
|
|
535155
535212
|
|
|
535213
|
+
//#endregion
|
|
535214
|
+
//#region src/commands/acceptanceLanguage.ts
|
|
535215
|
+
const HAN = {
|
|
535216
|
+
languageName: "Chinese",
|
|
535217
|
+
pattern: /\p{Script=Han}/u
|
|
535218
|
+
};
|
|
535219
|
+
const CYRILLIC = {
|
|
535220
|
+
languageName: "Cyrillic script",
|
|
535221
|
+
pattern: /\p{Script=Cyrillic}/u
|
|
535222
|
+
};
|
|
535223
|
+
const ARABIC = {
|
|
535224
|
+
languageName: "Arabic script",
|
|
535225
|
+
pattern: /\p{Script=Arabic}/u
|
|
535226
|
+
};
|
|
535227
|
+
/** Keyed by the primary language subtag of `responseLanguage` (`zh-CN` → `zh`). */
|
|
535228
|
+
const SCRIPT_RULES = {
|
|
535229
|
+
ar: {
|
|
535230
|
+
...ARABIC,
|
|
535231
|
+
languageName: "Arabic"
|
|
535232
|
+
},
|
|
535233
|
+
be: CYRILLIC,
|
|
535234
|
+
bg: {
|
|
535235
|
+
...CYRILLIC,
|
|
535236
|
+
languageName: "Bulgarian"
|
|
535237
|
+
},
|
|
535238
|
+
el: {
|
|
535239
|
+
languageName: "Greek",
|
|
535240
|
+
pattern: /\p{Script=Greek}/u
|
|
535241
|
+
},
|
|
535242
|
+
fa: {
|
|
535243
|
+
...ARABIC,
|
|
535244
|
+
languageName: "Persian"
|
|
535245
|
+
},
|
|
535246
|
+
he: {
|
|
535247
|
+
languageName: "Hebrew",
|
|
535248
|
+
pattern: /\p{Script=Hebrew}/u
|
|
535249
|
+
},
|
|
535250
|
+
hi: {
|
|
535251
|
+
languageName: "Hindi",
|
|
535252
|
+
pattern: /\p{Script=Devanagari}/u
|
|
535253
|
+
},
|
|
535254
|
+
ja: {
|
|
535255
|
+
languageName: "Japanese",
|
|
535256
|
+
pattern: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u
|
|
535257
|
+
},
|
|
535258
|
+
kk: CYRILLIC,
|
|
535259
|
+
ko: {
|
|
535260
|
+
languageName: "Korean",
|
|
535261
|
+
pattern: /\p{Script=Hangul}/u
|
|
535262
|
+
},
|
|
535263
|
+
mn: CYRILLIC,
|
|
535264
|
+
ru: {
|
|
535265
|
+
...CYRILLIC,
|
|
535266
|
+
languageName: "Russian"
|
|
535267
|
+
},
|
|
535268
|
+
sr: CYRILLIC,
|
|
535269
|
+
th: {
|
|
535270
|
+
languageName: "Thai",
|
|
535271
|
+
pattern: /\p{Script=Thai}/u
|
|
535272
|
+
},
|
|
535273
|
+
uk: {
|
|
535274
|
+
...CYRILLIC,
|
|
535275
|
+
languageName: "Ukrainian"
|
|
535276
|
+
},
|
|
535277
|
+
zh: HAN
|
|
535278
|
+
};
|
|
535279
|
+
const responseLanguageSettingsUrl = () => {
|
|
535280
|
+
const url = new URL("/settings/appearance", resolveServerUrl());
|
|
535281
|
+
url.username = "";
|
|
535282
|
+
url.password = "";
|
|
535283
|
+
return url.toString();
|
|
535284
|
+
};
|
|
535285
|
+
function reportLanguageFor(responseLanguage) {
|
|
535286
|
+
const locale = responseLanguage?.trim();
|
|
535287
|
+
if (!locale) return { status: "unset" };
|
|
535288
|
+
const rule = SCRIPT_RULES[locale.split(/[-_]/)[0].toLowerCase()];
|
|
535289
|
+
return rule ? {
|
|
535290
|
+
locale,
|
|
535291
|
+
rule,
|
|
535292
|
+
status: "enforced"
|
|
535293
|
+
} : {
|
|
535294
|
+
locale,
|
|
535295
|
+
status: "unchecked"
|
|
535296
|
+
};
|
|
535297
|
+
}
|
|
535298
|
+
async function resolveReportLanguage(client) {
|
|
535299
|
+
const value = (await client.user.getUserState.query())?.settings?.general?.responseLanguage;
|
|
535300
|
+
return reportLanguageFor(typeof value === "string" ? value : void 0);
|
|
535301
|
+
}
|
|
535302
|
+
/**
|
|
535303
|
+
* Code, links and paths are legitimately English inside any report; strip them
|
|
535304
|
+
* so "修复了 `useFoo` 的问题" passes and a bare `npm test` field is judged on
|
|
535305
|
+
* nothing rather than on the identifier.
|
|
535306
|
+
*/
|
|
535307
|
+
const stripNonProse = (text) => text.replaceAll(/(`{3,}|~{3,})[\s\S]*?\1/g, " ").replaceAll(/`[^`\n]*`/g, " ").replaceAll(/!?\[[^\]]*\]\([^)]*\)/g, (match) => match.replace(/\([^)]*\)$/, "")).replaceAll(/https?:\/\/[^\s<>"'`,。!?、;:()【】「」《》]+/g, " ");
|
|
535308
|
+
/**
|
|
535309
|
+
* A field fails when it contains letters but none in the target script — the
|
|
535310
|
+
* whole field is in another language. Mixed text with identifiers passes.
|
|
535311
|
+
*/
|
|
535312
|
+
function findLanguageViolations(fields, rule) {
|
|
535313
|
+
return fields.flatMap((field) => {
|
|
535314
|
+
if (typeof field.text !== "string") return [];
|
|
535315
|
+
const prose = stripNonProse(field.text);
|
|
535316
|
+
if (!/\p{L}/u.test(prose) || rule.pattern.test(prose)) return [];
|
|
535317
|
+
return [{
|
|
535318
|
+
path: field.path,
|
|
535319
|
+
text: field.text
|
|
535320
|
+
}];
|
|
535321
|
+
});
|
|
535322
|
+
}
|
|
535323
|
+
/** Prose in a check definition: its outcome, preconditions and step wording. */
|
|
535324
|
+
function checkDefinitionFields(at, definition) {
|
|
535325
|
+
const value = definition ?? {};
|
|
535326
|
+
const preconditions = Array.isArray(value.preconditions) ? value.preconditions : [];
|
|
535327
|
+
const steps = Array.isArray(value.steps) ? value.steps : [];
|
|
535328
|
+
return [
|
|
535329
|
+
{
|
|
535330
|
+
path: `${at}.expected`,
|
|
535331
|
+
text: value.expected
|
|
535332
|
+
},
|
|
535333
|
+
...preconditions.map((text, index) => ({
|
|
535334
|
+
path: `${at}.preconditions[${index}]`,
|
|
535335
|
+
text
|
|
535336
|
+
})),
|
|
535337
|
+
...steps.flatMap((step) => [{
|
|
535338
|
+
path: `${at}.steps[${String(step?.id)}].instruction`,
|
|
535339
|
+
text: step?.instruction
|
|
535340
|
+
}, {
|
|
535341
|
+
path: `${at}.steps[${String(step?.id)}].expected`,
|
|
535342
|
+
text: step?.expected
|
|
535343
|
+
}])
|
|
535344
|
+
];
|
|
535345
|
+
}
|
|
535346
|
+
/**
|
|
535347
|
+
* Video step/claim captions and the before/after caption band. Shared by
|
|
535348
|
+
* `ingest` and `evidence upload --metadata`, which persist the same shape.
|
|
535349
|
+
*/
|
|
535350
|
+
function evidenceCaptionFields(at, metadata) {
|
|
535351
|
+
const value = metadata ?? {};
|
|
535352
|
+
return [...(Array.isArray(value.chapters) ? value.chapters : []).flatMap((chapter, i) => [{
|
|
535353
|
+
path: `${at}.chapters[${i}].label`,
|
|
535354
|
+
text: chapter?.label
|
|
535355
|
+
}, {
|
|
535356
|
+
path: `${at}.chapters[${i}].note`,
|
|
535357
|
+
text: chapter?.note
|
|
535358
|
+
}]), {
|
|
535359
|
+
path: `${at}.comparison.label`,
|
|
535360
|
+
text: value.comparison?.label
|
|
535361
|
+
}];
|
|
535362
|
+
}
|
|
535363
|
+
/** Every `ingest` field that renders as prose on the acceptance page. */
|
|
535364
|
+
function ingestLanguageFields(input) {
|
|
535365
|
+
const fields = [
|
|
535366
|
+
{
|
|
535367
|
+
path: "title",
|
|
535368
|
+
text: input.title
|
|
535369
|
+
},
|
|
535370
|
+
{
|
|
535371
|
+
path: "focus",
|
|
535372
|
+
text: input.goal
|
|
535373
|
+
},
|
|
535374
|
+
{
|
|
535375
|
+
path: "requirement",
|
|
535376
|
+
text: input.requirement
|
|
535377
|
+
},
|
|
535378
|
+
{
|
|
535379
|
+
path: "summary.conclusion",
|
|
535380
|
+
text: input.conclusion
|
|
535381
|
+
},
|
|
535382
|
+
{
|
|
535383
|
+
path: "report.md",
|
|
535384
|
+
text: input.report
|
|
535385
|
+
},
|
|
535386
|
+
{
|
|
535387
|
+
path: "proposal.md",
|
|
535388
|
+
text: input.proposal
|
|
535389
|
+
}
|
|
535390
|
+
];
|
|
535391
|
+
for (const item of input.plan ?? []) {
|
|
535392
|
+
const config = item.verifierConfig ?? {};
|
|
535393
|
+
const evidenceSpecs = Array.isArray(config.requiredEvidence) ? config.requiredEvidence : [];
|
|
535394
|
+
fields.push({
|
|
535395
|
+
path: `plan[${item.id}].title`,
|
|
535396
|
+
text: item.title
|
|
535397
|
+
}, {
|
|
535398
|
+
path: `plan[${item.id}].category`,
|
|
535399
|
+
text: item.category
|
|
535400
|
+
}, {
|
|
535401
|
+
path: `plan[${item.id}].method`,
|
|
535402
|
+
text: config.method
|
|
535403
|
+
}, {
|
|
535404
|
+
path: `plan[${item.id}].expected`,
|
|
535405
|
+
text: config.expected
|
|
535406
|
+
}, {
|
|
535407
|
+
path: `plan[${item.id}].description`,
|
|
535408
|
+
text: item.description
|
|
535409
|
+
}, ...checkDefinitionFields(`plan[${item.id}].definition`, item.definition), ...evidenceSpecs.map((spec, index) => ({
|
|
535410
|
+
path: `plan[${item.id}].requiredEvidence[${index}].hint`,
|
|
535411
|
+
text: spec?.hint
|
|
535412
|
+
})));
|
|
535413
|
+
}
|
|
535414
|
+
for (const { case: c, checkItemId } of input.cases) {
|
|
535415
|
+
fields.push({
|
|
535416
|
+
path: `cases[${checkItemId}].name`,
|
|
535417
|
+
text: c.name ?? c.case ?? c.title
|
|
535418
|
+
}, {
|
|
535419
|
+
path: `cases[${checkItemId}].observation`,
|
|
535420
|
+
text: c.keyObservation ?? c.observation ?? c.note
|
|
535421
|
+
}, {
|
|
535422
|
+
path: `cases[${checkItemId}].suggestion`,
|
|
535423
|
+
text: c.suggestion
|
|
535424
|
+
});
|
|
535425
|
+
(Array.isArray(c.evidence) ? c.evidence : [c.evidence]).forEach((entry, index) => {
|
|
535426
|
+
if (!entry || typeof entry !== "object") return;
|
|
535427
|
+
const at = `cases[${checkItemId}].evidence[${index}]`;
|
|
535428
|
+
fields.push({
|
|
535429
|
+
path: `${at}.description`,
|
|
535430
|
+
text: entry.description ?? entry.desc
|
|
535431
|
+
}, ...evidenceCaptionFields(at, entry));
|
|
535432
|
+
});
|
|
535433
|
+
fields.push(...visualizationFields(`cases[${checkItemId}]`, c));
|
|
535434
|
+
}
|
|
535435
|
+
return fields;
|
|
535436
|
+
}
|
|
535437
|
+
/**
|
|
535438
|
+
* Visualization prose: titles, context and axis/series labels. Categories and
|
|
535439
|
+
* table cells are data (model names, file names, ids) and legitimately Latin,
|
|
535440
|
+
* so they are not checked — a false rejection has no bypass.
|
|
535441
|
+
*/
|
|
535442
|
+
function visualizationFields(at, c) {
|
|
535443
|
+
const datasets = Array.isArray(c.datasets) ? c.datasets : [];
|
|
535444
|
+
const views = Array.isArray(c.visualizations) ? c.visualizations : [];
|
|
535445
|
+
return [...datasets.flatMap((dataset, d) => (Array.isArray(dataset?.fields) ? dataset.fields : []).map((field, f) => ({
|
|
535446
|
+
path: `${at}.datasets[${d}].fields[${f}].label`,
|
|
535447
|
+
text: field?.label
|
|
535448
|
+
}))), ...views.flatMap((view, v) => {
|
|
535449
|
+
const series = Array.isArray(view?.encoding?.series) ? view.encoding.series : [];
|
|
535450
|
+
return [
|
|
535451
|
+
{
|
|
535452
|
+
path: `${at}.visualizations[${v}].title`,
|
|
535453
|
+
text: view?.title
|
|
535454
|
+
},
|
|
535455
|
+
{
|
|
535456
|
+
path: `${at}.visualizations[${v}].context`,
|
|
535457
|
+
text: view?.context
|
|
535458
|
+
},
|
|
535459
|
+
...[
|
|
535460
|
+
"xLabel",
|
|
535461
|
+
"yLabel",
|
|
535462
|
+
"valueLabel"
|
|
535463
|
+
].map((key) => ({
|
|
535464
|
+
path: `${at}.visualizations[${v}].encoding.${key}`,
|
|
535465
|
+
text: view?.encoding?.[key]
|
|
535466
|
+
})),
|
|
535467
|
+
...series.map((entry, i) => ({
|
|
535468
|
+
path: `${at}.visualizations[${v}].encoding.series[${i}].label`,
|
|
535469
|
+
text: entry?.label
|
|
535470
|
+
}))
|
|
535471
|
+
];
|
|
535472
|
+
})];
|
|
535473
|
+
}
|
|
535474
|
+
const preview = (text) => {
|
|
535475
|
+
const line = text.replaceAll(/\s+/g, " ").trim();
|
|
535476
|
+
return line.length > 80 ? `${line.slice(0, 77)}...` : line;
|
|
535477
|
+
};
|
|
535478
|
+
var ReportLanguageError = class extends Error {
|
|
535479
|
+
constructor(locale, violations, languageName) {
|
|
535480
|
+
super([
|
|
535481
|
+
`Report language check failed: your account's response language (settings.general.responseLanguage) is "${locale}", so every human-readable field must be written in ${languageName}.`,
|
|
535482
|
+
`These ${violations.length} field(s) contain no ${languageName} text:`,
|
|
535483
|
+
...violations.map((v) => ` - ${v.path}: "${preview(v.text)}"`),
|
|
535484
|
+
`Rewrite them in ${languageName} and retry. If "${locale}" is not the language you read, change Response Language at ${responseLanguageSettingsUrl()} first.`
|
|
535485
|
+
].join("\n"));
|
|
535486
|
+
this.locale = locale;
|
|
535487
|
+
this.violations = violations;
|
|
535488
|
+
this.name = "ReportLanguageError";
|
|
535489
|
+
}
|
|
535490
|
+
};
|
|
535491
|
+
const unsetLanguageWarning = () => `Report language not checked: your account has no response language set (Response Language is "Follow System" at ${responseLanguageSettingsUrl()}). Set it so acceptance text is checked against the language you read.`;
|
|
535492
|
+
/**
|
|
535493
|
+
* Throws `ReportLanguageError` before any remote write when a field is in the
|
|
535494
|
+
* wrong script. There is intentionally no bypass flag: an agent that hits the
|
|
535495
|
+
* error would just add it.
|
|
535496
|
+
*/
|
|
535497
|
+
async function assertReportLanguage(client, fields, language) {
|
|
535498
|
+
const resolved = language ?? await resolveReportLanguage(client);
|
|
535499
|
+
if (resolved.status === "unset") {
|
|
535500
|
+
log$13.warn(unsetLanguageWarning());
|
|
535501
|
+
return resolved;
|
|
535502
|
+
}
|
|
535503
|
+
if (resolved.status !== "enforced") return resolved;
|
|
535504
|
+
const violations = findLanguageViolations(fields, resolved.rule);
|
|
535505
|
+
if (violations.length > 0) throw new ReportLanguageError(resolved.locale, violations, resolved.rule.languageName);
|
|
535506
|
+
return resolved;
|
|
535507
|
+
}
|
|
535508
|
+
/** JSON-safe view of the resolved language for `--json` output. */
|
|
535509
|
+
const languageSummary = (language) => ({
|
|
535510
|
+
enforced: language.status === "enforced",
|
|
535511
|
+
locale: language.status === "unset" ? null : language.locale,
|
|
535512
|
+
...language.status === "enforced" ? { languageName: language.rule.languageName } : {},
|
|
535513
|
+
status: language.status
|
|
535514
|
+
});
|
|
535515
|
+
/**
|
|
535516
|
+
* Prose in a flow definition: its title, every inline check's wording, and
|
|
535517
|
+
* edge triggers/conditions, which render as graph labels.
|
|
535518
|
+
*/
|
|
535519
|
+
function flowLanguageFields(definition) {
|
|
535520
|
+
const flow = definition ?? {};
|
|
535521
|
+
const fields = [{
|
|
535522
|
+
path: "definition.title",
|
|
535523
|
+
text: flow.title
|
|
535524
|
+
}];
|
|
535525
|
+
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
535526
|
+
for (const node of nodes) {
|
|
535527
|
+
const check = node?.check;
|
|
535528
|
+
if (!check || typeof check !== "object") continue;
|
|
535529
|
+
const at = `nodes[${String(check.id ?? node.id)}].check`;
|
|
535530
|
+
fields.push({
|
|
535531
|
+
path: `${at}.title`,
|
|
535532
|
+
text: check.title
|
|
535533
|
+
}, {
|
|
535534
|
+
path: `${at}.description`,
|
|
535535
|
+
text: check.description
|
|
535536
|
+
}, ...checkDefinitionFields(`${at}.definition`, check.definition));
|
|
535537
|
+
}
|
|
535538
|
+
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
535539
|
+
for (const edge of edges) {
|
|
535540
|
+
const value = edge ?? {};
|
|
535541
|
+
fields.push({
|
|
535542
|
+
path: `edges[${String(value.id)}].trigger`,
|
|
535543
|
+
text: value.trigger
|
|
535544
|
+
}, {
|
|
535545
|
+
path: `edges[${String(value.id)}].condition`,
|
|
535546
|
+
text: value.condition
|
|
535547
|
+
});
|
|
535548
|
+
}
|
|
535549
|
+
return fields;
|
|
535550
|
+
}
|
|
535551
|
+
|
|
535156
535552
|
//#endregion
|
|
535157
535553
|
//#region src/commands/acceptanceRun.ts
|
|
535158
535554
|
const listMaterializedFiles = (directory) => {
|
|
@@ -535237,7 +535633,15 @@ function printWiring(links) {
|
|
|
535237
535633
|
}
|
|
535238
535634
|
}
|
|
535239
535635
|
async function runCreateAction(options) {
|
|
535240
|
-
const
|
|
535636
|
+
const client = await getTrpcClient();
|
|
535637
|
+
await assertReportLanguage(client, [{
|
|
535638
|
+
path: "--title",
|
|
535639
|
+
text: options.title
|
|
535640
|
+
}, {
|
|
535641
|
+
path: "--goal",
|
|
535642
|
+
text: options.goal
|
|
535643
|
+
}]);
|
|
535644
|
+
const created = await client.verify.createRun.mutate({
|
|
535241
535645
|
goal: options.goal,
|
|
535242
535646
|
operationId: options.operation,
|
|
535243
535647
|
source: options.source,
|
|
@@ -535294,7 +535698,22 @@ async function runDeleteAction(runId, options) {
|
|
|
535294
535698
|
console.log(`${import_picocolors.default.green("✓")} Deleted run ${import_picocolors.default.bold(result.id)}`);
|
|
535295
535699
|
}
|
|
535296
535700
|
async function resultIngestAction(options) {
|
|
535297
|
-
const
|
|
535701
|
+
const client = await getTrpcClient();
|
|
535702
|
+
await assertReportLanguage(client, [
|
|
535703
|
+
{
|
|
535704
|
+
path: "--title",
|
|
535705
|
+
text: options.title
|
|
535706
|
+
},
|
|
535707
|
+
{
|
|
535708
|
+
path: "--evidence",
|
|
535709
|
+
text: options.evidence
|
|
535710
|
+
},
|
|
535711
|
+
{
|
|
535712
|
+
path: "--suggestion",
|
|
535713
|
+
text: options.suggestion
|
|
535714
|
+
}
|
|
535715
|
+
]);
|
|
535716
|
+
const created = await client.verify.ingestResult.mutate({
|
|
535298
535717
|
checkItemId: options.check,
|
|
535299
535718
|
checkItemIndex: options.index ? Number.parseInt(options.index, 10) : void 0,
|
|
535300
535719
|
checkItemTitle: options.title,
|
|
@@ -535345,6 +535764,13 @@ async function submitAction(options) {
|
|
|
535345
535764
|
process.exit(1);
|
|
535346
535765
|
}
|
|
535347
535766
|
const client = await getTrpcClient();
|
|
535767
|
+
await assertReportLanguage(client, [{
|
|
535768
|
+
path: "--title",
|
|
535769
|
+
text: options.title
|
|
535770
|
+
}, {
|
|
535771
|
+
path: "--desc",
|
|
535772
|
+
text: options.desc
|
|
535773
|
+
}]);
|
|
535348
535774
|
let fileId;
|
|
535349
535775
|
let inlineContent = options.content;
|
|
535350
535776
|
if (options.file) {
|
|
@@ -535408,6 +535834,10 @@ async function evidenceUploadAction(options) {
|
|
|
535408
535834
|
}
|
|
535409
535835
|
const metadata = options.metadata ? JSON.parse(options.metadata) : void 0;
|
|
535410
535836
|
const client = await getTrpcClient();
|
|
535837
|
+
await assertReportLanguage(client, [{
|
|
535838
|
+
path: "--desc",
|
|
535839
|
+
text: options.desc
|
|
535840
|
+
}, ...evidenceCaptionFields("--metadata", metadata)]);
|
|
535411
535841
|
let fileId = options.fileId;
|
|
535412
535842
|
let inlineContent = options.content;
|
|
535413
535843
|
if (options.file) {
|
|
@@ -535464,7 +535894,15 @@ async function evidenceDeleteAction(evidenceId, options) {
|
|
|
535464
535894
|
}
|
|
535465
535895
|
async function reportUpsertAction(options) {
|
|
535466
535896
|
const num = (s) => s === void 0 ? void 0 : Number.parseInt(s, 10);
|
|
535467
|
-
const
|
|
535897
|
+
const client = await getTrpcClient();
|
|
535898
|
+
await assertReportLanguage(client, [{
|
|
535899
|
+
path: "--summary",
|
|
535900
|
+
text: options.summary
|
|
535901
|
+
}, {
|
|
535902
|
+
path: "--content",
|
|
535903
|
+
text: options.content
|
|
535904
|
+
}]);
|
|
535905
|
+
const created = await client.verify.upsertReport.mutate({
|
|
535468
535906
|
content: options.content,
|
|
535469
535907
|
failedChecks: num(options.failed),
|
|
535470
535908
|
passedChecks: num(options.passed),
|
|
@@ -535582,6 +536020,30 @@ async function ingestReportAction(reportDir, options) {
|
|
|
535582
536020
|
const requirement = options.requirement ?? subject?.requirement;
|
|
535583
536021
|
const conclusion = typeof summary.conclusion === "string" ? summary.conclusion : typeof summary.note === "string" ? summary.note : void 0;
|
|
535584
536022
|
const client = await getTrpcClient();
|
|
536023
|
+
const language = await assertReportLanguage(client, ingestLanguageFields({
|
|
536024
|
+
cases,
|
|
536025
|
+
conclusion,
|
|
536026
|
+
goal,
|
|
536027
|
+
plan,
|
|
536028
|
+
proposal,
|
|
536029
|
+
report: content,
|
|
536030
|
+
requirement,
|
|
536031
|
+
title
|
|
536032
|
+
}));
|
|
536033
|
+
if (options.dryRun) {
|
|
536034
|
+
const checked = language.status === "enforced" ? `report language ${language.locale} ✓` : language.status === "unchecked" ? `report language ${language.locale} (not script-checked)` : "report language not set";
|
|
536035
|
+
if (options.json !== void 0) {
|
|
536036
|
+
outputJson({
|
|
536037
|
+
cases: cases.length,
|
|
536038
|
+
dryRun: true,
|
|
536039
|
+
language: languageSummary(language),
|
|
536040
|
+
planItems: plan?.length ?? 0
|
|
536041
|
+
}, typeof options.json === "string" ? options.json : void 0);
|
|
536042
|
+
return;
|
|
536043
|
+
}
|
|
536044
|
+
console.log(`${import_picocolors.default.green("✓ Dry run passed")} — ${cases.length} case(s), ${plan?.length ?? 0} plan item(s), ${checked}. Nothing was published.`);
|
|
536045
|
+
return;
|
|
536046
|
+
}
|
|
535585
536047
|
let acceptance;
|
|
535586
536048
|
let bundle;
|
|
535587
536049
|
if (requestedAcceptanceId) {
|
|
@@ -535846,7 +536308,7 @@ function withReportUpsertOptions(cmd) {
|
|
|
535846
536308
|
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");
|
|
535847
536309
|
}
|
|
535848
536310
|
function withIngestReportOptions(cmd) {
|
|
535849
|
-
return cmd.option("--source <source>", "agent | agent-testing", "agent-testing").option("--operation <id>", "Link the session to an existing Agent Run").option("--acceptance <id>", "Append this round to an existing acceptance").option("--title <title>", "Override the session title").option("--goal <goal>", "The goal/task being verified").option("--subject <type:id>", "Attach to a task/topic/document (defaults to the current topic; otherwise standalone)").option("--requirement <text>", "Acceptance requirement recorded when the aggregate is first created").option("--open", "Print the in-app URL to open the report").option("--json [fields]", "Output JSON");
|
|
536311
|
+
return cmd.option("--source <source>", "agent | agent-testing", "agent-testing").option("--operation <id>", "Link the session to an existing Agent Run").option("--acceptance <id>", "Append this round to an existing acceptance").option("--title <title>", "Override the session title").option("--goal <goal>", "The goal/task being verified").option("--subject <type:id>", "Attach to a task/topic/document (defaults to the current topic; otherwise standalone)").option("--requirement <text>", "Acceptance requirement recorded when the aggregate is first created").option("--open", "Print the in-app URL to open the report").option("--dry-run", "Validate the report (including its language) without publishing; works on a plan-only result.json").option("--json [fields]", "Output JSON");
|
|
535850
536312
|
}
|
|
535851
536313
|
/**
|
|
535852
536314
|
* The run-scoped acceptance commands. A run is one immutable round of a
|
|
@@ -535960,6 +536422,7 @@ function attachAcceptanceFlowCommands(acceptance) {
|
|
|
535960
536422
|
flow.command("publish <acceptanceId>").requiredOption("--file <path>", "JSON: definition and optional flowId").action(async (id, options) => {
|
|
535961
536423
|
const client = await getTrpcClient();
|
|
535962
536424
|
const input = JSON.parse(await readFile(options.file, "utf8"));
|
|
536425
|
+
await assertReportLanguage(client, flowLanguageFields(input.definition));
|
|
535963
536426
|
outputJson(await client.acceptance.publishFlow.mutate({
|
|
535964
536427
|
...input,
|
|
535965
536428
|
id
|
|
@@ -535994,6 +536457,10 @@ function attachAcceptanceFlowCommands(acceptance) {
|
|
|
535994
536457
|
flow.command("record <acceptanceId>").requiredOption("--file <path>", "JSON: verifyRunId, checkItemId, verdict, observation").action(async (id, options) => {
|
|
535995
536458
|
const client = await getTrpcClient();
|
|
535996
536459
|
const input = JSON.parse(await readFile(options.file, "utf8"));
|
|
536460
|
+
await assertReportLanguage(client, [{
|
|
536461
|
+
path: "observation",
|
|
536462
|
+
text: input.observation
|
|
536463
|
+
}]);
|
|
535997
536464
|
outputJson(await client.acceptance.recordFlowStep.mutate({
|
|
535998
536465
|
...input,
|
|
535999
536466
|
id
|
|
@@ -536055,6 +536522,23 @@ const userGlyph = (userReview) => {
|
|
|
536055
536522
|
function registerAcceptanceCommands(parent, options) {
|
|
536056
536523
|
const acceptance = parent.command("acceptance").description(options?.deprecated ? "Deprecated alias — use `lh acceptance`" : "Delivery acceptances: the cross-round review loop (checks, feedback, decision)");
|
|
536057
536524
|
attachAcceptanceFlowCommands(acceptance);
|
|
536525
|
+
acceptance.command("preflight").description("Check that the CLI is authenticated and print the language every acceptance text must be written in. Run it before authoring anything, and pass the language to any subagent you delegate to.").option("--json [fields]", "Output JSON").action(async (options) => {
|
|
536526
|
+
const language = await resolveReportLanguage(await getTrpcClient());
|
|
536527
|
+
const instruction = language.status === "unset" ? "No response language is set on this account, so the language is not checked. Write in the language the user converses in." : `Write every human-readable acceptance field (title, plan, case names and observations, evidence descriptions, conclusion, report.md, proposal.md) in ${language.locale}${language.status === "enforced" ? `, i.e. ${language.rule.languageName}` : ""}. Pass this language explicitly to any subagent you delegate to.`;
|
|
536528
|
+
if (options.json !== void 0) {
|
|
536529
|
+
outputJson({
|
|
536530
|
+
authenticated: true,
|
|
536531
|
+
instruction,
|
|
536532
|
+
language: languageSummary(language),
|
|
536533
|
+
settingsUrl: responseLanguageSettingsUrl()
|
|
536534
|
+
}, typeof options.json === "string" ? options.json : void 0);
|
|
536535
|
+
return;
|
|
536536
|
+
}
|
|
536537
|
+
if (language.status === "unset") log$13.warn(unsetLanguageWarning());
|
|
536538
|
+
console.log(`${import_picocolors.default.green("✓")} Authenticated`);
|
|
536539
|
+
console.log(`${import_picocolors.default.bold("report language")}: ${language.status === "unset" ? import_picocolors.default.yellow("not set") : language.locale}${language.status === "unchecked" ? import_picocolors.default.dim(" (not script-checked)") : ""}`);
|
|
536540
|
+
console.log(instruction);
|
|
536541
|
+
});
|
|
536058
536542
|
acceptance.command("create").summary("Create or reuse an acceptance without creating a verification round or results").description("Create or reuse an acceptance record. This does not create checks, verification rounds, results, reports, or evidence.\n\nBefore creating, agents should load and read the acceptance skill (.agents/skills/acceptance/SKILL.md) and follow its workflow. If the skill is missing, run `lh acceptance install`, then read it.\n\nUse create for flow-first planning. If a completed local report already exists, use `lh acceptance run ingest <reportDir>` to publish it; a separate create call is unnecessary. An acceptance URL alone does not mean verification is complete: publish results and required evidence, then read back coverage before handing off a completed acceptance.").requiredOption("--requirement <text>", "Durable business goal (preserved when reusing a subject)").option("-t, --title <text>", "Display title for a standalone acceptance").option("--subject <type:id>", "Reuse or create for task/topic/document/standalone; omit for a new standalone acceptance").option("--json [fields]", "Output JSON, optionally select fields (acceptanceId, acceptanceUrl, requirement, status, subject)").action(async (options) => {
|
|
536059
536543
|
const requirement = options.requirement.trim();
|
|
536060
536544
|
const title = options.title?.trim();
|
|
@@ -536065,7 +536549,15 @@ function registerAcceptanceCommands(parent, options) {
|
|
|
536065
536549
|
subjectType: "standalone"
|
|
536066
536550
|
} : parseSubjectRef(options.subject);
|
|
536067
536551
|
if (!subject) throw new InvalidArgumentError(`--subject must be one of ${acceptanceSubjectTypes.map((type) => `${type}:<id>`).join(" | ")}`);
|
|
536068
|
-
const
|
|
536552
|
+
const client = await getTrpcClient();
|
|
536553
|
+
await assertReportLanguage(client, [{
|
|
536554
|
+
path: "--requirement",
|
|
536555
|
+
text: requirement
|
|
536556
|
+
}, {
|
|
536557
|
+
path: "--title",
|
|
536558
|
+
text: title
|
|
536559
|
+
}]);
|
|
536560
|
+
const result = await client.acceptance.ensure.mutate({
|
|
536069
536561
|
...subject,
|
|
536070
536562
|
requirement,
|
|
536071
536563
|
title
|
package/man/man1/lh.1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.\" Code generated by `npm run man:generate`; DO NOT EDIT.
|
|
2
2
|
.\" Manual command details come from the Commander command tree.
|
|
3
|
-
.TH LH 1 "" "@lobehub/cli 0.0.
|
|
3
|
+
.TH LH 1 "" "@lobehub/cli 0.0.65" "User Commands"
|
|
4
4
|
.SH NAME
|
|
5
5
|
lh \- LobeHub CLI \- manage and connect to LobeHub services
|
|
6
6
|
.SH SYNOPSIS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lh": "dist/index.js",
|
|
@@ -16,11 +16,36 @@
|
|
|
16
16
|
"dist",
|
|
17
17
|
"man"
|
|
18
18
|
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bun scripts/ensureWorkspaceLinks.ts && tsdown && tsdown src/daemon/maintenanceWorker.ts --no-config --no-clean --format esm --target node22",
|
|
21
|
+
"cli:link": "bun link",
|
|
22
|
+
"cli:unlink": "bun unlink",
|
|
23
|
+
"dev": "LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts",
|
|
24
|
+
"man:generate": "bun src/man/generate.ts",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run man:generate",
|
|
26
|
+
"test": "bunx vitest run --config vitest.config.mts --silent='passed-only'",
|
|
27
|
+
"test:coverage": "bunx vitest run --config vitest.config.mts --coverage",
|
|
28
|
+
"test:e2e": "bunx vitest run --config vitest.e2e.config.mts --silent='passed-only'",
|
|
29
|
+
"type-check": "tsc --noEmit"
|
|
30
|
+
},
|
|
19
31
|
"dependencies": {
|
|
20
32
|
"@lydell/node-pty": "1.2.0-beta.15",
|
|
21
33
|
"ws": "^8.21.0"
|
|
22
34
|
},
|
|
23
35
|
"devDependencies": {
|
|
36
|
+
"@lobechat/agent-gateway-client": "workspace:*",
|
|
37
|
+
"@lobechat/agent-tracing": "workspace:*",
|
|
38
|
+
"@lobechat/const": "workspace:*",
|
|
39
|
+
"@lobechat/device-control": "workspace:*",
|
|
40
|
+
"@lobechat/device-gateway-client": "workspace:*",
|
|
41
|
+
"@lobechat/device-identity": "workspace:*",
|
|
42
|
+
"@lobechat/eval-rubric": "workspace:*",
|
|
43
|
+
"@lobechat/heterogeneous-agents": "workspace:*",
|
|
44
|
+
"@lobechat/html-artifact": "workspace:*",
|
|
45
|
+
"@lobechat/local-file-shell": "workspace:*",
|
|
46
|
+
"@lobechat/tool-runtime": "workspace:*",
|
|
47
|
+
"@lobechat/types": "workspace:*",
|
|
48
|
+
"@lobechat/utils": "workspace:*",
|
|
24
49
|
"@trpc/client": "^11.18.0",
|
|
25
50
|
"@types/node": "^24.13.2",
|
|
26
51
|
"@types/semver": "^7.7.1",
|
|
@@ -39,20 +64,7 @@
|
|
|
39
64
|
"tsdown": "^0.21.10",
|
|
40
65
|
"typescript": "^6.0.3",
|
|
41
66
|
"url-join": "^5.0.0",
|
|
42
|
-
"vitest": "5.0.0"
|
|
43
|
-
"@lobechat/const": "1.0.0",
|
|
44
|
-
"@lobechat/agent-gateway-client": "1.0.0",
|
|
45
|
-
"@lobechat/agent-tracing": "1.0.0",
|
|
46
|
-
"@lobechat/device-gateway-client": "1.0.0",
|
|
47
|
-
"@lobechat/device-identity": "1.0.0",
|
|
48
|
-
"@lobechat/eval-rubric": "1.0.0",
|
|
49
|
-
"@lobechat/device-control": "1.0.0",
|
|
50
|
-
"@lobechat/heterogeneous-agents": "1.0.0",
|
|
51
|
-
"@lobechat/html-artifact": "1.0.0",
|
|
52
|
-
"@lobechat/local-file-shell": "1.0.0",
|
|
53
|
-
"@lobechat/tool-runtime": "1.0.0",
|
|
54
|
-
"@lobechat/utils": "1.0.0",
|
|
55
|
-
"@lobechat/types": "1.0.0"
|
|
67
|
+
"vitest": "5.0.0"
|
|
56
68
|
},
|
|
57
69
|
"engines": {
|
|
58
70
|
"node": ">=22.15"
|
|
@@ -60,16 +72,5 @@
|
|
|
60
72
|
"publishConfig": {
|
|
61
73
|
"access": "public",
|
|
62
74
|
"registry": "https://registry.npmjs.org"
|
|
63
|
-
},
|
|
64
|
-
"scripts": {
|
|
65
|
-
"build": "bun scripts/ensureWorkspaceLinks.ts && tsdown && tsdown src/daemon/maintenanceWorker.ts --no-config --no-clean --format esm --target node22",
|
|
66
|
-
"cli:link": "bun link",
|
|
67
|
-
"cli:unlink": "bun unlink",
|
|
68
|
-
"dev": "LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts",
|
|
69
|
-
"man:generate": "bun src/man/generate.ts",
|
|
70
|
-
"test": "bunx vitest run --config vitest.config.mts --silent='passed-only'",
|
|
71
|
-
"test:coverage": "bunx vitest run --config vitest.config.mts --coverage",
|
|
72
|
-
"test:e2e": "bunx vitest run --config vitest.e2e.config.mts --silent='passed-only'",
|
|
73
|
-
"type-check": "tsc --noEmit"
|
|
74
75
|
}
|
|
75
|
-
}
|
|
76
|
+
}
|