// SPEC: PJ2026-010402 HWLAB CLI draft-2026-06-17-r0; PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0. // Responsibility: HWLAB CLI Web-equivalent client commands, including session, trace, result and workbench diagnostics. import { createHash } from "node:crypto"; import { chmod, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { caseCommand } from "./hwlab-caserun-lib.ts"; import { collectAgentObserverRuns } from "./hwlab-cli/agent-observer-client.ts"; import { computeCodeAgentComposerState, isCodeAgentSessionUnusableStatus } from "./hwlab-cli/composer-policy.ts"; import { traceDisplayRows, traceNoiseEventCount } from "./hwlab-cli/trace-renderer.ts"; import { resolveRuntimeEndpoint, runtimeEndpointVisibility, sameRuntimeEndpointScope } from "./runtime-endpoint-resolver.ts"; const VERSION = "0.2.0-client"; const CLI_NAME = "hwlab-cli"; const DEFAULT_TIMEOUT_MS = 30000; const DEFAULT_AGENT_TIMEOUT_MS = 120000; const DEFAULT_POLL_INTERVAL_MS = 1000; const HARNESS_WAIT_DEFAULT_TIMEOUT_MS = 50000; const HARNESS_WAIT_MAX_TIMEOUT_MS = 50000; const DEFAULT_GATEWAY_SESSION_ID = "gws_D601_F103"; const DEFAULT_GATEWAY_RESOURCE_ID = "res_windows_host"; const DEFAULT_GATEWAY_CAPABILITY_ID = "cap_windows_cmd_exec"; const DEFAULT_GATEWAY_PROJECT_ID = "prj_mvp_topology"; const DEFAULT_WORKBENCH_PROJECT_ID = "prj_hwpod_workbench"; type EnvLike = Record; type ParsedArgs = Record & { _: string[] }; type FetchLike = typeof fetch; type CliOptions = { env?: EnvLike; fetchImpl?: FetchLike; stdinText?: string; cwd?: string; now?: () => string; sleep?: (ms: number) => Promise; runProcess?: (command: string[], cwd: string, env: Record) => Promise<{ command: string[]; stdout: string; stderr: string; exitCode: number }>; startProcess?: (input: { command: string; args: string[]; cwd: string; env: Record; stdoutPath: string; stderrPath: string }) => Promise<{ pid: number }> | { pid: number }; }; export async function main(argv = process.argv.slice(2), options: CliOptions = {}) { const result = await runHwlabCli(argv, options); console.log(JSON.stringify(result.payload, null, 2)); process.exitCode = result.exitCode; } export async function runHwlabCli(argv: string[], options: CliOptions = {}) { const env = options.env ?? process.env; const now = options.now ?? (() => new Date().toISOString()); try { const parsed = parseOptions(argv); const target = parsed._[0] || "help"; const context = { parsed, env, fetchImpl: options.fetchImpl ?? fetch, stdinText: options.stdinText, cwd: options.cwd ?? process.cwd(), now, sleep: options.sleep ?? wait, runProcess: options.runProcess, startProcess: options.startProcess, argv }; const payload = target === "client" ? await clientCommand({ ...context, rest: parsed._.slice(1) }) : target === "case" ? await caseCommand({ ...context, rest: parsed._.slice(1) }) : help(); return { exitCode: payload.ok === false ? 1 : 0, payload: withMeta(payload, now) }; } catch (error) { return { exitCode: 1, payload: withMeta(failure("hwlab-cli", error), now) }; } } async function clientCommand(context: any) { const group = context.rest[0] || "help"; const next = { ...context, rest: context.rest.slice(1) }; if (["help", "--help", "-h"].includes(group)) return help(); if (context.parsed.help === true) { const groupHelp = clientSubcommandHelp(group); if (groupHelp) return groupHelp; } if (group === "auth") return authCommand(next); if (group === "account" || group === "user") return accountCommand(next); if (group === "api-keys" || group === "apikeys" || group === "keys") return apiKeysCommand(next); if (group === "billing") return billingCommand(next); if (group === "usage") return usageCommand(next); if (group === "access") return accessCommand(next); if (group === "provider-profiles" || group === "provider-profile" || group === "providers") return providerProfilesCommand(next); if (group === "runtime") return runtimeCommand(next); if (group === "gateway") return gatewayCommand(next); if (group === "agent") return agentCommand(next); if (group === "agent-observer") return agentObserverCommand(next); if (group === "session" || group === "sessions") return sessionCommand(next); if (["harness", "harness-ops", "harness-opt"].includes(group)) return harnessCommand(next); if (group === "workbench") return workbenchCommand(next); if (group === "rpc") return rpcCommand(next); if (group === "request") return requestCommand(next); throw cliError("unsupported_client_command", `unsupported client command: ${group}`, { group }); } function clientSubcommandHelp(group: string) { if (group === "auth") return authHelp(); if (group === "account" || group === "user") return accountHelp(); if (group === "api-keys" || group === "apikeys" || group === "keys") return apiKeysHelp(); if (group === "billing") return billingHelp(); if (group === "usage") return usageHelp(); if (group === "access") return accessHelp(); if (group === "provider-profiles" || group === "provider-profile" || group === "providers") return providerProfilesHelp(); if (group === "runtime") return runtimeHelp(); if (group === "gateway") return gatewayHelp(); if (group === "agent") return agentHelp(); if (group === "agent-observer") return agentObserverHelp(); if (group === "session" || group === "sessions") return sessionHelp(); if (["harness", "harness-ops", "harness-opt"].includes(group)) return harnessHelp(); if (group === "workbench") return workbenchHelp(); if (group === "rpc") return rpcHelp(); if (group === "request") return requestHelp(); return null; } function help() { return ok("help", { version: VERSION, contractVersion: "hwlab-web-equivalent-client-v1", mode: "short-connection-client", endpointResolver: { defaultBaseUrl: null, behavior: "fail-closed unless HWLAB_RUNTIME_* env or HWLAB_RUNTIME_NAMESPACE resolves the current lane", explicitOverride: "local-debug-only; forbidden when HWLAB_RUNTIME_ENDPOINT_LOCKED=1" }, stateFile: ".state/hwlab-cli/session.json", serviceRuntime: false, imagePublished: false, jobTemplate: false, usage: [ "HWLAB_API_KEY=hwl_live_... HWLAB_RUNTIME_NAMESPACE=hwlab-v02 hwlab-cli client auth whoami", "HWLAB_API_KEY=hwl_live_... HWLAB_RUNTIME_NAMESPACE=hwlab-v02 hwlab-cli client request GET /v1/users/me", "hwlab-cli client auth status", "hwlab-cli client account status", "hwlab-cli client api-keys list", "hwlab-cli client billing summary --limit 5", "hwlab-cli client usage summary", "hwlab-cli client auth session --web-session", "hwlab-cli client auth profiles", "hwlab-cli client access summary", "hwlab-cli client access users list", "hwlab-cli client access users inspect USER_ID", "hwlab-cli client access users set-role USER_ID --role admin|user [--status active|disabled]", "hwlab-cli client access tools grant USER_ID hwpod", "hwlab-cli client access tools revoke USER_ID hwpod", "hwlab-cli client provider-profiles list", "hwlab-cli client provider-profiles config deepseek", "hwlab-cli client provider-profiles set-config deepseek --config-stdin", "hwlab-cli client provider-profiles set-key deepseek --key-stdin", "hwlab-cli client provider-profiles validate deepseek --wait --timeout-ms 120000", "hwlab-cli client runtime routes", "hwlab-cli client gateway sessions", "hwlab-cli client gateway pressure --gateway-session-id gws_D601_F103", "hwlab-cli client session list", "hwlab-cli client session create", "hwlab-cli client session switch ses_...", "hwlab-cli client session inspect ses_... [--trace-id trc_...]", "hwlab-cli client workbench summary", "hwlab-cli client workbench status --profile NAME", "hwlab-cli client request GET /v1/access/status [--full]", "hwlab-cli client request GET /v1/web-performance/summary [--full]", "hwlab-cli client rpc system.health [--full]", "hwlab-cli client agent session create --provider-profile PROFILE", "hwlab-cli client agent session select SESSION_ID", "hwlab-cli client agent send --message TEXT|--message-file PATH [--from-trace TRACE] [--session-id ID] [--thread-id ID] [--retry-of TRACE] --provider-profile PROFILE", "hwlab-cli client agent send --message TEXT --wait --timeout-ms 50000", "hwlab-cli client agent result TRACE_ID", "hwlab-cli client agent trace TRACE_ID [--render web]", "hwlab-cli client agent inspect --trace-id TRACE_ID", "hwlab-cli client agent cancel TRACE_ID", "hwlab-cli client agent steer TRACE_ID --message TEXT|--message-file PATH", "hwlab-cli client harness submit --message TEXT --provider-profile PROFILE", "hwlab-cli client harness wait TRACE_ID --timeout-ms 50000", "hwlab-cli client harness audit TRACE_ID", "hwlab-cli case run CASE_ID --case-repo /root/hwlab-case-registry" ] }); } async function authCommand(context: any) { const subcommand = context.rest[0] || "session"; if (wantsHelp(context)) return authHelp(); if (subcommand === "login") return authLogin(context); if (subcommand === "oidc-login") { const response = await fetch(`${runtimeEndpoint(context.parsed, context.env, "web").baseUrl}/auth/oidc/login?returnTo=/`, { method: "GET", redirect: "manual" }); const location = response.headers.get("location") ?? ""; return ok("client.auth.oidc-login", { baseUrl: runtimeEndpoint(context.parsed, context.env, "web").baseUrl, authorizeUrl: location, hint: "open this URL in a browser to complete OIDC login; once the callback returns, run `client auth whoami` to verify the session" }); } if (subcommand === "status") return authStatus(context); if (subcommand === "profiles") return authProfiles(context); if (subcommand === "whoami") { const response = await requestJson({ ...context, method: "GET", path: "/v1/users/me" }); return responsePayload("client.auth.whoami", response, context, { route: route("GET", "/v1/users/me"), actor: response.body?.actor ?? null, authMethod: response.body?.authMethod ?? null }); } if (subcommand === "session") { const localSession = sessionStateSummary(await readSessionState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() })); const response = await requestJson({ ...context, method: "GET", path: "/auth/session", allowWebSession: true }); return responsePayload("client.auth.session", response, context, { route: route("GET", "/auth/session"), localSession }); } if (subcommand === "logout") { const response = await requestJson({ ...context, method: "POST", path: "/auth/logout", allowWebSession: true }); await clearSession(context); return responsePayload("client.auth.logout", response, context, { route: route("POST", "/auth/logout"), localSessionCleared: true }); } throw cliError("unsupported_auth_command", `unsupported auth command: ${subcommand}`, { subcommand }); } function authHelp() { return ok("client.auth.help", { serviceRuntime: false, imagePublished: false, commands: [ "whoami", "status [--profile NAME]", "session [--profile NAME] --web-session", "oidc-login", "profiles", "logout [--profile NAME] --web-session", "login --username USER --password-env HWLAB_PASSWORD [--profile NAME] (legacy web-session debug)" ] }); } async function accountCommand(context: any) { const subcommand = context.rest[0] || "status"; if (wantsHelp(context)) return accountHelp(); if (subcommand !== "status" && subcommand !== "summary") throw cliError("unsupported_account_command", `unsupported account command: ${subcommand}`, { subcommand }); const identityPath = "/v1/users/me"; const accessPath = "/v1/access/status"; const [identity, access] = await Promise.all([ requestJson({ ...context, method: "GET", path: identityPath }), requestJson({ ...context, method: "GET", path: accessPath }) ]); const success = responseSucceeded(identity) && responseSucceeded(access); const endpoint = identity.runtimeEndpoint ?? access.runtimeEndpoint ?? runtimeEndpoint(context.parsed, context.env, context.endpointKind ?? "web"); return ok("client.account.status", { baseUrl: endpoint.baseUrl, runtimeEndpoint: runtimeEndpointVisibility(endpoint), routes: { identity: route("GET", identityPath), access: route("GET", accessPath) }, requests: { identity: requestVisibility(identity), access: requestVisibility(access) }, auth: identity.auth ?? access.auth, authDiagnosis: identity.authDiagnosis ?? access.authDiagnosis ?? null, identity: identityBodyForCli(identity.body, context.parsed), access: accessStatusBodyForCli(access.body, context.parsed), body: { identity: identityBodyForCli(identity.body, context.parsed), access: accessStatusBodyForCli(access.body, context.parsed) } }, success ? "succeeded" : "failed"); } function accountHelp() { return ok("client.account.help", { serviceRuntime: false, imagePublished: false, sameOrigin: true, valuesPrinted: false, commands: [ "status" ], routes: { identity: route("GET", "/v1/users/me"), access: route("GET", "/v1/access/status") } }); } async function apiKeysCommand(context: any) { const subcommand = context.rest[0] || "list"; if (wantsHelp(context)) return apiKeysHelp(); if (subcommand !== "list" && subcommand !== "summary") throw cliError("unsupported_api_keys_command", `unsupported api-keys command: ${subcommand}`, { subcommand }); const pathName = "/v1/api-keys"; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.api-keys.list", response, context, { route: route("GET", pathName), body: apiKeysBodyForCli(response.body, context.parsed), valuesPrinted: false }); } function apiKeysHelp() { return ok("client.api-keys.help", { serviceRuntime: false, imagePublished: false, sameOrigin: true, valuesPrinted: false, commands: ["list"], routes: { list: route("GET", "/v1/api-keys") } }); } async function billingCommand(context: any) { const subcommand = context.rest[0] || "summary"; if (wantsHelp(context)) return billingHelp(); if (subcommand !== "summary") throw cliError("unsupported_billing_command", `unsupported billing command: ${subcommand}`, { subcommand }); const limit = boundedInteger(context.parsed.limit, 5, 1, 100); const pathName = `/v1/billing/summary?limit=${encodeURIComponent(String(limit))}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.billing.summary", response, context, { route: route("GET", pathName), limit, availability: selfServiceAvailability(response, "billing-summary"), body: billingSummaryBodyForCli(response.body, context.parsed), valuesPrinted: false }); } function billingHelp() { return ok("client.billing.help", { serviceRuntime: false, imagePublished: false, sameOrigin: true, valuesPrinted: false, commands: ["summary [--limit N]"], routes: { summary: route("GET", "/v1/billing/summary?limit=N") } }); } async function usageCommand(context: any) { const subcommand = context.rest[0] || "summary"; if (wantsHelp(context)) return usageHelp(); if (subcommand !== "summary") throw cliError("unsupported_usage_command", `unsupported usage command: ${subcommand}`, { subcommand }); const pathName = "/v1/usage/summary"; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.usage.summary", response, context, { route: route("GET", pathName), availability: selfServiceAvailability(response, "usage-summary"), body: usageSummaryBodyForCli(response.body, context.parsed), valuesPrinted: false }); } function usageHelp() { return ok("client.usage.help", { serviceRuntime: false, imagePublished: false, sameOrigin: true, valuesPrinted: false, commands: ["summary"], routes: { summary: route("GET", "/v1/usage/summary") } }); } async function accessCommand(context: any) { const subcommand = context.rest[0] || "summary"; if (wantsHelp(context)) return accessHelp(); if (subcommand === "summary") { const pathName = "/v1/admin/access/summary"; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.access.summary", response, context, { route: route("GET", pathName), body: adminAccessBodyForCli(response.body, context.parsed) }); } if (subcommand === "users") { return accessUsersCommand({ ...context, rest: context.rest.slice(1) }); } if (subcommand === "tools") { return accessToolsCommand({ ...context, rest: context.rest.slice(1) }); } if (subcommand === "check") { return accessCheckCommand({ ...context, rest: context.rest.slice(1) }); } throw cliError("unsupported_access_command", `unsupported access command: ${subcommand}`, { subcommand }); } async function accessUsersCommand(context: any) { const subcommand = context.rest[0] || "list"; if (subcommand === "list") { const pathName = "/v1/admin/access/users"; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.access.users.list", response, context, { route: route("GET", pathName), body: adminAccessBodyForCli(response.body, context.parsed) }); } if (subcommand === "inspect") { const userId = requiredText(context.parsed.userId ?? context.rest[1], "userId"); const pathName = `/v1/admin/access/users/${encodeURIComponent(userId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.access.users.inspect", response, context, { route: route("GET", pathName), userId, body: adminAccessBodyForCli(response.body, context.parsed) }); } if (subcommand === "set-role") { const userId = requiredText(context.parsed.userId ?? context.rest[1], "userId"); const role = normalizeAccessUserRole(context.parsed.role ?? context.rest[2]); const status = context.parsed.status === undefined ? undefined : normalizeAccessUserStatus(context.parsed.status); const pathName = `/v1/admin/access/users/${encodeURIComponent(userId)}`; const response = await requestJson({ ...context, method: "PATCH", path: pathName, body: pruneUndefined({ role, status }) }); return responsePayload("client.access.users.set-role", response, context, { route: route("PATCH", pathName), userId, role, status, body: adminAccessBodyForCli(response.body, context.parsed) }); } throw cliError("unsupported_access_users_command", `unsupported access users command: ${subcommand}`, { subcommand }); } async function accessToolsCommand(context: any) { const subcommand = context.rest[0] || "help"; if (subcommand !== "grant" && subcommand !== "revoke") throw cliError("unsupported_access_tools_command", `unsupported access tools command: ${subcommand}`, { subcommand }); const userId = requiredText(context.parsed.userId ?? context.rest[1], "userId"); const toolId = normalizeToolId(context.parsed.toolId ?? context.rest[2]); const method = subcommand === "grant" ? "PUT" : "DELETE"; const pathName = `/v1/admin/access/users/${encodeURIComponent(userId)}/tools/${encodeURIComponent(toolId)}/can-use`; const response = await requestJson({ ...context, method, path: pathName, body: {} }); return responsePayload(`client.access.tools.${subcommand}`, response, context, { route: route(method, pathName), userId, toolId, relation: "can_use", body: adminAccessBodyForCli(response.body, context.parsed) }); } async function accessCheckCommand(context: any) { const userId = requiredText(context.parsed.user ?? context.parsed.userId ?? context.parsed.actorId ?? context.rest[0], "userId"); const relation = requiredText(context.parsed.relation ?? context.rest[1], "relation"); const object = requiredText(context.parsed.object ?? context.rest[2], "object"); const pathName = "/v1/admin/access/check"; const response = await requestJson({ ...context, method: "POST", path: pathName, body: { userId, relation, object } }); return responsePayload("client.access.check", response, context, { route: route("POST", pathName), userId, relation, object, body: adminAccessBodyForCli(response.body, context.parsed) }); } function accessHelp() { return ok("client.access.help", { serviceRuntime: false, imagePublished: false, sameOrigin: true, commands: [ "summary", "users list", "users inspect USER_ID", "users set-role USER_ID --role admin|user [--status active|disabled]", "tools grant USER_ID hwpod|unidesk_ssh|trans_cmd|github_pr", "tools revoke USER_ID hwpod|unidesk_ssh|trans_cmd|github_pr", "check --user USER_ID --relation REL --object TYPE:ID" ], routes: { summary: route("GET", "/v1/admin/access/summary"), users: route("GET", "/v1/admin/access/users") } }); } async function providerProfilesCommand(context: any) { const subcommand = context.rest[0] || "list"; if (wantsHelp(context)) return providerProfilesHelp(); if (subcommand === "list") { const pathName = "/v1/admin/provider-profiles"; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.provider-profiles.list", response, context, { route: route("GET", pathName), body: providerProfileBodyForCli(response.body, context.parsed) }); } if (subcommand === "remove" || subcommand === "delete" || subcommand === "rm") { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}`; const response = await requestJson({ ...context, method: "DELETE", path: pathName, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); return responsePayload("client.provider-profiles.remove", response, context, { route: route("DELETE", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false }); } if (subcommand === "set-key" || subcommand === "set" || subcommand === "credential") { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); const authJsonMode = context.parsed.authJsonStdin === true; if (!authJsonMode && context.parsed.keyStdin !== true) throw cliError("credential_stdin_required", "set-key requires --key-stdin, or use set-auth-json PROFILE --auth-json-stdin for Codex auth.json", { command: "provider-profiles set-key PROFILE --key-stdin" }); const credentialBody = authJsonMode ? { authJson: await providerProfileAuthJsonFromStdin(context) } : { apiKey: await providerProfileApiKeyFromStdin(context) }; const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/credential`; const response = await requestJson({ ...context, method: "PUT", path: pathName, body: credentialBody, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); return responsePayload(authJsonMode ? "client.provider-profiles.set-auth-json" : "client.provider-profiles.set-key", response, context, { route: route("PUT", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false }); } if (subcommand === "set-auth-json" || subcommand === "auth-json" || subcommand === "set-auth") { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); if (context.parsed.authJsonStdin !== true) throw cliError("auth_json_stdin_required", "set-auth-json requires --auth-json-stdin so auth.json is not passed in argv", { command: "provider-profiles set-auth-json PROFILE --auth-json-stdin" }); const authJson = await providerProfileAuthJsonFromStdin(context); const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/credential`; const response = await requestJson({ ...context, method: "PUT", path: pathName, body: { authJson }, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); return responsePayload("client.provider-profiles.set-auth-json", response, context, { route: route("PUT", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false }); } if (subcommand === "config") { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/config`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.provider-profiles.config", response, context, { route: route("GET", pathName), profile, body: providerProfileConfigBodyForCli(response.body, context.parsed) }); } if (subcommand === "set-config") { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); if (context.parsed.configStdin !== true) throw cliError("config_stdin_required", "set-config requires --config-stdin so config.toml is not passed in argv", { command: "provider-profiles set-config PROFILE --config-stdin" }); const configToml = await providerProfileConfigFromStdin(context); const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/config`; const response = await requestJson({ ...context, method: "PUT", path: pathName, body: { configToml }, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); return responsePayload("client.provider-profiles.set-config", response, context, { route: route("PUT", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false }); } if (subcommand === "validate") { return providerProfilesValidate(context); } if (subcommand === "validation" || subcommand === "result") { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); const validationId = requiredText(context.parsed.validationId ?? context.rest[2], "validationId"); const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/validations/${encodeURIComponent(validationId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.provider-profiles.validation", response, context, { route: route("GET", pathName), profile, validationId, body: providerProfileBodyForCli(response.body, context.parsed) }); } throw cliError("unsupported_provider_profiles_command", `unsupported provider-profiles command: ${subcommand}`, { subcommand }); } async function providerProfilesValidate(context: any) { const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]); const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/validate`; const response = await requestJson({ ...context, method: "POST", path: pathName, body: {}, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); const body = providerProfileBodyForCli(response.body, context.parsed); const validationId = text(response.body?.data?.validationId ?? response.body?.validationId); if (context.parsed.wait !== true || !responseSucceeded(response) || !validationId) { return responsePayload("client.provider-profiles.validate", response, context, { route: route("POST", pathName), profile, validationId: validationId || undefined, waited: false, body, waitPolicy: providerProfileWaitPolicy(profile, validationId) }); } const timeoutMs = Math.min(numberOption(context.parsed.timeoutMs) ?? 120000, 120000); const pollIntervalMs = Math.max(numberOption(context.parsed.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS, 500); const deadline = Date.now() + timeoutMs; let polls = 0; let lastResponse = response; let lastBody = body; while (Date.now() < deadline) { const status = validationTerminalStatus(lastResponse.body); if (status.terminal) { return responsePayload("client.provider-profiles.validate", lastResponse, context, { route: route("GET", `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/validations/${encodeURIComponent(validationId)}`), profile, validationId, waited: true, timedOut: false, polls, body: lastBody, waitPolicy: providerProfileWaitPolicy(profile, validationId) }); } await context.sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))); polls += 1; const resultPath = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/validations/${encodeURIComponent(validationId)}`; lastResponse = await requestJson({ ...context, method: "GET", path: resultPath, timeoutMs: Math.min(DEFAULT_TIMEOUT_MS, pollIntervalMs + 2000) }); lastBody = providerProfileBodyForCli(lastResponse.body, context.parsed); } return ok("client.provider-profiles.validate", { baseUrl: response.runtimeEndpoint?.baseUrl ?? baseUrl(context.parsed, context.env, context.endpointKind ?? "web"), route: route("GET", `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/validations/${encodeURIComponent(validationId)}`), profile, validationId, waited: true, timedOut: true, polls, timeoutMsRequested: numberOption(context.parsed.timeoutMs) ?? 120000, timeoutMsEffective: timeoutMs, body: lastBody, waitPolicy: providerProfileWaitPolicy(profile, validationId) }, "timeout"); } function providerProfilesHelp() { return ok("client.provider-profiles.help", { serviceRuntime: false, imagePublished: false, sameOrigin: true, valuesPrinted: false, commands: [ "list", "config PROFILE", "remove PROFILE", "set-config PROFILE --config-stdin", "set-key PROFILE --key-stdin", "set-auth-json PROFILE --auth-json-stdin", "validate PROFILE [--wait] [--timeout-ms N] [--poll-interval-ms N]", "validation PROFILE VALIDATION_ID" ], routes: { list: route("GET", "/v1/admin/provider-profiles"), config: route("GET", "/v1/admin/provider-profiles/:profile/config"), remove: route("DELETE", "/v1/admin/provider-profiles/:profile"), setConfig: route("PUT", "/v1/admin/provider-profiles/:profile/config"), setKey: route("PUT", "/v1/admin/provider-profiles/:profile/credential"), validate: route("POST", "/v1/admin/provider-profiles/:profile/validate"), validation: route("GET", "/v1/admin/provider-profiles/:profile/validations/:validationId") } }); } async function providerProfileApiKeyFromStdin(context: any) { const apiKey = context.stdinText !== undefined ? String(context.stdinText).trimEnd() : await readStdin(); if (apiKey.trim().length < 8) throw cliError("api_key_too_short", "provider profile API key from stdin is too short", { minLength: 8 }); return apiKey.trim(); } async function providerProfileConfigFromStdin(context: any) { const configToml = context.stdinText !== undefined ? String(context.stdinText) : await readRawStdin(); if (!configToml.trim()) throw cliError("config_toml_empty", "provider profile config.toml from stdin must not be empty"); return configToml; } async function providerProfileAuthJsonFromStdin(context: any) { const authJson = context.stdinText !== undefined ? String(context.stdinText) : await readRawStdin(); if (!authJson.trim()) throw cliError("auth_json_empty", "provider profile auth.json from stdin must not be empty"); try { const parsed = JSON.parse(authJson); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("auth.json object expected"); } catch { throw cliError("auth_json_invalid", "provider profile auth.json from stdin must be a JSON object"); } return authJson; } function normalizeProviderProfile(value: unknown) { const profile = text(value).toLowerCase(); const normalized = profile === "codex" ? "codex-api" : profile; const backendProfile = normalized === "gpt.pika" ? "gpt-pika" : normalized; if (/^[a-z0-9][a-z0-9.-]{0,63}$/u.test(normalized) && /^[a-z0-9][a-z0-9-]{0,63}$/u.test(backendProfile) && normalized !== "runtime-default") return normalized; throw cliError("invalid_provider_profile", "provider profile must be a lowercase id such as deepseek, minimax-m3, dsflash-go, codex-api, or gpt.pika", { profile, alias: "codex -> codex-api, gpt.pika -> gpt-pika", pattern: "public=^[a-z0-9][a-z0-9.-]{0,63}$ backend=^[a-z0-9][a-z0-9-]{0,63}$" }); } function validationTerminalStatus(body: any) { const status = text(body?.data?.result?.terminalStatus ?? body?.data?.status ?? body?.result?.terminalStatus ?? body?.status).toLowerCase(); return { status, terminal: ["completed", "failed", "canceled", "cancelled", "timeout", "blocked"].includes(status) }; } function providerProfileWaitPolicy(profile: string, validationId: string) { return pruneUndefined({ webEquivalent: true, shortConnection: true, remotePassthroughSafe: true, defaultWait: false, profile, validationId: text(validationId) || undefined, maxTimeoutMs: 120000, nextCommands: text(validationId) ? [`hwlab-cli client provider-profiles validation ${profile} ${validationId}`, `hwlab-cli client provider-profiles validate ${profile} --wait --timeout-ms 120000`] : [`hwlab-cli client provider-profiles validate ${profile} --wait --timeout-ms 120000`] }); } function normalizeToolId(value: unknown) { const toolId = text(value).replace(/-/gu, "_"); if (["hwpod", "unidesk_ssh", "trans_cmd", "github_pr"].includes(toolId)) return toolId; throw cliError("invalid_tool_id", "tool must be hwpod, unidesk_ssh, trans_cmd, or github_pr", { toolId }); } function normalizeAccessUserRole(value: unknown) { const role = text(value); if (role === "admin" || role === "user") return role; throw cliError("invalid_access_user_role", "role must be admin or user", { role }); } function normalizeAccessUserStatus(value: unknown) { const status = text(value); if (status === "active" || status === "disabled") return status; throw cliError("invalid_access_user_status", "status must be active or disabled", { status }); } async function authStatus(context: any) { const endpoint = runtimeEndpoint(context.parsed, context.env, "web"); const localSession = sessionStateSummary(await readSessionState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() })); const apiKey = explicitApiKey(context.parsed, context.env); return ok("client.auth.status", { baseUrl: endpoint.baseUrl, runtimeEndpoint: runtimeEndpointVisibility(endpoint), authBoundary: { defaultAuthMethod: "api-key", webSessionDefault: false, webSessionDiagnosticFlags: ["--web-session", "--cookie", "HWLAB_SESSION_COOKIE"], localSessionDefault: false }, stateFile: localSession.stateFile, localSession, apiKey: apiKey ? { source: "env-or-explicit", prefix: apiKeyPrefixSummary(apiKey), authMethod: "api-key" } : { source: null, prefix: null, authMethod: null }, nextCommands: authNextCommands({ baseUrl: endpoint.baseUrl, username: text(context.parsed.username ?? context.env.HWLAB_USERNAME) || "admin", stateFile: localSession.stateFile }) }); } async function authProfiles(context: any) { const dir = profileStateDir(context.parsed, context.cwd ?? process.cwd(), context.env); let entries: string[] = []; try { entries = await readdir(dir); } catch (error) { if (error?.code !== "ENOENT") throw error; } const profiles = []; for (const entry of entries.filter((name) => name.endsWith(".json")).sort()) { const profile = entry.slice(0, -5); const state = await readSessionState({ parsed: { ...context.parsed, profile }, env: context.env, cwd: context.cwd ?? process.cwd() }); profiles.push({ profile, ...sessionStateSummary(state) }); } return ok("client.auth.profiles", { baseUrl: baseUrl(context.parsed, context.env), runtimeEndpoint: runtimeEndpointVisibility(runtimeEndpoint(context.parsed, context.env, "web")), profileDir: dir, profiles, profileCount: profiles.length }); } async function authLogin(context: any) { const { parsed, env } = context; const username = text(parsed.username ?? env.HWLAB_USERNAME) || "admin"; const password = await passwordValue(context); const response = await requestJson({ ...context, method: "POST", path: "/auth/login", body: { username, password }, auth: false }); const cookie = cookieFromResponse(response); const success = isHttpSuccess(response) && response.body?.authenticated === true; if (success && cookie) { await saveSession(context, { baseUrl: response.runtimeEndpoint?.baseUrl ?? baseUrl(parsed, env), cookie, user: safeUser(response.body?.user ?? response.body?.actor), expiresAt: textOrNull(response.body?.expiresAt), updatedAt: context.now() }); } return responsePayload("client.auth.login", response, context, { route: route("POST", "/auth/login"), username, cookieStored: Boolean(success && cookie), body: authBodySummary(response.body) }); } async function runtimeCommand(context: any) { const subcommand = context.rest[0] || "routes"; if (wantsHelp(context)) return runtimeHelp(); if (subcommand !== "routes" && subcommand !== "pods") { throw cliError("unsupported_runtime_command", `unsupported runtime command: ${subcommand}`, { subcommand }); } const response = await requestJson({ ...context, method: "GET", path: "/v1/live-builds", auth: false }); const services = liveBuildServices(response.body); const requested = text(context.parsed.serviceId); const selected = services.filter((item: any) => { const serviceId = text(item?.serviceId ?? item?.id ?? item?.name); return !requested || serviceId === requested; }).map(runtimeRouteSummary).filter(Boolean); return responsePayload("client.runtime.routes", response, context, { route: route("GET", "/v1/live-builds"), discovery: { ok: true, source: "cloud-web:/v1/live-builds", serviceCount: services.length, routeCount: selected.length, readyRouteCount: selected.filter((item: any) => Boolean(item.unideskRoute)).length }, serviceId: requested || null, items: selected, body: responseBodyForCli(response.body, context.parsed) }); } function runtimeHelp() { return ok("client.runtime.help", { serviceRuntime: false, imagePublished: false, commands: [ "routes [--service-id SERVICE]", "pods [--service-id SERVICE]" ] }); } function liveBuildServices(body: any) { const builds = Array.isArray(body?.builds) ? body.builds : []; if (builds.length > 0) return builds; return Array.isArray(body?.services) ? body.services : []; } function runtimeRouteSummary(value: any) { const serviceId = text(value?.serviceId ?? value?.id ?? value?.name); const runtime = value?.runtime && typeof value.runtime === "object" ? value.runtime : {}; const pod = runtime?.pod && typeof runtime.pod === "object" ? runtime.pod : {}; const podText = typeof value?.pod === "string" ? value.pod : typeof runtime?.pod === "string" ? runtime.pod : ""; const podName = text( value?.podName ?? runtime?.podName ?? pod?.name ?? podText ?? value?.live?.podName ?? value?.health?.podName ); const namespace = text( value?.namespace ?? runtime?.namespace ?? pod?.namespace ?? value?.live?.namespace ?? value?.desiredState?.namespace ) || "hwlab-v02"; const container = text( value?.container ?? value?.containerName ?? runtime?.container ?? runtime?.containerName ?? pod?.container ?? pod?.containerName ?? value?.live?.containerName ) || defaultRuntimeContainer(serviceId); if (!serviceId && !podName) return null; const unideskRoute = normalizeUnideskPodRoute( text(value?.unideskRoute ?? runtime?.unideskRoute ?? pod?.unideskRoute), namespace, podName, container ); return pruneUndefined({ serviceId, podName, namespace, container, unideskRoute, source: "cloud-web:/v1/live-builds", useWith: unideskRoute ? `bun scripts/cli.ts ssh '${unideskRoute}' script -- 'pwd'` : undefined }); } function defaultRuntimeContainer(serviceId: string) { if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-skills"].includes(serviceId)) return serviceId; return ""; } function normalizeUnideskPodRoute(routeValue: string, namespace: string, podName: string, container: string) { if (podName) return `G14:k3s:${namespace}:pod:${podName}${container ? `:${container}` : ""}`; if (!routeValue) return null; return routeValue.replace(/:pod\//gu, ":pod:"); } async function gatewayCommand(context: any) { const subcommand = context.rest[0] || "sessions"; if (["help", "--help", "-h"].includes(subcommand)) return gatewayHelp(); const apiContext = gatewayApiContext(context); if (subcommand === "sessions") { const response = await requestJson({ ...apiContext, method: "GET", path: "/v1/gateway/sessions", auth: context.parsed.noAuth === true ? false : context.parsed.auth === true }); return responsePayload("client.gateway.sessions", response, apiContext, { route: route("GET", "/v1/gateway/sessions"), body: responseBodyForCli(response.body, context.parsed) }); } if (subcommand === "invoke") { const command = requiredText(context.parsed.command ?? context.rest.slice(1).join(" "), "command"); const result = await invokeGatewayShell(apiContext, { name: "invoke", command, timeoutMs: numberOption(context.parsed.commandTimeoutMs) ?? numberOption(context.parsed.timeoutMs) ?? 30000 }); return ok("client.gateway.invoke", { status: result.ok ? "succeeded" : "failed", result }, result.ok ? "succeeded" : "failed"); } if (subcommand === "pressure") return gatewayPressure(apiContext); throw cliError("unsupported_gateway_command", `unsupported gateway command: ${subcommand}`, { subcommand }); } function gatewayHelp() { return ok("client.gateway.help", { serviceRuntime: false, imagePublished: false, commands: [ "sessions", "invoke --command CMD [--gateway-session-id ID]", "pressure [--gateway-session-id ID] [--large-bytes N] [--parallel N]" ] }); } function gatewayApiContext(context: any) { return { ...context, endpointKind: "api" }; } async function gatewayPressure(context: any) { const largeBytes = boundedInteger(context.parsed.largeBytes, 131072, 4096, 1048576); const parallel = boundedInteger(context.parsed.parallel, 8, 0, 32); const requestTimeoutMs = boundedInteger(context.parsed.requestTimeoutMs, 45000, 1000, 180000); const timeoutScenarioMs = boundedInteger(context.parsed.timeoutScenarioMs, 1000, 250, 30000); const startedAt = Date.now(); const scenarios = gatewayPressureScenarios({ largeBytes, timeoutScenarioMs }); const results = []; for (const scenario of scenarios) { results.push(await invokeGatewayShell(context, { ...scenario, requestTimeoutMs })); } if (parallel > 0) { const parallelResults = await Promise.all(Array.from({ length: parallel }, (_, index) => invokeGatewayShell(context, { name: `parallel-${index + 1}`, kind: "parallel", command: powershellEncodedCommand(`Start-Sleep -Milliseconds 3500; Write-Output 'parallel-${index + 1}-ok'`), timeoutMs: 12000, requestTimeoutMs, allowGatewayBusy: true, marker: `parallel-${index + 1}-ok` }))); results.push(...parallelResults); } const failures = results.filter((item) => item.ok !== true); return ok("client.gateway.pressure", { status: failures.length > 0 ? "failed" : "succeeded", baseUrl: baseUrl(context.parsed, context.env, "api"), runtimeEndpoint: runtimeEndpointVisibility(runtimeEndpoint(context.parsed, context.env, "api")), gatewaySessionId: gatewaySelector(context).gatewaySessionId, scenarioCount: results.length, failedCount: failures.length, elapsedMs: Date.now() - startedAt, config: { largeBytes, parallel, requestTimeoutMs, timeoutScenarioMs }, results, recommendation: failures.length > 0 ? "Gateway transport pressure found failures; inspect failed result.request/result.rpcSummary before continuing HWPOD work." : "Gateway transport pressure passed: large stdout/stderr and over-capacity parallel requests returned bounded structured results without request blackholes." }, failures.length > 0 ? "failed" : "succeeded"); } function gatewayPressureScenarios({ largeBytes, timeoutScenarioMs }: { largeBytes: number; timeoutScenarioMs: number }) { return [ { name: "small-stdout", kind: "small", command: "echo hwlab-gateway-pressure-small-ok", timeoutMs: 10000, marker: "hwlab-gateway-pressure-small-ok" }, { name: "large-stdout", kind: "large-stdout", command: powershellEncodedCommand(`[Console]::Out.Write(('O' * ${largeBytes}))`), timeoutMs: 20000, expectStdoutTruncated: true }, { name: "long-single-line", kind: "long-line", command: powershellEncodedCommand(`[Console]::Out.Write(('L' * ${largeBytes}))`), timeoutMs: 20000, expectStdoutTruncated: true }, { name: "stderr-flood", kind: "stderr", command: powershellEncodedCommand(`[Console]::Error.Write(('E' * ${largeBytes}))`), timeoutMs: 20000, expectStderrTruncated: true }, { name: "timeout-kill", kind: "timeout", command: powershellEncodedCommand("Start-Sleep -Seconds 5; Write-Output 'timeout-missed'"), timeoutMs: timeoutScenarioMs, expectTimedOut: true } ]; } function powershellEncodedCommand(script: string) { const encoded = Buffer.from(script, "utf16le").toString("base64"); return `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`; } async function invokeGatewayShell(context: any, scenario: any) { const selector = gatewaySelector(context); const traceId = text(context.parsed.traceId) || makeId(`trc_gateway_${scenario.name.replace(/[^a-z0-9]+/giu, "_")}`); const requestId = text(context.parsed.id) || makeId(`req_gateway_${scenario.name.replace(/[^a-z0-9]+/giu, "_")}`); const body = { gatewaySessionId: selector.gatewaySessionId, resourceId: selector.resourceId, capabilityId: selector.capabilityId, projectId: selector.projectId, input: { command: scenario.command, timeoutMs: scenario.timeoutMs } }; const startedAt = Date.now(); const response = await requestJson({ ...context, method: "POST", path: "/v1/rpc/hardware.invoke.shell", body, auth: context.parsed.auth === true, timeoutMs: scenario.requestTimeoutMs ?? numberOption(context.parsed.requestTimeoutMs) ?? DEFAULT_TIMEOUT_MS, extraHeaders: { "x-trace-id": traceId, "x-request-id": requestId } }); return summarizeGatewayScenario({ scenario, response, traceId, requestId, startedAt }); } function gatewaySelector(context: any) { return { gatewaySessionId: text(context.parsed.gatewaySessionId) || DEFAULT_GATEWAY_SESSION_ID, resourceId: text(context.parsed.resourceId) || DEFAULT_GATEWAY_RESOURCE_ID, capabilityId: text(context.parsed.capabilityId) || DEFAULT_GATEWAY_CAPABILITY_ID, projectId: text(context.parsed.projectId) || DEFAULT_GATEWAY_PROJECT_ID }; } function summarizeGatewayScenario({ scenario, response, traceId, requestId, startedAt }: any) { const rpc = response.body && typeof response.body === "object" ? response.body : {}; const result = rpc.result && typeof rpc.result === "object" ? rpc.result : {}; const dispatch = result.dispatch && typeof result.dispatch === "object" ? result.dispatch : {}; const stdout = String(dispatch.stdout ?? ""); const stderr = String(dispatch.stderr ?? ""); const gatewayBusy = dispatch.error?.data?.reason === "gateway_busy" || /gateway is busy/iu.test(String(dispatch.error?.message ?? dispatch.message ?? "")); const okByKind = gatewayScenarioPass({ scenario, response, dispatch, stdout, stderr, gatewayBusy }); return pruneUndefined({ name: scenario.name, kind: scenario.kind, ok: okByKind.ok, reason: okByKind.reason, httpStatus: response.status, elapsedMs: Date.now() - startedAt, requestElapsedMs: response.elapsedMs, traceId, requestId, operationId: result.operationId ?? dispatch.operationId, rpcStatus: result.status, dispatchStatus: dispatch.dispatchStatus, shellExecuted: dispatch.shellExecuted, exitCode: dispatch.exitCode, timedOut: dispatch.timedOut, gatewayBusy, stdoutBytes: Buffer.byteLength(stdout, "utf8"), stderrBytes: Buffer.byteLength(stderr, "utf8"), stdoutTruncated: dispatch.stdoutTruncated, stderrTruncated: dispatch.stderrTruncated, stdoutSha256: stdout ? sha256(stdout) : undefined, stderrSha256: stderr ? sha256(stderr) : undefined, stdoutPreview: stdout ? preview(stdout, 160) : undefined, stderrPreview: stderr ? preview(stderr, 160) : undefined, error: rpc.error ?? dispatch.error, message: dispatch.message, commandPreview: preview(scenario.command, 220) }); } function gatewayScenarioPass({ scenario, response, dispatch, stdout, stderr, gatewayBusy }: any) { if (!isHttpSuccess(response)) return { ok: false, reason: `http_${response.status}` }; if (scenario.allowGatewayBusy && gatewayBusy) return { ok: true, reason: "structured_gateway_busy" }; if (scenario.expectTimedOut) { return dispatch.timedOut === true || dispatch.dispatchStatus === "timed_out" ? { ok: true, reason: "timeout_structured" } : { ok: false, reason: "timeout_not_observed" }; } if (scenario.expectStdoutTruncated && dispatch.stdoutTruncated !== true) return { ok: false, reason: "stdout_not_truncated" }; if (scenario.expectStderrTruncated && dispatch.stderrTruncated !== true) return { ok: false, reason: "stderr_not_truncated" }; if (scenario.marker && !stdout.includes(scenario.marker)) return { ok: false, reason: "marker_missing" }; if (dispatch.exitCode !== 0) return { ok: false, reason: `exit_${dispatch.exitCode}` }; if (scenario.expectStdoutTruncated && Buffer.byteLength(stdout, "utf8") === 0) return { ok: false, reason: "stdout_empty" }; if (scenario.expectStderrTruncated && Buffer.byteLength(stderr, "utf8") === 0) return { ok: false, reason: "stderr_empty" }; return { ok: true, reason: "completed" }; } async function agentCommand(context: any) { const subcommand = context.rest[0] || "send"; if (wantsHelp(context)) return agentHelp(); if (subcommand === "session" || subcommand === "sessions") return agentSessionCommand(context); if (subcommand === "send") return agentSend(context); if (subcommand === "composer") return agentComposer(context); if (subcommand === "result") { const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId); const pathName = `/v1/agent/turns/${encodeURIComponent(traceId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.agent.result", response, context, { route: route("GET", pathName), traceId, body: responseBodyForCli(response.body, context.parsed), traceResultSummary: traceResultSummaryForCli(response.body, { traceId, command: "result" }) }); } if (subcommand === "trace") { const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId); const pathName = `/v1/agent/traces/${encodeURIComponent(traceId)}${agentTraceQueryString(context.parsed)}`; const turnPath = `/v1/agent/turns/${encodeURIComponent(traceId)}`; const turnResponse = await requestJson({ ...context, method: "GET", path: turnPath }); const response = await requestJson({ ...context, method: "GET", path: pathName }); const traceBody = traceBodyForCli(response.body, context.parsed); return responsePayload("client.agent.trace", response, context, { route: route("GET", pathName), turnRoute: route("GET", turnPath), traceId, body: traceBody, turnStatus: responseBodyForCli(turnResponse.body, context.parsed), traceResultSummary: traceResultSummaryForCli(turnResponse.body ?? response.body, { traceId, command: "trace", renderedTrace: traceBody }), ...traceResponseAliases(traceBody, context.parsed) }); } if (subcommand === "inspect") { return agentInspect(context); } if (subcommand === "steer") return agentSteer(context); if (subcommand === "cancel") { const traceId = requiredText(context.rest[1] ?? context.parsed.traceId, "traceId"); const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat/cancel", body: clean({ traceId, sessionId: text(context.parsed.sessionId), threadId: text(context.parsed.threadId) }), extraHeaders: { "x-trace-id": traceId } }); return responsePayload("client.agent.cancel", response, context, { route: route("POST", "/v1/agent/chat/cancel"), traceId }); } throw cliError("unsupported_agent_command", `unsupported agent command: ${subcommand}`, { subcommand }); } async function agentInspect(context: any) { const pathName = agentInspectPath(context); const response = await requestJson({ ...context, method: "GET", path: pathName }); const traceId = text(context.parsed.traceId); return responsePayload("client.agent.inspect", response, context, { route: route("GET", pathName), body: responseBodyForCli(response.body, context.parsed), traceResultSummary: traceResultSummaryForCli(response.body, { traceId, command: "inspect" }) }); } function traceResultSummaryForCli(source: any, options: { traceId?: string; command?: string; renderedTrace?: any } = {}) { const rendered = options.renderedTrace && typeof options.renderedTrace === "object" ? options.renderedTrace : null; const prompt = tracePromptForCli(source); const toolCalls = traceToolCallsForCli(source, rendered); const ids = completeTraceResultIdsFromToolCalls(traceResultIdsForCli(source, rendered, options.traceId), toolCalls); const finalResponse = traceFinalResponseForCli(source, rendered, ids.traceId); const agentMessages = traceAgentMessagesForCli(source, rendered, finalResponse); const diagnostics = traceDiagnosticsForCli(source, rendered); const counts = traceCountsForCli(source, rendered, toolCalls.length); const upstreamGaps = traceResultUpstreamGaps({ ids, prompt, finalResponse, toolCalls, agentMessages, diagnostics }); return pruneUndefined({ command: text(options.command) || undefined, ids, prompt, diagnostics, counts, toolCalls, toolCallCount: toolCalls.length, agentMessages, finalResponse, upstreamGaps: upstreamGaps.length > 0 ? upstreamGaps : undefined, valuesPrinted: false, fullBodyAvailable: true }); } function completeTraceResultIdsFromToolCalls(ids: any, toolCalls: any[]) { const source = toolCalls.find((call) => call?.runId || call?.commandId || call?.runnerId) ?? null; if (!source) return ids; return pruneUndefined({ ...ids, runId: ids.runId ?? (text(source.runId) || undefined), commandId: ids.commandId ?? (text(source.commandId) || undefined), runnerId: ids.runnerId ?? (text(source.runnerId) || undefined) }); } function traceResultIdsForCli(source: any, rendered: any, traceId: unknown) { const agentRun = traceAgentRunForCli(source, rendered); const terminal = source?.terminalEvidence && typeof source.terminalEvidence === "object" ? source.terminalEvidence : null; const session = source?.session && typeof source.session === "object" ? source.session : null; const sessionReuse = source?.sessionReuse && typeof source.sessionReuse === "object" ? source.sessionReuse : null; return pruneUndefined({ promptId: text(source?.promptId ?? source?.prompt?.id) || undefined, traceId: text(traceId ?? source?.traceId ?? rendered?.traceId ?? terminal?.traceId ?? source?.traceSummary?.traceId ?? rendered?.traceSummary?.traceId) || undefined, conversationId: text(source?.conversationId ?? session?.conversationId ?? sessionReuse?.conversationId ?? terminal?.conversationId ?? source?.agentRun?.conversationId) || undefined, sessionId: text(source?.sessionId ?? session?.sessionId ?? sessionReuse?.sessionId ?? terminal?.sessionId) || undefined, threadId: text(source?.threadId ?? session?.threadId ?? sessionReuse?.threadId ?? terminal?.threadId ?? source?.agentRun?.threadId) || undefined, runId: text(agentRun?.runId) || undefined, commandId: text(agentRun?.commandId) || undefined, runnerId: text(agentRun?.runnerId) || undefined, messageId: text(source?.messageId ?? source?.finalResponse?.messageId ?? terminal?.finalResponse?.messageId) || undefined }); } function traceAgentRunForCli(source: any, rendered: any) { const candidates = [ source?.agentRun, source?.runner, source?.traceSummary?.agentRun, source?.terminalEvidence?.agentRun, source?.terminalEvidence?.traceSummary?.agentRun, rendered?.traceSummary?.agentRun, rendered?.terminalEvidence?.agentRun ]; return candidates.find((candidate) => candidate && typeof candidate === "object") ?? null; } function tracePromptForCli(source: any) { const events = traceEventsForCli(source); const promptEvent = events.find((event: any) => text(event?.promptId ?? event?.promptSummary ?? event?.promptPreview ?? event?.prompt ?? event?.userPrompt ?? event?.inputPreview ?? event?.input)); const promptText = text(source?.promptSummary ?? source?.promptPreview ?? source?.prompt?.summary ?? source?.prompt?.text ?? promptEvent?.promptSummary ?? promptEvent?.promptPreview ?? promptEvent?.prompt ?? promptEvent?.userPrompt ?? promptEvent?.inputPreview ?? promptEvent?.input); const promptId = text(source?.promptId ?? source?.prompt?.id ?? promptEvent?.promptId ?? promptEvent?.prompt?.id); const promptSha256 = text(source?.promptSha256 ?? source?.prompt?.sha256 ?? promptEvent?.promptSha256); if (!promptText && !promptId && !promptSha256) return undefined; return pruneUndefined({ promptId: promptId || undefined, promptSha256: promptSha256 || undefined, textPreview: clippedText(promptText, 500), textChars: promptText ? promptText.length : undefined, sourceSeq: promptEvent?.seq ?? promptEvent?.sourceSeq, valuesPrinted: false }); } function traceDiagnosticsForCli(source: any, rendered: any) { const agentRun = traceAgentRunForCli(source, rendered); const providerTrace = source?.providerTrace ?? source?.agentRun?.providerTrace ?? agentRun?.providerTrace; return pruneUndefined({ status: text(source?.status ?? rendered?.status) || undefined, traceStatus: text(source?.traceStatus ?? rendered?.traceStatus ?? rendered?.status) || undefined, terminalStatus: text(source?.terminalStatus ?? source?.traceSummary?.terminalStatus ?? rendered?.traceSummary?.terminalStatus ?? agentRun?.terminalStatus ?? agentRun?.status) || undefined, provider: text(source?.provider ?? source?.runner?.provider ?? source?.longLivedSessionGate?.provider ?? providerTrace?.backendProfile) || undefined, providerProfile: text(source?.providerProfile ?? source?.profile ?? source?.agentRun?.backendProfile ?? agentRun?.backendProfile) || undefined, adapter: text(agentRun?.adapter ?? providerTrace?.transport) || undefined, namespace: text(agentRun?.namespace ?? providerTrace?.namespace ?? source?.runner?.namespace) || undefined, lane: text(agentRun?.namespace ?? providerTrace?.namespace ?? source?.runner?.namespace) || undefined, runnerJobName: text(agentRun?.jobName ?? source?.runner?.jobName) || undefined, runStatus: text(agentRun?.runStatus) || undefined, commandState: text(agentRun?.commandState) || undefined, failureKind: text(agentRun?.failureKind ?? providerTrace?.failureKind) || undefined, valuesPrinted: false }); } function traceCountsForCli(source: any, rendered: any, toolCallCount: number) { const events = traceEventsForCli(source); const sourceEventCount = source?.traceSummary?.sourceEventCount ?? rendered?.traceSummary?.sourceEventCount ?? source?.eventCount ?? events.length; return pruneUndefined({ sourceEventCount: sourceEventCount || undefined, renderedRowCount: rendered?.renderedRowCount, returnedRowCount: rendered?.returnedRowCount, noiseEventCount: rendered?.noiseEventCount, omittedNoiseCount: rendered?.omittedNoiseCount, toolCallCount }); } function traceFinalResponseForCli(source: any, rendered: any, traceId: unknown) { const terminal = source?.terminalEvidence && typeof source.terminalEvidence === "object" ? source.terminalEvidence : null; const final = firstObjectForCli(source?.finalResponse, terminal?.finalResponse, rendered?.finalResponse); const finalText = text(final?.text ?? final?.textPreview ?? assistantText(source) ?? rendered?.assistantText); if (!finalText && !final) return undefined; return pruneUndefined({ role: text(final?.role) || "assistant", status: text(final?.status ?? source?.status ?? rendered?.status) || undefined, traceId: text(final?.traceId ?? traceId) || undefined, messageId: text(final?.messageId ?? source?.messageId) || undefined, text: clippedText(finalText, 4000), textChars: finalText ? finalText.length : final?.textChars, valuesPrinted: false }); } function traceAgentMessagesForCli(source: any, rendered: any, finalResponse: any) { const messages = []; const seen = new Set(); const pushMessage = (value: any) => { const body = text(value?.text ?? value?.body ?? value?.message ?? value?.content); if (!body) return; const key = `${value?.kind ?? value?.role ?? "message"}:${body}`; if (seen.has(key)) return; seen.add(key); messages.push(pruneUndefined({ kind: text(value?.kind) || undefined, role: text(value?.role) || "assistant", status: text(value?.status) || undefined, sourceSeq: value?.sourceSeq ?? value?.seq, terminal: value?.terminal === true ? true : undefined, text: clippedText(body, 1200), textChars: body.length, valuesPrinted: false })); }; if (Array.isArray(source?.assistantStreams)) { for (const item of source.assistantStreams.slice(-3)) pushMessage({ ...item, kind: "assistant-stream", text: item?.text ?? item?.lastChunk }); } for (const row of firstArray(rendered?.rows, source?.rows)) { if (row?.terminal === true || /助手|assistant/iu.test(text(row?.header))) pushMessage({ ...row, kind: "trace-row", text: row?.body }); } if (finalResponse) pushMessage({ ...finalResponse, kind: "final-response" }); return messages.slice(0, 8); } function traceToolCallsForCli(source: any, rendered: any) { const calls = []; for (const event of traceEventsForCli(source)) { const call = traceToolCallFromEvent(event); if (call) calls.push(call); } for (const row of firstArray(rendered?.rows, source?.rows)) { const call = traceToolCallFromRow(row); if (call) calls.push(call); } if (calls.length === 0 && Array.isArray(source?.toolCalls)) { for (const item of source.toolCalls) { const call = traceToolCallFromObject(item); if (call) calls.push(call); } } return dedupeTraceToolCalls(calls).slice(0, 20); } function traceEventsForCli(source: any) { return firstArray(source?.events, source?.runnerTrace?.events, source?.terminalEvidence?.events); } function traceToolCallFromObject(value: any) { if (!value || typeof value !== "object") return null; return pruneUndefined({ source: "toolCalls", status: text(value.status) || undefined, toolName: text(value.toolName ?? value.name) || undefined, command: text(value.command) || undefined, runId: text(value.runId) || undefined, commandId: text(value.commandId) || undefined, runnerId: text(value.runnerId) || undefined, exitCode: value.exitCode, durationMs: value.durationMs, outputBytes: value.outputBytes, outputTruncated: value.outputTruncated, outputPreview: clippedText(text(value.outputPreview ?? value.stdoutSummary ?? value.stderrSummary ?? value.message), 500), valuesPrinted: false }); } function traceToolCallFromEvent(event: any) { if (!event || typeof event !== "object") return null; const command = text(event.command); const hasCommandOutput = command || /commandExecution/iu.test(text(event.message ?? event.stdoutSummary ?? event.stderrSummary)); if (!hasCommandOutput) return null; const outputText = traceToolOutputText(event); return pruneUndefined({ source: "runnerTrace.events", sourceSeq: event.sourceSeq ?? event.seq, status: traceToolStatusForCli(event.status, event.exitCode), label: text(event.label) || undefined, toolName: text(event.toolName ?? event.name) || undefined, command: command || undefined, runId: text(event.runId) || undefined, commandId: text(event.commandId) || undefined, runnerId: text(event.runnerId) || undefined, exitCode: event.exitCode, durationMs: event.durationMs, outputBytes: event.outputBytes, outputTruncated: event.outputTruncated, outputPreview: outputText ? clippedText(outputText, 500) : undefined, valuesPrinted: false }); } function traceToolCallFromRow(row: any) { if (!row || typeof row !== "object") return null; const rowId = text(row.rowId); const body = text(row.body); const header = text(row.header); const isTool = rowId.startsWith("tool:") || /commandExecution|exitCode=/iu.test(`${header} ${body}`); if (!isTool) return null; const parsed = parseRenderedToolBody(body); return pruneUndefined({ source: "trace-render-row", rowId: rowId || undefined, sourceSeq: row.seq, status: traceToolStatusForCli(row.tone === "ok" ? "completed" : row.tone, parsed.exitCode), label: header || undefined, toolName: /commandExecution/u.test(header) ? "commandExecution" : undefined, command: parsed.command, exitCode: parsed.exitCode, outputPreview: body ? clippedText(body, 500) : undefined, valuesPrinted: false }); } function traceToolOutputText(event: any) { const stdout = text(event.stdoutSummary ?? event.stdoutPreview ?? event.stdout); const stderr = text(event.stderrSummary ?? event.stderrPreview ?? event.stderr); const message = text(event.message); const value = stdout || stderr || (/commandExecution inProgress/iu.test(message) ? "" : message); return value; } function parseRenderedToolBody(value: string) { const exitMatch = value.match(/\bexitCode=(-?\d+)\b/u); const streamMatch = value.match(/^(.+?)\s+(?:stdout|stderr):\s+/u); return pruneUndefined({ command: text(streamMatch?.[1]) || undefined, exitCode: exitMatch ? Number.parseInt(exitMatch[1], 10) : undefined }); } function traceToolStatusForCli(status: unknown, exitCode: unknown) { const code = typeof exitCode === "number" ? exitCode : Number.isFinite(Number(exitCode)) ? Number(exitCode) : null; if (code !== null) return code === 0 ? "completed" : "failed"; const value = text(status); if (value === "ok") return "completed"; return value || undefined; } function dedupeTraceToolCalls(calls: any[]) { const byKey = new Map(); for (const call of calls) { const key = [call.commandId, call.command, call.sourceSeq, call.rowId].map((part) => text(part)).filter(Boolean).join(":") || JSON.stringify(call); const existing = byKey.get(key); if (!existing || traceToolCallScore(call) > traceToolCallScore(existing)) byKey.set(key, call); } return [...byKey.values()]; } function traceToolCallScore(call: any) { return [call.command, call.exitCode !== undefined, call.outputPreview, call.runId, call.commandId].filter(Boolean).length; } function traceResultUpstreamGaps({ ids, prompt, finalResponse, toolCalls, agentMessages, diagnostics }: any) { const gaps = []; if (!ids?.traceId) gaps.push("traceId_missing"); if (!ids?.conversationId) gaps.push("conversationId_missing"); if (!ids?.sessionId) gaps.push("sessionId_missing"); if (!ids?.runId) gaps.push("runId_missing"); if (!ids?.commandId) gaps.push("commandId_missing"); if (!prompt?.promptId && !prompt?.textPreview) gaps.push("prompt_not_returned_by_upstream"); if (!Array.isArray(toolCalls) || toolCalls.length === 0) gaps.push("tool_calls_missing"); if (!Array.isArray(agentMessages) || agentMessages.length === 0) gaps.push("agent_message_missing"); if (!finalResponse?.text) gaps.push("final_response_missing"); if (!diagnostics?.provider && !diagnostics?.namespace) gaps.push("provider_runner_lane_missing"); return gaps; } function firstObjectForCli(...values: any[]) { return values.find((value) => value && typeof value === "object") ?? null; } function clippedText(value: unknown, max: number) { const result = text(value); if (!result) return undefined; return result.length > max ? `${result.slice(0, max)}...` : result; } function agentHelp() { return ok("client.agent.help", { serviceRuntime: false, imagePublished: false, commands: [ "session create [--provider-profile PROFILE] [--session-id ID]", "session select SESSION_ID", "session status SESSION_ID", "session list [--limit N]", "send --message TEXT|--message-file PATH [--from-trace TRACE] [--session-id ID] [--thread-id ID] [--retry-of TRACE] [--provider-profile PROFILE] [default: short return]", "composer status [--profile NAME] [--full]", "composer submit --message TEXT|--message-file PATH [--profile NAME] [default: auto turn/steer]", "send --message TEXT --wait [--timeout-ms N] [--poll-interval-ms N]", "result TRACE_ID [--full]", "trace TRACE_ID [--render web] [--after-seq N|--since-seq N] [--limit N|--tail N] [--full]", "inspect --trace-id TRACE_ID|--session-id ID|--thread-id ID", "steer TRACE_ID --message TEXT|--message-file PATH [--session-id ID] [--thread-id ID]", "cancel TRACE_ID" ] }); } function agentTraceQueryString(parsed: any) { const query = new URLSearchParams(); const sinceSeq = nonNegativeOption(parsed.sinceSeq ?? parsed.afterSeq ?? parsed.after); if (sinceSeq !== undefined) query.set("sinceSeq", String(sinceSeq)); const limit = positiveOption(parsed.limit ?? parsed.tail); if (limit !== undefined) query.set("limit", String(limit)); const text = query.toString(); return text ? `?${text}` : ""; } function nonNegativeOption(value: unknown) { const parsed = numberOption(value); if (parsed === undefined || parsed < 0) return undefined; return Math.trunc(parsed); } function positiveOption(value: unknown) { const parsed = numberOption(value); if (parsed === undefined || parsed <= 0) return undefined; return Math.trunc(parsed); } async function sessionCommand(context: any) { const subcommand = context.rest[0] || "list"; if (wantsHelp(context)) return sessionHelp(); if (subcommand === "list" || subcommand === "ls") return sessionListCommand(context); if (subcommand === "current" || subcommand === "status") return sessionCurrentCommand(context); if (subcommand === "create" || subcommand === "new") return sessionCreateCommand(context); if (subcommand === "switch" || subcommand === "select") return sessionSwitchCommand(context); if (subcommand === "delete" || subcommand === "rm") return sessionDeleteCommand(context); if (subcommand === "inspect" || subcommand === "describe" || subcommand === "show") return sessionInspectCommand(context); if (subcommand === "final-response" || subcommand === "final") return sessionFinalResponseCommand(context); throw cliError("unsupported_session_command", `unsupported session command: ${subcommand}`, { subcommand }); } function sessionHelp() { return ok("client.session.help", { serviceRuntime: false, imagePublished: false, webEquivalent: true, commands: [ "list [--limit N]", "current", "create [--session-id ID] [--thread-id ID] [--provider-profile PROFILE]", "switch SESSION_ID|--session-id ID", "inspect SESSION_ID|--session-id ID [--trace-id TRACE]", "final-response SESSION_ID|--session-id ID [--trace-id TRACE]" ] }); } async function sessionInspectCommand(context: any) { const sessionId = requiredSessionId(context.rest[1] ?? context.parsed.sessionId); const pathName = `/v1/workbench/sessions/${encodeURIComponent(sessionId)}`; const headers = text(context.parsed.traceId) ? { "x-trace-id": text(context.parsed.traceId) } : undefined; const response = await requestJson({ ...context, method: "GET", path: pathName, extraHeaders: headers }); const session = response.body?.session ?? null; return responsePayload("client.session.inspect", response, context, { route: route("GET", pathName), lookup: clean({ sessionId, traceId: text(context.parsed.traceId) }), summary: sessionSummary(session), session, body: responseBodyForCli(response.body, context.parsed) }); } async function sessionListCommand(context: any) { const limit = numberOption(context.parsed.limit) ?? 100; const includeSessionId = safeOptionalSessionId(context.parsed.sessionId ?? context.parsed.includeSessionId); const params = new URLSearchParams(); params.set("limit", String(limit)); if (includeSessionId) params.set("includeSessionId", includeSessionId); const pathName = `/v1/workbench/sessions?${params.toString()}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.session.list", response, context, { route: route("GET", pathName), sessions: sessionSummaries(response.body?.sessions), body: responseBodyForCli(response.body, context.parsed) }); } async function sessionFinalResponseCommand(context: any) { const sessionId = requiredSessionId(context.rest[1] ?? context.parsed.sessionId); const inspectPath = `/v1/workbench/sessions/${encodeURIComponent(sessionId)}`; const inspectResponse = await requestJson({ ...context, method: "GET", path: inspectPath }); const session = inspectResponse.body?.session ?? null; if (!responseSucceeded(inspectResponse) || !session) { const validation = finalResponseEarlyValidation({ sessionId, routeName: "inspect", response: inspectResponse, failure: session ? "inspect-request-failed" : "inspect-session-missing" }); return { ...responsePayload("client.session.final-response", inspectResponse, context, { route: route("GET", inspectPath), sessionId, routes: { inspect: route("GET", inspectPath) }, httpStatuses: { inspect: inspectResponse.status }, validation, inspect: finalResponseSessionSummary(session), body: context.parsed.full === true ? { inspect: inspectResponse.body } : compactApiBody(inspectResponse.body) }), ok: false, status: "failed" }; } const limit = numberOption(context.parsed.limit) ?? 100; const messagesPath = `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages?limit=${encodeURIComponent(String(limit))}`; const messagesResponse = await requestJson({ ...context, method: "GET", path: messagesPath }); if (!responseSucceeded(messagesResponse)) { const validation = finalResponseEarlyValidation({ sessionId, routeName: "messages", response: messagesResponse, failure: "messages-request-failed" }); return { ...responsePayload("client.session.final-response", messagesResponse, context, { route: route("GET", messagesPath), sessionId, routes: { inspect: route("GET", inspectPath), messages: route("GET", messagesPath) }, httpStatuses: { inspect: inspectResponse.status, messages: messagesResponse.status }, validation, inspect: finalResponseSessionSummary(session), body: context.parsed.full === true ? { inspect: inspectResponse.body, messages: messagesResponse.body } : compactApiBody(messagesResponse.body) }), ok: false, status: "failed" }; } const messages = Array.isArray(messagesResponse.body?.messages) ? messagesResponse.body.messages : []; const resolvedTraceId = text(context.parsed.traceId ?? session?.lastTraceId ?? latestAgentMessage({ messages })?.traceId); if (!resolvedTraceId) { const validation = finalResponseEarlyValidation({ sessionId, routeName: "session", response: messagesResponse, failure: "trace-id-missing" }); return { ...responsePayload("client.session.final-response", messagesResponse, context, { route: route("GET", messagesPath), sessionId, routes: { inspect: route("GET", inspectPath), messages: route("GET", messagesPath) }, httpStatuses: { inspect: inspectResponse.status, messages: messagesResponse.status }, validation, inspect: finalResponseSessionSummary(session), messages: finalResponseMessagesSummary(messages), body: context.parsed.full === true ? { inspect: inspectResponse.body, messages: messagesResponse.body } : undefined }), ok: false, status: "failed" }; } const traceId = requiredTraceId(resolvedTraceId); const resultPath = `/v1/workbench/turns/${encodeURIComponent(traceId)}`; const resultResponse = await requestJson({ ...context, method: "GET", path: resultPath }); const validation = finalResponseValidation({ sessionId, inspectSession: session, messages, traceId, resultBody: resultResponse.body }); const success = responseSucceeded(inspectResponse) && responseSucceeded(messagesResponse) && responseSucceeded(resultResponse) && validation.ok; return { ...responsePayload("client.session.final-response", resultResponse, context, { route: route("GET", resultPath), sessionId, traceId, routes: { inspect: route("GET", inspectPath), messages: route("GET", messagesPath), result: route("GET", resultPath) }, httpStatuses: { inspect: inspectResponse.status, messages: messagesResponse.status, result: resultResponse.status }, validation, inspect: finalResponseSessionSummary(session), messages: finalResponseMessagesSummary(messages), result: finalResponseResultSummary(resultResponse.body), body: context.parsed.full === true ? { inspect: inspectResponse.body, messages: messagesResponse.body, result: resultResponse.body } : finalResponseResultSummary(resultResponse.body) }), ok: success, status: success ? "succeeded" : "failed" }; } async function sessionCurrentCommand(context: any) { const stored = await loadCliWorkbenchState(context); const includeSessionId = safeOptionalSessionId(context.parsed.sessionId ?? stored?.activeSessionId); const params = new URLSearchParams(); params.set("limit", "1"); if (includeSessionId) params.set("includeSessionId", includeSessionId); const pathName = `/v1/workbench/sessions?${params.toString()}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); const sessions = Array.isArray(response.body?.sessions) ? response.body.sessions : []; const selected = includeSessionId ? sessions.find((item: any) => text(item?.sessionId) === includeSessionId) ?? sessions[0] : sessions[0]; return responsePayload("client.session.current", response, context, { route: route("GET", pathName), stored, current: sessionSummary(selected), body: responseBodyForCli(response.body, context.parsed) }); } async function sessionCreateCommand(context: any) { const sessionId = safeOptionalSessionId(context.parsed.sessionId); const pathName = "/v1/agent/sessions"; const response = await requestJson({ ...context, method: "POST", path: pathName, body: clean({ sessionId, threadId: text(context.parsed.threadId), providerProfile: text(context.parsed.providerProfile) || "deepseek", updatedByClient: "hwlab-cli" }) }); if (responseSucceeded(response)) await saveManualAgentSessionState(context, response.body, { providerProfile: text(context.parsed.providerProfile) || "deepseek" }); return responsePayload("client.session.create", response, context, { route: route("POST", pathName), current: agentSessionSummary(response.body?.session, { sessionId, providerProfile: text(context.parsed.providerProfile) || "deepseek" }), body: responseBodyForCli(response.body, context.parsed) }); } async function sessionSwitchCommand(context: any) { const sessionId = requiredSessionId(context.rest[1] ?? context.parsed.sessionId); const pathName = `/v1/workbench/sessions/${encodeURIComponent(sessionId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); if (responseSucceeded(response)) await saveCliWorkbenchState(context, { activeSessionId: sessionId, activeTraceId: text(response.body?.session?.lastTraceId), threadId: text(response.body?.session?.threadId), providerProfile: text(response.body?.session?.providerProfile) }); return responsePayload("client.session.switch", response, context, { route: route("GET", pathName), current: sessionSummary(response.body?.session), session: response.body?.session ?? null, body: responseBodyForCli(response.body, context.parsed) }); } async function sessionDeleteCommand(context: any) { const sessionId = requiredSessionId(context.rest[1] ?? context.parsed.sessionId); throw cliError("session_delete_removed", "client session delete was tied to a removed legacy Workbench API; use a session lifecycle API when one is introduced.", { sessionId }); } async function agentSessionCommand(context: any) { const subcommand = context.rest[1] || "status"; if (["help", "--help", "-h"].includes(subcommand)) return agentSessionHelp(); if (subcommand === "create" || subcommand === "new") return agentSessionCreate(context); if (subcommand === "select") return agentSessionSelect(context); if (subcommand === "status" || subcommand === "show") return agentSessionStatus(context); if (subcommand === "list" || subcommand === "ls") return agentSessionList(context); throw cliError("unsupported_agent_session_command", `unsupported agent session command: ${subcommand}`, { subcommand }); } function agentSessionHelp() { return ok("client.agent.session.help", { serviceRuntime: false, imagePublished: false, commands: [ "create [--provider-profile PROFILE] [--session-id ID]", "select SESSION_ID", "status SESSION_ID", "list [--limit N]" ] }); } async function agentSessionCreate(context: any) { const body = clean({ sessionId: text(context.parsed.sessionId), providerProfile: text(context.parsed.providerProfile) || "deepseek", updatedByClient: "hwlab-cli" }); const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/sessions", body, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); if (responseSucceeded(response)) await saveManualAgentSessionState(context, response.body, body); const session = agentSessionSummary(response.body?.session, body); return responsePayload("client.agent.session.create", response, context, { route: route("POST", "/v1/agent/sessions"), ...session, session: response.body?.session ?? null, workbench: await loadCliWorkbenchState(context), body: responseBodyForCli(response.body, context.parsed) }); } async function agentSessionSelect(context: any) { const sessionId = requiredText(context.rest[2] ?? context.parsed.sessionId, "sessionId"); const pathName = `/v1/agent/sessions/${encodeURIComponent(sessionId)}/select`; const body = clean({ providerProfile: text(context.parsed.providerProfile), updatedByClient: "hwlab-cli" }); const response = await requestJson({ ...context, method: "POST", path: pathName, body, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS }); if (responseSucceeded(response)) await saveManualAgentSessionState(context, response.body, { ...body, sessionId }); const session = agentSessionSummary(response.body?.session, { sessionId, providerProfile: body.providerProfile }); return responsePayload("client.agent.session.select", response, context, { route: route("POST", pathName), ...session, session: response.body?.session ?? null, workbench: await loadCliWorkbenchState(context), body: responseBodyForCli(response.body, context.parsed) }); } async function agentSessionStatus(context: any) { const sessionId = requiredText(context.rest[2] ?? context.parsed.sessionId, "sessionId"); const pathName = `/v1/agent/sessions/${encodeURIComponent(sessionId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.agent.session.status", response, context, { route: route("GET", pathName), ...agentSessionSummary(response.body?.session, { sessionId }), body: responseBodyForCli(response.body, context.parsed) }); } async function fetchAgentSessionForSend(context: any, sessionId: string) { const pathName = `/v1/agent/sessions/${encodeURIComponent(sessionId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); if (!isHttpSuccess(response)) return { response, session: null }; const session = response.body?.session && typeof response.body.session === "object" ? response.body.session : null; return { response, session }; } async function agentSessionList(context: any) { const params = new URLSearchParams(); const limit = numberOption(context.parsed.limit); if (limit) params.set("limit", String(limit)); const query = params.toString(); const pathName = `/v1/agent/sessions${query ? `?${query}` : ""}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.agent.session.list", response, context, { route: route("GET", pathName), body: responseBodyForCli(response.body, context.parsed) }); } async function agentComposer(context: any) { const subcommand = context.rest[1] || "status"; if (subcommand === "status") return agentComposerStatus(context); if (subcommand === "submit" || subcommand === "send") return agentComposerSubmit(context); throw cliError("unsupported_agent_composer_command", `unsupported agent composer command: ${subcommand}`, { subcommand }); } async function agentComposerStatus(context: any) { const localState = await loadStoredState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }); const composer = computeCliComposerState(context, localState?.workbench ?? null); return ok("client.agent.composer.status", { baseUrl: baseUrl(context.parsed, context.env), runtimeEndpoint: runtimeEndpointVisibility(runtimeEndpoint(context.parsed, context.env, "web")), status: "succeeded", route: route("POST", composer.route), composer, workbench: localState?.workbench ?? null, nextCommands: composer.submitMode === "steer" ? [`hwlab-cli client agent composer submit --message TEXT`, `hwlab-cli client agent trace ${composer.targetTraceId} --render web`] : ["hwlab-cli client agent composer submit --message TEXT"] }); } async function agentComposerSubmit(context: any) { const message = await messageValue(context); if (!message) throw cliError("message_required", "client agent composer submit requires --message or stdin text"); const localState = await loadStoredState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }); const composer = computeCliComposerState(context, localState?.workbench ?? null); if (composer.submitMode === "steer") { const traceId = requiredTraceId(composer.targetTraceId ?? context.parsed.traceId ?? context.parsed.targetTraceId); const steerTraceId = text(context.parsed.steerTraceId) || makeId("trc_steer"); const body = clean({ traceId, targetTraceId: traceId, steerTraceId, message, sessionId: text(context.parsed.sessionId) || composer.sessionId, threadId: text(context.parsed.threadId) || composer.threadId }); const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat/steer", body, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS, extraHeaders: { "x-trace-id": traceId, "prefer": "respond-async", "x-hwlab-short-connection": "1" } }); return responsePayload("client.agent.composer.submit", response, context, { route: route("POST", "/v1/agent/chat/steer"), traceId, steerTraceId, composer, workbench: await loadCliWorkbenchState(context), body: responseBodyForCli(response.body, context.parsed), waitPolicy: { defaultWait: false, webEquivalent: true, reason: "Composer detected an active Web turn and submitted steer through the same Cloud Web path.", nextCommands: [ `hwlab-cli client agent result ${traceId}`, `hwlab-cli client agent trace ${traceId} --render web` ] } }); } const sendContext = { ...context, parsed: { ...context.parsed, message, sessionId: text(context.parsed.sessionId) || composer.sessionId, threadId: text(context.parsed.threadId) || composer.threadId, } }; const result = await agentSend(sendContext); return { ...result, action: "client.agent.composer.submit", composer }; } function computeCliComposerState(context: any, workspaceState: any) { const state = workspaceState && typeof workspaceState === "object" ? workspaceState : {}; return computeCodeAgentComposerState({ workspace: state ? { activeTraceId: state.activeTraceId } : null, sessionId: text(context.parsed.sessionId) || text(state.activeSessionId), threadId: text(context.parsed.threadId) || text(state.threadId), sessionStatus: text(context.parsed.sessionStatus) || text(state.sessionStatus), targetTraceId: text(context.parsed.traceId ?? context.parsed.targetTraceId) || text(state?.activeTraceId), messages: Array.isArray(state?.messages) ? state.messages : undefined }); } async function agentSteer(context: any) { const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId ?? context.parsed.targetTraceId); const message = await messageValue(context); if (!message) throw cliError("message_required", "client agent steer requires --message or stdin text"); const steerTraceId = text(context.parsed.steerTraceId) || makeId("trc_steer"); const body = clean({ traceId, targetTraceId: traceId, steerTraceId, message, sessionId: text(context.parsed.sessionId), threadId: text(context.parsed.threadId) }); const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat/steer", body, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS, extraHeaders: { "x-trace-id": traceId, "prefer": "respond-async", "x-hwlab-short-connection": "1" } }); return responsePayload("client.agent.steer", response, context, { route: route("POST", "/v1/agent/chat/steer"), traceId, steerTraceId, body: responseBodyForCli(response.body, context.parsed), waitPolicy: { defaultWait: false, webEquivalent: true, reason: "Steer is applied to the active Code Agent turn; observe the original target trace/result to confirm whether the runner accepted and applied it.", nextCommands: [ `hwlab-cli client agent result ${traceId}`, `hwlab-cli client agent trace ${traceId} --render web` ] } }); } function agentInspectPath(context: any) { const params = new URLSearchParams(); const traceId = text(context.rest[1] ?? context.parsed.traceId); const sessionId = text(context.parsed.sessionId); const threadId = text(context.parsed.threadId); if (traceId) params.set("traceId", requiredTraceId(traceId)); if (sessionId) params.set("sessionId", sessionId); if (threadId) params.set("threadId", threadId); if ([...params.keys()].length === 0) { throw cliError("missing_inspect_query", "client agent inspect requires --trace-id, --session-id, or --thread-id"); } return `/v1/agent/chat/inspect?${params.toString()}`; } async function resolveAgentReplayContext(context: any) { const fromTrace = text(context.parsed.fromTrace); if (!fromTrace) return { fromTrace: null, conversationId: "", sessionId: "", threadId: "", retryOf: "", summary: null }; const traceId = requiredTraceId(fromTrace); const pathName = `/v1/agent/chat/inspect?traceId=${encodeURIComponent(traceId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); if (!isHttpSuccess(response) || response.body?.ok === false) { throw cliError("from_trace_inspect_failed", "client agent send --from-trace could not inspect the source trace", { fromTrace: traceId, route: route("GET", pathName), httpStatus: response.status, status: response.body?.status ?? null, error: response.body?.error ?? null }); } const body = response.body ?? {}; const conversationId = safeReplayConversationId(body.query?.conversationId) || safeReplayConversationId(body.conversationFacts?.conversationId) || safeReplayConversationId(body.session?.conversationId) || ""; const sessionId = safeReplaySessionId(body.query?.sessionId) || safeReplaySessionId(body.conversationFacts?.sessionId) || safeReplaySessionId(body.session?.sessionId) || ""; const threadId = text(body.query?.threadId) || text(body.session?.threadId) || text(body.conversationFacts?.threadId); const sessionStatus = text(body.session?.status ?? body.conversationFacts?.sessionStatus ?? body.runnerTrace?.sessionStatus ?? body.status); if (isCodeAgentSessionUnusableStatus(sessionStatus)) { throw cliError("session_not_usable", "client agent send --from-trace refuses to reuse a blocked/stale Code Agent session; create/select a new session first", { fromTrace: traceId, conversationId: conversationId || null, sessionId: sessionId || null, threadId: threadId || null, sessionStatus, nextCommands: ["hwlab-cli client agent session create --provider-profile PROFILE"] }); } return { fromTrace: traceId, conversationId, sessionId, threadId, retryOf: traceId, summary: pruneUndefined({ source: "cloud-web:/v1/agent/chat/inspect", fromTrace: traceId, inspectHttpStatus: response.status, status: body.status, latestTraceId: body.latestTraceId, conversationId, sessionId, sessionStatus: sessionStatus || null, threadId, valuesPrinted: false }) }; } function safeReplayConversationId(value: unknown) { const result = text(value); return /^cnv_[A-Za-z0-9_.:-]+$/u.test(result) ? result : ""; } function safeReplaySessionId(value: unknown) { const result = text(value); return /^ses_[A-Za-z0-9_.:-]+$/u.test(result) ? result : ""; } async function harnessCommand(context: any) { const subcommand = context.rest[0] || "help"; if (["help", "--help", "-h"].includes(subcommand)) return harnessHelp(); if (subcommand === "health") { const response = await requestJson({ ...context, method: "GET", path: "/health/live", auth: false }); return responsePayload("client.harness.health", response, context, { route: route("GET", "/health/live"), body: responseBodyForCli(response.body, context.parsed) }); } if (subcommand === "submit") return harnessSubmit({ ...context, rest: context.rest.slice(1) }); if (subcommand === "result") return harnessResult({ ...context, rest: context.rest.slice(1) }); if (subcommand === "trace") return harnessTrace({ ...context, rest: context.rest.slice(1) }); if (subcommand === "wait") return harnessWait({ ...context, rest: context.rest.slice(1) }); if (subcommand === "audit") return harnessAudit({ ...context, rest: context.rest.slice(1) }); throw cliError("unsupported_harness_command", `unsupported harness command: ${subcommand}`, { subcommand }); } function harnessHelp() { return ok("client.harness.help", { serviceRuntime: false, imagePublished: false, aliases: ["harness", "harness-ops", "harness-opt"], commands: [ "health [--base-url URL]", "submit --message TEXT|--message-file PATH [--session-id ID] [--trace-id ID] [--provider-profile PROFILE]", "result TRACE_ID [--full]", "trace TRACE_ID [--limit N] [--full]", "wait TRACE_ID [--timeout-ms N] [--poll-interval-ms N]", "audit TRACE_ID|--trace-file PATH [--strict]" ] }); } async function harnessSubmit(context: any) { const message = await messageValue(context); if (!message.trim()) { throw cliError( "message_required", "client harness submit requires --message, --message-file, or stdin text" ); } const traceId = text(context.parsed.traceId) || makeId("trc"); const conversationId = text(context.parsed.conversationId) || makeId("cnv"); const body = clean({ message, conversationId, sessionId: text(context.parsed.sessionId), threadId: text(context.parsed.threadId), traceId, providerProfile: text(context.parsed.providerProfile) || "deepseek", timeoutMs: numberOption(context.parsed.agentTimeoutMs ?? context.parsed.timeoutMs), hardTimeoutMs: numberOption(context.parsed.hardTimeoutMs), gatewayShellTimeoutMs: numberOption(context.parsed.gatewayShellTimeoutMs), shortConnection: true, projectId: text(context.parsed.projectId) || "prj_harness_ops" }); const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat", body, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS, extraHeaders: { "x-trace-id": traceId, prefer: "respond-async", "x-hwlab-short-connection": "1" } }); return responsePayload("client.harness.submit", response, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, accepted: response.body?.accepted ?? false, resultUrl: response.body?.resultUrl ?? `/v1/agent/chat/result/${encodeURIComponent(traceId)}`, traceUrl: response.body?.traceUrl ?? `/v1/agent/traces/${encodeURIComponent(traceId)}`, body: responseBodyForCli(response.body, context.parsed) }); } async function harnessResult(context: any) { const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId); const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.harness.result", response, context, { route: route("GET", pathName), traceId, body: responseBodyForCli(response.body, context.parsed) }); } async function harnessTrace(context: any) { const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId); const pathName = `/v1/agent/traces/${encodeURIComponent(traceId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); const body = context.parsed.full === true ? response.body : compactTraceBody(response.body, numberOption(context.parsed.limit) ?? 24); return responsePayload("client.harness.trace", response, context, { route: route("GET", pathName), traceId, body }); } async function harnessWait(context: any) { const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId); const requestedTimeoutMs = numberOption(context.parsed.timeoutMs) ?? HARNESS_WAIT_DEFAULT_TIMEOUT_MS; const timeoutMs = Math.min(requestedTimeoutMs, HARNESS_WAIT_MAX_TIMEOUT_MS); const pollIntervalMs = Math.max(numberOption(context.parsed.pollIntervalMs) ?? 1000, 250); const waitPolicy = harnessWaitPolicy(traceId, requestedTimeoutMs, timeoutMs); const deadline = Date.now() + timeoutMs; const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`; let polls = 0; let lastResponse: any = null; while (Date.now() < deadline) { polls += 1; lastResponse = await requestJson({ ...context, method: "GET", path: pathName, timeoutMs: Math.min(DEFAULT_TIMEOUT_MS, pollIntervalMs + 2000) }); if (lastResponse.status === 200 && lastResponse.body?.status && lastResponse.body.status !== "running") { return responsePayload("client.harness.wait", lastResponse, context, { route: route("GET", pathName), traceId, polls, timedOut: false, timeoutMsRequested: requestedTimeoutMs, timeoutMsEffective: timeoutMs, waitPolicy, body: responseBodyForCli(lastResponse.body, context.parsed) }); } await context.sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))); } return ok("client.harness.wait", { status: "timeout", traceId, route: route("GET", pathName), polls, timedOut: true, timeoutMsRequested: requestedTimeoutMs, timeoutMsEffective: timeoutMs, waitPolicy, body: lastResponse ? responseBodyForCli(lastResponse.body, context.parsed) : null }, "timeout"); } function harnessWaitPolicy(traceId: string, requestedTimeoutMs: number, timeoutMs: number) { return { remotePassthroughSafe: true, capped: requestedTimeoutMs > timeoutMs, reason: "UniDesk ssh/tran has a 60s top-level runtime timeout; keep harness wait below that and continue with short polling.", nextCommands: [ `hwlab-cli client harness result ${traceId}`, `hwlab-cli client harness trace ${traceId} --limit 24`, `hwlab-cli client harness audit ${traceId}` ] }; } async function harnessAudit(context: any) { let traceObject: any; if (typeof context.parsed.traceFile === "string") { traceObject = JSON.parse(await readFile(path.resolve(context.cwd, context.parsed.traceFile), "utf8")); } else { const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId); const response = await requestJson({ ...context, method: "GET", path: `/v1/agent/traces/${encodeURIComponent(traceId)}` }); traceObject = response.body; } const report = auditTrace(traceObject); const status = context.parsed.strict === true && report.frictionSignals.length > 0 ? "failed" : "succeeded"; return ok("client.harness.audit", { traceId: traceObject?.traceId ?? context.rest[0] ?? null, ...report }, status); } async function agentSend(context: any) { const { parsed } = context; const message = await messageValue(context); if (!message) throw cliError("message_required", "client agent send requires --message or stdin text"); const replay = await resolveAgentReplayContext(context); const traceId = text(parsed.traceId) || makeId("trc"); const localWorkbench = await loadCliWorkbenchState(context); const explicitSessionId = safeOptionalSessionId(parsed.sessionId); const sessionId = explicitSessionId || replay.sessionId || safeOptionalSessionId(localWorkbench?.activeSessionId); let explicitSessionRecord = null; if (sessionId && !text(parsed.providerProfile)) { const lookup = await fetchAgentSessionForSend(context, sessionId); if (!isHttpSuccess(lookup.response)) { return responsePayload("client.agent.send", lookup.response, context, { route: route("GET", `/v1/agent/sessions/${encodeURIComponent(sessionId)}`), traceId, sessionId, replay: replay.summary }); } explicitSessionRecord = lookup.session; } if (!sessionId) { throw cliError("session_required", "client agent send requires an explicit Code Agent session; run client agent session create first", { traceId, nextCommands: [ "hwlab-cli client agent session create --provider-profile PROFILE", "hwlab-cli client agent send --session-id --message TEXT --provider-profile PROFILE" ] }); } const sessionStatus = text(explicitSessionRecord?.status ?? localWorkbench?.sessionStatus); if (isCodeAgentSessionUnusableStatus(sessionStatus) && !text(parsed.ignoreSessionStatus)) { throw cliError("session_not_usable", `client agent send refuses to reuse ${sessionStatus} Code Agent session; create/select a new session first`, { traceId, sessionId, sessionStatus, nextCommands: ["hwlab-cli client agent session create --provider-profile PROFILE"] }); } const threadId = text(parsed.threadId) || replay.threadId || (explicitSessionRecord ? text(explicitSessionRecord.threadId) : "") || text(localWorkbench?.threadId); const retryOf = text(parsed.retryOf) || replay.retryOf; const continuation = agentContinuationSummary({ sessionId, threadId, retryOf, fromTrace: replay.fromTrace }); const requestProviderProfile = text(parsed.providerProfile) || text(explicitSessionRecord?.providerProfile) || text(localWorkbench?.providerProfile) || "deepseek"; const requestBody = clean({ message, sessionId, threadId, retryOf, traceId, providerProfile: requestProviderProfile, gatewayShellTimeoutMs: numberOption(parsed.gatewayShellTimeoutMs), shortConnection: true, updatedByClient: "hwlab-cli" }); const accepted = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat", body: requestBody, timeoutMs: numberOption(parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS, extraHeaders: { "x-trace-id": traceId, "prefer": "respond-async", "x-hwlab-short-connection": "1" } }); if (!isHttpSuccess(accepted)) { return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, sessionId, continuation, replay: replay.summary }); } await saveAcceptedAgentState(context, accepted.body, { traceId, sessionId, threadId, providerProfile: requestBody.providerProfile, message }); if (parsed.wait !== true) { return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, sessionId, continuation, replay: replay.summary, workbench: await loadCliWorkbenchState(context), waited: false, waitPolicy: agentSendWaitPolicy(traceId) }); } const result = await pollAgentResult(context, traceId, accepted.body); const completedState = await saveCompletedAgentState(context, result.response?.body, { traceId, sessionId, threadId, providerProfile: requestBody.providerProfile, message }); return ok("client.agent.send", { status: result.final ? "succeeded" : "timeout", route: route("POST", "/v1/agent/chat"), traceId, sessionId, continuation, replay: replay.summary, accepted: compactBody(accepted.body), result: result.response ? compactResponse(result.response) : null, workbench: completedState ?? await loadCliWorkbenchState(context), polls: result.polls, timeoutMs: result.timeoutMs, resultUrl: result.resultPath }, result.final ? "succeeded" : "timeout"); } async function saveManualAgentSessionState(context: any, body: any, fallback: any = {}) { const sessionRecord = body?.session && typeof body.session === "object" ? body.session : null; if (!sessionRecord) return null; return await saveCliWorkbenchState(context, { activeSessionId: text(sessionRecord.sessionId) || text(fallback.sessionId), threadId: text(sessionRecord.threadId) || text(fallback.threadId), activeTraceId: null, providerProfile: text(fallback.providerProfile) || text(sessionRecord.providerProfile), sessionStatus: text(sessionRecord.status) }); } async function saveAcceptedAgentState(context: any, acceptedBody: any, fallback: any) { return await saveCliWorkbenchState(context, { activeSessionId: text(acceptedBody?.sessionId) || text(fallback.sessionId), threadId: text(acceptedBody?.threadId ?? acceptedBody?.session?.threadId ?? acceptedBody?.providerTrace?.threadId) || text(fallback.threadId), activeTraceId: text(acceptedBody?.traceId) || text(fallback.traceId), providerProfile: text(fallback.providerProfile), sessionStatus: "running" }); } async function saveCompletedAgentState(context: any, resultBody: any, fallback: any) { const status = text(resultBody?.status); return await saveCliWorkbenchState(context, { activeSessionId: text(resultBody?.sessionId) || text(fallback.sessionId), threadId: text(resultBody?.threadId ?? resultBody?.session?.threadId ?? resultBody?.providerTrace?.threadId) || text(fallback.threadId), activeTraceId: status && status !== "running" ? null : text(resultBody?.traceId) || text(fallback.traceId), providerProfile: text(fallback.providerProfile), sessionStatus: status || undefined, messages: resultBody ? compactAgentMessagesForWorkbench(resultBody, fallback) : undefined }); } async function pollAgentResult(context: any, traceId: string, acceptedBody: any) { const timeoutMs = numberOption(context.parsed.timeoutMs) ?? DEFAULT_AGENT_TIMEOUT_MS; const pollIntervalMs = numberOption(context.parsed.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS; const startedAt = Date.now(); const resultPath = text(acceptedBody?.turnUrl) || `/v1/agent/turns/${encodeURIComponent(traceId)}`; let polls = 0; let lastResponse = null; while (Date.now() - startedAt < timeoutMs) { polls += 1; const response = await requestJson({ ...context, method: "GET", path: resultPath, timeoutMs: Math.min(DEFAULT_TIMEOUT_MS, pollIntervalMs + 2000) }); lastResponse = response; if (response.status === 200 && response.body?.terminal === true) { return { final: true, response, polls, timeoutMs, resultPath }; } await context.sleep(pollIntervalMs); } return { final: false, response: lastResponse, polls, timeoutMs, resultPath }; } function agentSendWaitPolicy(traceId: string) { return { defaultWait: false, webEquivalent: true, reason: "Cloud Web submits /v1/agent/chat as a short request and observes progress by polling /v1/agent/turns/:traceId; trace is only used for event details.", waitCommand: `hwlab-cli client agent send --from-trace ${traceId} --message TEXT --wait`, nextCommands: [ `hwlab-cli client agent result ${traceId}`, `hwlab-cli client agent trace ${traceId} --render web`, `hwlab-cli client workbench status` ] }; } async function agentObserverCommand(context: any) { const subcommand = context.rest[0] || "list"; if (wantsHelp(context)) return agentObserverHelp(); if (subcommand !== "list") throw cliError("unsupported_agent_observer_command", `unsupported agent-observer command: ${subcommand}`, { subcommand }); const endpoint = runtimeEndpoint(context.parsed, context.env, "web"); const apiKey = explicitApiKey(context.parsed, context.env); if (!apiKey) throw cliError("hwlab_api_key_required", "client agent-observer list requires HWLAB_API_KEY.", { credentialSource: "HWLAB_API_KEY" }); const timeoutMs = numberOption(context.parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS; const page = numberOption(context.parsed.page) ?? 1; const pageSize = numberOption(context.parsed.pageSize) ?? numberOption(context.parsed.limit) ?? 25; if (!Number.isInteger(page) || page <= 0) throw cliError("invalid_page", "--page must be a positive integer.", { page }); if (!Number.isInteger(pageSize) || pageSize <= 0 || pageSize > 100) throw cliError("invalid_page_size", "--page-size must be between 1 and 100.", { pageSize }); const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); for (const [option, parameter] of [["status", "status"], ["projectId", "projectId"], ["workspace", "workspace"], ["backendProfile", "backendProfile"], ["providerId", "providerId"], ["search", "search"], ["updatedAfter", "updatedAfter"], ["sort", "sort"]] as const) { const value = text(context.parsed[option]); if (value) query.set(parameter, value); } const observation = await collectAgentObserverRuns({ fetchImpl: context.fetchImpl, url: `${endpoint.baseUrl}/v1/agent-observer/runs?${query.toString()}`, apiKey, timeoutMs, }); return ok("client.agent-observer.list", { baseUrl: endpoint.baseUrl, runtimeEndpoint: runtimeEndpointVisibility(endpoint), route: route("GET", "/v1/agent-observer/runs"), authority: "agentrun-manager-durable-store", ...observation, valuesPrinted: false }); } function agentObserverHelp() { return ok("client.agent-observer.help", { serviceRuntime: false, imagePublished: false, commands: ["list [--page 1] [--page-size 25] [--status ] [--project-id ] [--workspace ] [--backend-profile ] [--provider-id ] [--search ] [--updated-after ] [--sort updated-desc|updated-asc|status-asc|id-asc] [--timeout-ms 30000]"], auth: "HWLAB_API_KEY", authority: "agentrun-manager-durable-store" }); } async function workbenchCommand(context: any) { const subcommand = context.rest[0] || "summary"; if (wantsHelp(context)) return workbenchHelp(); if (subcommand === "status") return workbenchStatusCommand(context); if (["restore", "watch", "reset"].includes(subcommand)) return removedWorkbenchWorkspaceCommand(subcommand); if (subcommand !== "summary") throw cliError("unsupported_workbench_command", `unsupported workbench command: ${subcommand}`, { subcommand }); const paths = [ "/health/live", "/v1", "/v1/live-builds", "/v1/skills" ]; const probes = await Promise.all(paths.map(async (pathName) => { try { const response = await requestJson({ ...context, method: "GET", path: pathName, auth: !["/health/live", "/v1", "/v1/live-builds", "/v1/skills"].includes(pathName) }); return compactProbe("GET", pathName, response); } catch (error) { return { route: route("GET", pathName), ok: false, error: errorSummary(error) }; } })); const failed = probes.some((probe) => probe.ok === false); const endpoint = runtimeEndpoint(context.parsed, context.env, "web"); return ok("client.workbench.summary", { status: failed ? "degraded" : "succeeded", baseUrl: endpoint.baseUrl, runtimeEndpoint: runtimeEndpointVisibility(endpoint), probes }, failed ? "degraded" : "succeeded"); } function workbenchHelp() { return ok("client.workbench.help", { serviceRuntime: false, imagePublished: false, commands: [ "summary", "status [--profile NAME]" ] }); } async function workbenchStatusCommand(context: any) { const stored = await loadCliWorkbenchState(context); const includeSessionId = safeOptionalSessionId(context.parsed.sessionId ?? stored?.activeSessionId); const params = new URLSearchParams(); params.set("limit", "20"); if (includeSessionId) params.set("includeSessionId", includeSessionId); const pathName = `/v1/workbench/sessions?${params.toString()}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.workbench.status", response, context, { route: route("GET", pathName), stateFile: stateFile(context.parsed, context.cwd ?? process.cwd(), context.env), stored, sessions: sessionSummaries(response.body?.sessions), body: responseBodyForCli(response.body, context.parsed) }); } function removedWorkbenchWorkspaceCommand(subcommand: string) { throw cliError("workbench_legacy_command_removed", `client workbench ${subcommand} was tied to a removed legacy Workbench API; use client workbench status or client session switch SESSION_ID.`, { subcommand, nextCommands: ["hwlab-cli client workbench status", "hwlab-cli client session switch "] }); } async function requestCommand(context: any) { if (wantsHelp(context)) return requestHelp(); const method = requiredText(context.rest[0], "method").toUpperCase(); const pathName = normalizeRequestPath(requiredText(context.rest[1], "path")); const body = await requestBodyValue(context); const traceId = text(context.parsed.traceId); const response = await requestJson({ ...context, method, path: pathName, body, auth: context.parsed.public === true ? false : true, extraHeaders: clean({ ...(traceId ? { "x-trace-id": traceId } : {}), ...(context.parsed.preferAsync === true ? { prefer: "respond-async" } : {}) }) }); return responsePayload("client.request", response, context, { route: route(method, pathName), requestedPath: pathName, body: responseBodyForCli(response.body, context.parsed) }); } function requestHelp() { return ok("client.request.help", { serviceRuntime: false, imagePublished: false, commands: [ "GET /path [--full]", "GET /v1/web-performance/summary [--full]", "POST /path --body-json '{...}' [--full]", "METHOD /path --body-file FILE" ] }); } async function rpcCommand(context: any) { if (wantsHelp(context)) return rpcHelp(); const method = requiredText(context.rest[0] ?? context.parsed.method, "method"); const params = await rpcParamsValue(context); const traceId = text(context.parsed.traceId) || makeId("trc"); const id = text(context.parsed.id) || makeId("req"); const environment = text(context.parsed.environment) || "dev"; const serviceId = text(context.parsed.serviceId) || "hwlab-cloud-web"; const body = { jsonrpc: "2.0", id, method, params, meta: { traceId, serviceId, environment } }; const response = await requestJson({ ...context, method: "POST", path: "/json-rpc", body, extraHeaders: { "x-trace-id": traceId } }); return responsePayload("client.rpc", response, context, { route: route("POST", "/json-rpc"), rpcMethod: method, traceId, requestId: id, body: responseBodyForCli(response.body, context.parsed) }); } function rpcHelp() { return ok("client.rpc.help", { serviceRuntime: false, imagePublished: false, commands: [ "METHOD [--params-json '{...}']", "METHOD --params-file FILE", "METHOD --full" ] }); } function wantsHelp(context: any) { const token = text(context.rest?.[0]); return context.parsed.help === true || token === "help" || token === "--help" || token === "-h"; } async function rpcParamsValue(context: any) { const { parsed } = context; const raw = typeof parsed.paramsJson === "string" ? parsed.paramsJson : typeof parsed.params === "string" ? parsed.params : typeof parsed.paramsFile === "string" ? await readFile(path.resolve(context.cwd, parsed.paramsFile), "utf8") : "{}"; try { const parsedParams = JSON.parse(raw); if (!parsedParams || typeof parsedParams !== "object" || Array.isArray(parsedParams)) throw new Error("params must be a JSON object"); return parsedParams; } catch (error) { throw cliError("invalid_params_json", "rpc params must be a JSON object", { message: error instanceof Error ? error.message : String(error) }); } } async function messageValue(context: any) { if (typeof context.parsed.message === "string") return context.parsed.message; if (typeof context.parsed.text === "string") return context.parsed.text; if (typeof context.parsed.messageFile === "string") return await readFile(path.resolve(context.cwd, context.parsed.messageFile), "utf8"); return text(context.stdinText); } async function requestBodyValue(context: any) { const { parsed } = context; const raw = typeof parsed.bodyJson === "string" ? parsed.bodyJson : typeof parsed.json === "string" ? parsed.json : typeof parsed.bodyFile === "string" ? await readFile(path.resolve(context.cwd, parsed.bodyFile), "utf8") : parsed.bodyStdin === true ? (context.stdinText !== undefined ? context.stdinText : await readStdin()) : null; if (raw === null) return undefined; try { return JSON.parse(raw); } catch (error) { throw cliError("invalid_body_json", "request body must be valid JSON", { message: error instanceof Error ? error.message : String(error) }); } } function normalizeRequestPath(value: string) { const pathName = value.trim(); if (!pathName.startsWith("/")) throw cliError("invalid_request_path", "request path must be a Cloud Web relative path starting with /", { path: value }); if (pathName.startsWith("//") || /^[a-z][a-z0-9+.-]*:\/\//iu.test(pathName)) throw cliError("invalid_request_path", "request path must not be an absolute URL", { path: value }); return pathName; } async function requestJson(context: any) { const { parsed, env, cwd, method, path: pathName, body, auth = true, extraHeaders = {}, timeoutMs } = context; const endpoint = runtimeEndpoint(parsed, env, context.endpointKind ?? "web"); const resolvedBaseUrl = endpoint.baseUrl; const url = `${resolvedBaseUrl}${pathName}`; const requestTimeoutMs = timeoutMs ?? numberOption(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), requestTimeoutMs); try { const effectiveAuth = auth && parsed.noAuth !== true; const apiKey = effectiveAuth ? explicitApiKey(parsed, env) : ""; if (apiKey) { const response = await sendJsonRequest({ context, url, method, body, apiKey, extraHeaders, signal: controller.signal, pathName, timeoutMs: requestTimeoutMs }); const authState = { required: effectiveAuth, baseUrl: resolvedBaseUrl, username: null, authMethod: "api-key", requiredAuthMethod: "api-key", credentialSource: "api-key", stateFile: null, localSession: null, cookieSource: null, apiKeySource: "env-or-explicit", apiKeyPrefix: apiKeyPrefixSummary(apiKey), autoLoginAttempted: false, autoLoginStatus: null, autoLoginHttpStatus: null, retryAfterLogin: false }; return { ...response, runtimeEndpoint: endpoint, auth: authVisibility(authState), authDiagnosis: authDiagnosis(response, authState) }; } const explicitCookie = explicitAuthCookie(parsed, env); const webSessionAllowed = effectiveAuth && (context.allowWebSession === true || parsed.webSession === true || Boolean(explicitCookie)); const sessionState = effectiveAuth && !explicitCookie && parsed.noSession !== true ? await readSessionState({ parsed, env, cwd: cwd ?? process.cwd() }) : null; const session = sessionState?.session ?? null; const webSession = webSessionAllowed ? explicitCookie || session?.cookie || null : null; const requestedUsername = text(parsed.username ?? env.HWLAB_USERNAME); const sessionUsername = text(session?.user?.username ?? sessionState?.user?.username); const authState: any = { required: effectiveAuth, baseUrl: resolvedBaseUrl, username: requestedUsername || sessionUsername || (explicitCookie ? undefined : "admin"), authMethod: webSession ? "web-session" : null, requiredAuthMethod: webSessionAllowed ? "web-session" : "api-key", credentialSource: webSession ? explicitCookie ? "explicit-web-session" : "state-web-session" : null, stateFile: stateFile(parsed, cwd ?? process.cwd(), env), localSession: sessionState ? sessionStateSummary(sessionState) : null, cookieSource: webSession ? explicitCookie ? "explicit" : "state" : null, webSessionAllowed, webSessionIgnoredReason: !webSessionAllowed && sessionState?.usable ? "cli_requires_api_key" : null, apiKeySource: null, apiKeyPrefix: null, autoLoginAttempted: false, autoLoginStatus: null, autoLoginHttpStatus: null, retryAfterLogin: false }; const response = await sendJsonRequest({ context, url, method, body, cookie: webSession, extraHeaders, signal: controller.signal, pathName, timeoutMs: requestTimeoutMs }); return { ...response, runtimeEndpoint: endpoint, auth: authVisibility(authState), authDiagnosis: authDiagnosis(response, authState) }; } finally { clearTimeout(timer); } } async function sendJsonRequest({ context, url, method, body, cookie, apiKey, extraHeaders, signal, pathName, timeoutMs }: any) { const headers = clean({ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}), ...(cookie ? { cookie } : {}), ...extraHeaders }); const startedAt = Date.now(); try { const response = await context.fetchImpl(url, { method, headers, body: body ? JSON.stringify(body) : undefined, signal }); const textBody = await response.text(); return { status: response.status, headers: response.headers, body: parseJson(textBody), url, method, path: pathName, elapsedMs: Date.now() - startedAt, timeoutMs }; } catch (error) { const timedOut = signal?.aborted === true || error?.name === "AbortError"; const code = timedOut ? "request_timeout" : "request_failed"; const message = timedOut ? `${method} ${pathName} timed out after ${timeoutMs}ms` : `${method} ${pathName} failed: ${error?.message ?? String(error)}`; return { status: 0, headers: null, body: { ok: false, status: "failed", error: { code, message } }, url, method, path: pathName, elapsedMs: Date.now() - startedAt, timeoutMs, timedOut, transportError: { code, name: error?.name ?? null, message: error?.message ?? String(error) } }; } } async function autoLoginSession(context: any, signal: AbortSignal, { force = false } = {}) { const password = await optionalPasswordValue(context); if (!password) return { status: "credentials_missing", cookie: null }; const username = text(context.parsed.username ?? context.env.HWLAB_USERNAME) || "admin"; const endpoint = runtimeEndpoint(context.parsed, context.env, context.endpointKind ?? "web"); const response = await sendJsonRequest({ context, url: `${endpoint.baseUrl}/auth/login`, method: "POST", pathName: "/auth/login", body: { username, password }, cookie: null, extraHeaders: {}, signal }); const cookie = cookieFromResponse(response); const success = isHttpSuccess(response) && response.body?.authenticated === true && cookie; if (success) { await saveSession(context, { baseUrl: endpoint.baseUrl, cookie, user: safeUser(response.body?.user ?? response.body?.actor), expiresAt: textOrNull(response.body?.expiresAt), updatedAt: context.now(), refreshedAfter401: force === true || undefined }); } return { status: success ? "succeeded" : `failed_http_${response.status}`, httpStatus: response.status, cookie: success ? cookie : null }; } function parseOptions(argv: string[]): ParsedArgs { const out: ParsedArgs = { _: [] }; for (let index = 0; index < argv.length; index += 1) { const item = argv[index] ?? ""; if (!item.startsWith("--")) { out._.push(item); continue; } const eq = item.indexOf("="); const rawKey = eq >= 0 ? item.slice(2, eq) : item.slice(2); const key = rawKey.replace(/-([a-z])/gu, (_, c) => String(c).toUpperCase()); if (eq >= 0) { out[key] = item.slice(eq + 1); continue; } const next = argv[index + 1]; if (next && !next.startsWith("--")) { out[key] = next; index += 1; } else out[key] = true; } return out; } async function passwordValue({ parsed, env, stdinText }: any) { const password = await optionalPasswordValue({ parsed, env, stdinText }); return requiredText(password, "password"); } async function optionalPasswordValue({ parsed, env, stdinText }: any) { if (typeof parsed.passwordEnv === "string") return text(env[parsed.passwordEnv]); if (parsed.passwordStdin === true) return stdinText !== undefined ? stdinText.trimEnd() : await readStdin(); return text(parsed.password ?? env.HWLAB_PASSWORD); } async function readStdin() { const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); return Buffer.concat(chunks).toString("utf8").trimEnd(); } async function readRawStdin() { const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); return Buffer.concat(chunks).toString("utf8"); } function baseUrl(parsed: ParsedArgs, env: EnvLike, kind: "web" | "api" = "web") { return runtimeEndpoint(parsed, env, kind).baseUrl; } function runtimeEndpoint(parsed: ParsedArgs, env: EnvLike, kind: "web" | "api" = "web") { return resolveRuntimeEndpoint({ kind, parsed, env }); } async function authCookie({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) { const explicit = explicitAuthCookie(parsed, env); if (explicit) return explicit.includes("=") ? explicit : `hwlab_session=${encodeURIComponent(explicit)}`; if (parsed.noSession === true) return null; const session = await loadSession({ parsed, env, cwd }); return session?.cookie ?? null; } function explicitAuthCookie(parsed: ParsedArgs, env: EnvLike) { const explicit = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE); return explicit ? explicit.includes("=") ? explicit : `hwlab_session=${encodeURIComponent(explicit)}` : null; } const HWLAB_API_KEY_PREFIX = "hwl_live_"; function explicitApiKey(parsed: ParsedArgs, env: EnvLike) { assertOnlyHwlabApiKeySource(parsed, env); const explicit = text(env.HWLAB_API_KEY); if (!explicit) return ""; if (explicit.startsWith(HWLAB_API_KEY_PREFIX)) return explicit; return ""; } function assertOnlyHwlabApiKeySource(parsed: ParsedArgs, env: EnvLike) { const unsupported = [ text(parsed.apiKey) ? "--api-key" : "", text(parsed.bearerToken) ? "--bearer-token" : "", text(env.HWLAB_BEARER_TOKEN) ? "HWLAB_BEARER_TOKEN" : "" ].filter(Boolean); if (unsupported.length === 0) return; throw cliError("unsupported_api_key_source", "HWLAB CLI auth uses only HWLAB_API_KEY; remove API key aliases and export HWLAB_API_KEY instead.", { allowed: "HWLAB_API_KEY", unsupported }); } function apiKeyPrefixSummary(secret) { const text = String(secret ?? ""); if (!text) return ""; if (text.length <= 12) return text; return `${text.slice(0, 12)}\u2026`; } function autoAuthAllowed(parsed: ParsedArgs) { return parsed.noAuth !== true && parsed.noAutoAuth !== true; } async function loadSession({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) { return (await readSessionState({ parsed, env, cwd })).session; } async function loadStoredState({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) { const state = await readSessionState({ parsed, env, cwd }); return state.stored ?? state.session ?? null; } async function readSessionState({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) { const file = stateFile(parsed, cwd, env); const expectedBaseUrl = baseUrl(parsed, env); let raw = ""; try { raw = await readFile(file, "utf8"); } catch (error) { return { stateFile: file, expectedBaseUrl, exists: false, readable: false, usable: false, ignoredReason: error?.code === "ENOENT" ? "state_file_missing" : "state_file_unreadable", session: null }; } let stored: any = null; try { stored = JSON.parse(raw); } catch { return { stateFile: file, expectedBaseUrl, exists: true, readable: true, parseOk: false, usable: false, ignoredReason: "state_file_invalid_json", session: null }; } const storedBaseUrl = text(stored?.baseUrl); const cookieStored = Boolean(text(stored?.cookie)); const baseUrlMatches = storedBaseUrl === expectedBaseUrl; const runtimeScopeMatches = !baseUrlMatches && sameRuntimeEndpointScope(storedBaseUrl, expectedBaseUrl); const endpointMatches = baseUrlMatches || runtimeScopeMatches; const usable = Boolean(endpointMatches && cookieStored); return { stateFile: file, expectedBaseUrl, exists: true, readable: true, parseOk: true, usable, ignoredReason: usable ? null : !endpointMatches ? "base_url_mismatch" : "cookie_missing", baseUrlMatch: baseUrlMatches ? "exact" : runtimeScopeMatches ? "runtime_scope" : "none", baseUrl: storedBaseUrl || null, cookieStored, user: safeUser(stored?.user), expiresAt: textOrNull(stored?.expiresAt), updatedAt: textOrNull(stored?.updatedAt), stored, session: usable ? stored : null }; } async function saveSession(context: any, session: Record) { const file = stateFile(context.parsed, context.cwd, context.env); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(session, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); await chmod(file, 0o600).catch(() => {}); } async function clearSession(context: any) { await rm(stateFile(context.parsed, context.cwd, context.env), { force: true }); } function stateFile(parsed: ParsedArgs, cwd: string, env: EnvLike = process.env) { const explicit = text(parsed.stateFile); if (explicit) return path.resolve(cwd, explicit); const profile = profileName(parsed); if (profile) return path.join(profileStateDir(parsed, cwd, env), `${profile}.json`); return path.resolve(cwd, ".state/hwlab-cli/session.json"); } function profileName(parsed: ParsedArgs) { const value = text(parsed.profile); if (!value) return ""; if (!/^[A-Za-z0-9_.:-]+$/u.test(value)) { throw cliError("invalid_profile_name", "profile may only contain letters, numbers, dot, underscore, colon, and dash", { profile: value }); } return value; } function profileStateDir(parsed: ParsedArgs, cwd: string, env: EnvLike = process.env) { return path.resolve(cwd, ".state/hwlab-cli/profiles", sha256(baseUrl(parsed, env)).slice(0, 16)); } function cookieFromResponse(response: any) { const values = typeof response.headers?.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers?.get?.("set-cookie")].filter(Boolean); const raw = values.find((value: string) => /(?:^|,)\s*hwlab_session=/u.test(value)) ?? values[0]; if (!raw) return null; const match = String(raw).match(/hwlab_session=[^;,\s]+/u); return match?.[0] ?? String(raw).split(";", 1)[0]; } function responsePayload(action: string, response: any, context: any, extra: Record = {}) { const success = responseSucceeded(response); return { ok: success, action, status: success ? "succeeded" : "failed", baseUrl: response.runtimeEndpoint?.baseUrl ?? baseUrl(context.parsed, context.env, context.endpointKind ?? "web"), runtimeEndpoint: runtimeEndpointVisibility(response.runtimeEndpoint ?? runtimeEndpoint(context.parsed, context.env, context.endpointKind ?? "web")), httpStatus: response.status, route: extra.route ?? route(response.method, response.path), request: requestVisibility(response), ...(response.auth ? { auth: response.auth } : {}), ...(response.authDiagnosis ? { authDiagnosis: response.authDiagnosis } : {}), ...extra, body: extra.body ?? response.body }; } function responseSucceeded(response: any) { return isHttpSuccess(response) && response.body?.ok !== false; } function requestVisibility(response: any) { return pruneUndefined({ method: response.method, path: response.path, url: response.url, httpStatus: response.status, elapsedMs: response.elapsedMs, timeoutMs: response.timeoutMs, timedOut: response.timedOut === true ? true : undefined, transportError: response.transportError ? pruneUndefined({ code: response.transportError.code, name: response.transportError.name, message: response.transportError.message }) : undefined }); } function authVisibility(authState: any) { if (!authState?.required) return { required: false }; return pruneUndefined({ required: true, authMethod: authState.authMethod, requiredAuthMethod: authState.requiredAuthMethod, credentialSource: authState.credentialSource, baseUrl: authState.baseUrl, username: authState.username, cookieSource: authState.cookieSource, stateFile: authState.stateFile, localSession: authState.localSession, apiKeySource: authState.apiKeySource, apiKeyPrefix: authState.apiKeyPrefix, webSessionAllowed: authState.webSessionAllowed, webSessionIgnoredReason: authState.webSessionIgnoredReason, autoLoginAttempted: authState.autoLoginAttempted, autoLoginStatus: authState.autoLoginStatus, autoLoginHttpStatus: authState.autoLoginHttpStatus, retryAfterLogin: authState.retryAfterLogin }); } function authDiagnosis(response: any, authState: any) { if (!authState?.required) return null; if (response.status !== 401 && response.status !== 403) return null; if (response.status === 401 && authState.apiKeySource) { return { code: "api_key_invalid", status: "unauthorized", message: "HWLAB_API_KEY was rejected by the cloud-api; regenerate the key with `client auth default` or check that the env value matches the one shown in the Web API key page.", httpStatus: response.status, apiKeySource: authState.apiKeySource, apiKeyPrefix: authState.apiKeyPrefix, nextCommands: ["bun tools/hwlab-cli/bin/hwlab-cli.ts client request POST /v1/api-keys -H 'content-type: application/json' --body '{\"name\":\"rotated\"}' --help"] }; } if (response.status === 403) { return { code: "auth_forbidden", status: "forbidden", message: "当前账号无权访问该受保护资源,可能是 trace/result 不属于当前 owner,或当前用户不是 admin。", httpStatus: response.status, authMethod: authState.authMethod, requiredAuthMethod: authState.requiredAuthMethod, cookieSource: authState.cookieSource, autoLoginAttempted: authState.autoLoginAttempted, retryAfterLogin: authState.retryAfterLogin, stateFile: authState.stateFile, localSession: authState.localSession, nextCommands: authNextCommands(authState) }; } if (authState.requiredAuthMethod === "api-key") { return { code: "api_key_required", status: "credentials_missing", message: "CLI protected commands require HWLAB_API_KEY / Authorization: Bearer hwl_live_...; local Web session state is not used by default.", httpStatus: response.status, authMethod: authState.authMethod, requiredAuthMethod: "api-key", webSessionIgnoredReason: authState.webSessionIgnoredReason, stateFile: authState.stateFile, localSession: authState.localSession, nextCommands: authNextCommands(authState) }; } const missingCredentials = authState.autoLoginAttempted && authState.autoLoginStatus === "credentials_missing"; return { code: missingCredentials ? "web_session_credentials_missing" : "web_session_required_or_expired", status: missingCredentials ? "credentials_missing" : "unauthorized", message: missingCredentials ? "Web session 诊断需要显式登录态;请先用浏览器或 legacy client auth login 建立 cookie。" : "Web session 不存在或已过期;CLI 默认鉴权请改用 HWLAB_API_KEY。", httpStatus: response.status, authMethod: authState.authMethod, requiredAuthMethod: authState.requiredAuthMethod, cookieSource: authState.cookieSource, autoLoginAttempted: authState.autoLoginAttempted, autoLoginStatus: authState.autoLoginStatus, autoLoginHttpStatus: authState.autoLoginHttpStatus, retryAfterLogin: authState.retryAfterLogin, stateFile: authState.stateFile, localSession: authState.localSession, nextCommands: authNextCommands(authState) }; } function sessionStateSummary(state: any) { return pruneUndefined({ stateFile: state.stateFile, expectedBaseUrl: state.expectedBaseUrl, exists: state.exists, readable: state.readable, parseOk: state.parseOk, usable: state.usable, ignoredReason: state.ignoredReason, baseUrlMatch: state.baseUrlMatch, baseUrl: state.baseUrl, cookieStored: state.cookieStored, user: state.user, expiresAt: state.expiresAt, updatedAt: state.updatedAt }); } function authNextCommands(authState: any) { const cli = "bun tools/hwlab-cli/bin/hwlab-cli.ts"; return [ `export HWLAB_API_KEY='hwl_live_...'`, `${cli} client auth whoami --runtime-namespace `, `${cli} client auth status --runtime-namespace ` ]; } async function loadCliWorkbenchState(context: any) { const session = await loadStoredState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }) ?? {}; return normalizeCliWorkbenchState(session.workbench); } async function saveCliWorkbenchState(context: any, patch: any) { const session = await loadStoredState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }) ?? {}; const existing = normalizeCliWorkbenchState(session.workbench) ?? {}; const next = normalizeCliWorkbenchState({ ...existing, ...patch, updatedByClient: "hwlab-cli", updatedAt: context.now() }); await saveSession(context, { ...session, baseUrl: baseUrl(context.parsed, context.env), workbench: next, updatedAt: context.now() }); return next; } function normalizeCliWorkbenchState(value: any) { if (!value || typeof value !== "object") return null; return clean({ activeSessionId: safeOptionalSessionId(value.activeSessionId), activeTraceId: text(value.activeTraceId), threadId: text(value.threadId), providerProfile: text(value.providerProfile), sessionStatus: text(value.sessionStatus), messages: Array.isArray(value.messages) ? value.messages : undefined, updatedAt: text(value.updatedAt), updatedByClient: text(value.updatedByClient), valuesRedacted: value.valuesRedacted === true ? true : undefined, secretMaterialStored: value.secretMaterialStored === true ? true : undefined }); } function sessionSummaries(sessions: any) { return Array.isArray(sessions) ? sessions.map(sessionSummary).filter(Boolean) : []; } function sessionSummary(session: any) { if (!session || typeof session !== "object") return null; return clean({ sessionId: text(session.sessionId ?? session.id), threadId: text(session.threadId ?? session.session?.threadId), status: text(session.status ?? session.sessionStatus), lastTraceId: text(session.lastTraceId ?? session.currentTraceId), providerProfile: text(session.providerProfile), updatedAt: text(session.updatedAt), messageCount: numberOrZero(session.messageCount) || (Array.isArray(session.messages) ? session.messages.length : 0) || undefined, firstUserMessagePreview: text(session.firstUserMessagePreview) || undefined, valuesRedacted: session.valuesRedacted === true ? true : undefined }); } function latestAgentMessage(source: any) { const messages = Array.isArray(source?.messages) ? source.messages : []; return [...messages].reverse().find((message: any) => ["agent", "assistant"].includes(text(message?.role))) ?? null; } function finalResponseSessionSummary(session: any) { if (!session || typeof session !== "object") return null; const latest = latestAgentMessage(session); return clean({ found: true, ...sessionSummary(session), latestAgent: latest ? clean({ id: text(latest.id), status: text(latest.status), traceId: text(latest.traceId), updatedAt: text(latest.updatedAt), textPreview: preview(latest.text, 240), markdown: markdownTextSummary(latest.text) }) : undefined }); } function finalResponseMessagesSummary(messages: any[]) { const latest = latestAgentMessage({ messages }); return clean({ count: Array.isArray(messages) ? messages.length : 0, latestAgent: latest ? clean({ id: text(latest.id), status: text(latest.status), traceId: text(latest.traceId), updatedAt: text(latest.updatedAt), textPreview: preview(latest.text, 240), markdown: markdownTextSummary(latest.text) }) : undefined }); } function finalResponseResultSummary(body: any) { const resultText = assistantText(body) || ""; return clean({ status: text(body?.status), traceId: text(body?.traceId), sessionId: text(body?.sessionId), threadId: text(body?.threadId ?? body?.session?.threadId ?? body?.providerTrace?.threadId), assistantTextPreview: preview(resultText, 240), assistantTextChars: resultText.length || undefined, markdown: markdownTextSummary(resultText) }); } function markdownTextSummary(value: unknown) { const source = messageTextValue(value); if (!source) return undefined; const newlineCount = (source.match(/\n/gu) ?? []).length; const hasTableSeparator = /\|\s*:?-{3,}:?\s*\|/u.test(source); const hasFence = /```/u.test(source); const likelyFlattened = newlineCount === 0 && (hasTableSeparator || /```[\s\S]+```/u.test(source)); return clean({ textChars: source.length, newlineCount, hasTablePipe: /\|/u.test(source) || undefined, hasTableSeparator: hasTableSeparator || undefined, hasFence: hasFence || undefined, likelyFlattened: likelyFlattened || undefined }); } function finalResponseEarlyValidation({ sessionId, routeName, response, failure }: any) { const failures = [failure].filter(Boolean); return { ok: false, state: "failed", summary: `${routeName} step failed before final response comparison`, sessionId, traceId: undefined, checks: { inspectFound: false, messagesFound: false, resultHasFinalText: false }, httpStatus: response?.status, error: response?.body?.error && typeof response.body.error === "object" ? clean({ code: text(response.body.error.code), message: text(response.body.error.message) }) : compactApiBody(response?.body), failures, nextCommands: [ `hwlab-cli client session inspect ${sessionId} --full`, `hwlab-cli client session final-response ${sessionId} --full` ] }; } function finalResponseValidation({ sessionId, inspectSession, messages, traceId, resultBody }: any) { const inspectedLatest = latestAgentMessage(inspectSession); const messageLatest = latestAgentMessage({ messages }); const inspectText = text(inspectedLatest?.text); const messageText = text(messageLatest?.text); const resultText = assistantText(resultBody) || ""; const inspectMarkdown = markdownTextSummary(inspectText) ?? {}; const messageMarkdown = markdownTextSummary(messageText) ?? {}; const resultMarkdown = markdownTextSummary(resultText) ?? {}; const failures = [ !inspectSession ? "inspect-session-missing" : "", !Array.isArray(messages) ? "messages-page-missing" : "", !traceId ? "trace-id-missing" : "", !resultText ? "result-final-response-missing" : "", text(inspectSession?.lastTraceId) && text(inspectSession?.lastTraceId) !== traceId ? "inspect-last-trace-mismatch" : "", inspectedLatest && text(inspectedLatest.traceId) !== traceId ? "inspect-latest-agent-trace-mismatch" : "", messageLatest && text(messageLatest.traceId) !== traceId ? "message-latest-agent-trace-mismatch" : "", inspectText && resultText && inspectText !== resultText ? "inspect-text-result-mismatch" : "", messageText && resultText && messageText !== resultText ? "message-text-result-mismatch" : "", inspectMarkdown.likelyFlattened === true && resultMarkdown.newlineCount > 0 ? "inspect-markdown-flattened" : "", messageMarkdown.likelyFlattened === true && resultMarkdown.newlineCount > 0 ? "message-markdown-flattened" : "" ].filter(Boolean); return { ok: failures.length === 0, state: failures.length === 0 ? "passed" : "failed", summary: failures.length === 0 ? "session detail, message page, and Workbench turn agree on the final assistant response" : `final response validation failed with ${failures.length} issue(s)`, sessionId, traceId, checks: { inspectFound: Boolean(inspectSession), messagesFound: Array.isArray(messages), resultHasFinalText: Boolean(resultText), inspectLastTraceMatches: !text(inspectSession?.lastTraceId) || text(inspectSession?.lastTraceId) === traceId, inspectLatestAgentTraceMatches: inspectedLatest ? text(inspectedLatest.traceId) === traceId : false, messageLatestAgentTraceMatches: messageLatest ? text(messageLatest.traceId) === traceId : false, inspectTextMatchesResult: Boolean(inspectText && resultText && inspectText === resultText), messageTextMatchesResult: Boolean(messageText && resultText && messageText === resultText), inspectMarkdownNotFlattened: inspectMarkdown.likelyFlattened !== true, messageMarkdownNotFlattened: messageMarkdown.likelyFlattened !== true, resultMarkdownHasStructure: resultMarkdown.newlineCount > 0 || resultMarkdown.hasTableSeparator === true || resultMarkdown.hasFence === true }, previews: { inspectText: preview(inspectText, 200), messageText: preview(messageText, 200), resultText: preview(resultText, 200) }, markdown: { inspect: inspectMarkdown, messages: messageMarkdown, result: resultMarkdown }, failures, nextCommands: [ `hwlab-cli client session inspect ${sessionId} --full`, `hwlab-cli client session final-response ${sessionId} --full`, `hwlab-cli client agent result ${traceId} --full` ] }; } function numberOrZero(value: any) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; } function agentSessionSummary(session: any, fallback: any = {}) { return pruneUndefined({ sessionId: text(session?.sessionId) || text(session?.id) || text(fallback.sessionId), threadId: text(session?.threadId) || text(fallback.threadId), providerProfile: text(session?.providerProfile) || text(fallback.providerProfile), sessionStatus: text(session?.status) || text(fallback.status), sessionUsable: typeof session?.usable === "boolean" ? session.usable : undefined }); } function compactAgentMessagesForWorkbench(resultBody: any, fallback: any) { const traceId = text(resultBody?.traceId) || text(fallback.traceId); const sessionId = text(resultBody?.sessionId) || text(fallback.sessionId); const threadId = text(resultBody?.threadId ?? resultBody?.session?.threadId ?? resultBody?.providerTrace?.threadId ?? fallback.threadId); const now = new Date().toISOString(); const userText = messageTextValue(fallback.message ?? resultBody?.userMessage ?? resultBody?.prompt); const messages = []; if (userText) { messages.push({ id: traceId ? `msg_${traceId.slice(4)}_user` : makeId("msg_user"), role: "user", title: "用户", text: userText, status: "sent", traceId, sessionId, threadId, updatedAt: now }); } messages.push({ id: makeId("msg"), role: "agent", title: "Code Agent result", text: messageTextValue(assistantText(resultBody) || resultBody?.error?.message || resultBody?.status), status: text(resultBody?.status) || "unknown", traceId, sessionId, threadId, updatedAt: now }); return messages; } function compactProbe(method: string, pathName: string, response: any) { return { ok: isHttpSuccess(response) && response.body?.ok !== false, httpStatus: response.status, route: route(method, pathName), body: compactApiBody(response.body) }; } function compactResponse(response: any) { return { httpStatus: response.status, route: route(response.method, response.path), body: compactApiBody(response.body) }; } function agentContinuationSummary(value: { sessionId?: string | null; threadId?: string | null; retryOf?: string | null; fromTrace?: string | null }) { return pruneUndefined({ webEquivalent: true, shortConnection: true, replayedFromTrace: text(value.fromTrace), sessionId: text(value.sessionId), threadId: text(value.threadId), retryOf: text(value.retryOf), valuesPrinted: false }); } function responseBodyForCli(body: any, parsed: ParsedArgs) { return parsed.full === true ? body : compactApiBody(body); } function identityBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactUserQueryBody(body); const source = body?.data && typeof body.data === "object" ? body.data : body; return pruneUndefined({ ok: body?.ok, authenticated: source?.authenticated ?? body?.authenticated, actor: actorForCli(source?.actor ?? body?.actor), user: actorForCli(source?.user ?? body?.user), authMethod: text(source?.authMethod ?? body?.authMethod) || undefined, session: apiKeyMetadataForCli(source?.session ?? body?.session ?? source?.apiKey ?? body?.apiKey), roles: arrayOfText(source?.roles ?? body?.roles), permissions: arrayOfText(source?.permissions ?? body?.permissions), scopes: arrayOfText(source?.scopes ?? body?.scopes), error: errorForCli(body?.error), valuesPrinted: false, fullBodyAvailable: true }); } function accessStatusBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactUserQueryBody(body); const source = body?.data && typeof body.data === "object" ? body.data : body; const routes = source?.routes && typeof source.routes === "object" ? Object.keys(source.routes).sort() : undefined; const roleCatalog = arrayOfText(source?.roles ?? body?.roles); return pruneUndefined({ ok: body?.ok, authenticated: source?.authenticated ?? body?.authenticated, actor: actorForCli(source?.actor ?? body?.actor), user: actorForCli(source?.user ?? body?.user), role: text(source?.role ?? body?.role ?? source?.actor?.role ?? body?.actor?.role) || undefined, authMethod: text(source?.authMethod ?? body?.authMethod) || undefined, permissions: arrayOfText(source?.permissions ?? body?.permissions), scopes: arrayOfText(source?.scopes ?? body?.scopes), tools: source?.tools, roleCatalogCount: roleCatalog?.length, routeCount: routes?.length, key: apiKeyMetadataForCli(source?.key ?? source?.apiKey ?? body?.key ?? body?.apiKey), error: errorForCli(body?.error), valuesPrinted: false, fullBodyAvailable: true }); } function apiKeysBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactUserQueryBody(body); const source = body?.data && typeof body.data === "object" ? body.data : body; const items = firstArray(source?.items, source?.apiKeys, source?.keys, body?.items, body?.apiKeys, body?.keys); return pruneUndefined({ ok: body?.ok, status: text(source?.status ?? body?.status) || undefined, actor: actorForCli(source?.actor ?? body?.actor), authMethod: text(source?.authMethod ?? body?.authMethod) || undefined, items: items.map(apiKeyMetadataForCli).filter(Boolean), count: source?.count ?? body?.count ?? items.length, valuesPrinted: false, fullBodyAvailable: true }); } function billingSummaryBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactUserQueryBody(body); const source = body?.data && typeof body.data === "object" ? body.data : body; const ledgerItems = ledgerItemsFromBody(source); return pruneUndefined({ ok: body?.ok, status: text(source?.status ?? body?.status) || undefined, actor: actorForCli(source?.actor ?? body?.actor), authMethod: text(source?.authMethod ?? body?.authMethod) || undefined, credits: creditsForCli(source?.credits ?? source?.balance ?? source?.wallet), balance: typeof source?.balance === "number" ? source.balance : undefined, available: source?.available ?? source?.availableCredits, reserved: source?.reserved ?? source?.reservedCredits, plan: compactApiBody(redactUserQueryBody(source?.plan ?? source?.subscription?.plan)), entitlement: compactApiBody(redactUserQueryBody(source?.entitlement ?? source?.entitlements)), usage: usageSummaryCompact(source?.usage), ledger: ledgerItems.length > 0 ? { count: source?.ledger?.count ?? source?.ledgerCount ?? ledgerItems.length, items: ledgerItems.slice(0, 20).map(ledgerEntryForCli) } : undefined, error: errorForCli(body?.error), valuesPrinted: false, fullBodyAvailable: true }); } function usageSummaryBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactUserQueryBody(body); const source = body?.data && typeof body.data === "object" ? body.data : body; const usageRecords = firstArray(source?.records, source?.usageRecords, source?.usage?.records); const recordCount = source?.recordCount ?? source?.usage?.recordCount ?? usageRecords.length; return pruneUndefined({ ok: body?.ok, status: text(source?.status ?? body?.status) || undefined, actor: actorForCli(source?.actor ?? body?.actor), authMethod: text(source?.authMethod ?? body?.authMethod) || undefined, usage: usageSummaryCompact(source?.usage ?? source), byService: usageByServiceForCli(source?.byService ?? source?.usage?.byService), records: usageRecords.slice(0, 20).map(usageRecordForCli), recordCount: recordCount || undefined, error: errorForCli(body?.error), valuesPrinted: false, fullBodyAvailable: true }); } function actorForCli(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ id: text(value.id), username: text(value.username), displayName: text(value.displayName), role: text(value.role), status: text(value.status), email: text(value.email) || undefined }); } function apiKeyMetadataForCli(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ id: text(value.id ?? value.keyId), keyId: text(value.keyId ?? value.id), name: text(value.name ?? value.keyName), prefix: text(value.prefix ?? value.keyPrefix), source: text(value.source ?? value.sourceKind), status: text(value.status), scopes: arrayOfText(value.scopes), createdAt: text(value.createdAt), updatedAt: text(value.updatedAt), lastUsedAt: text(value.lastUsedAt), expiresAt: text(value.expiresAt), fingerprint: text(value.fingerprint ?? value.keyFingerprint), valuesRedacted: value.valuesRedacted === false ? false : true }); } function ledgerItemsFromBody(source: any) { const ledger = source?.ledger; return firstArray(ledger?.items, ledger?.entries, ledger?.rows, source?.ledgerItems, source?.entries, source?.rows); } function ledgerEntryForCli(value: any) { return pruneUndefined({ id: text(value?.id ?? value?.ledgerId), kind: text(value?.kind ?? value?.type), reason: text(value?.reason), service: text(value?.service ?? value?.serviceId), resourceType: text(value?.resourceType), deltaCredits: value?.deltaCredits ?? value?.delta, beforeCredits: value?.beforeCredits ?? value?.before, afterCredits: value?.afterCredits ?? value?.after, traceId: text(value?.traceId ?? value?.metadata?.traceId), sessionId: text(value?.sessionId ?? value?.metadata?.sessionId), conversationId: text(value?.conversationId ?? value?.metadata?.conversationId), createdAt: text(value?.createdAt), valuesPrinted: false }); } function usageSummaryCompact(value: any) { if (!value || typeof value !== "object") return compactApiBody(redactUserQueryBody(value)); return pruneUndefined({ credits: value.credits, totalCredits: value.totalCredits, recordCount: value.recordCount, byService: usageByServiceForCli(value.byService), lastUsedAt: text(value.lastUsedAt), fullBodyAvailable: true }); } function creditsForCli(value: any) { if (!value || typeof value !== "object") return value; return pruneUndefined({ balance: value.balance ?? value.balanceCredits, available: value.available ?? value.availableCredits, reserved: value.reserved ?? value.reservedCredits, total: value.total ?? value.totalCredits, currency: text(value.currency), fullBodyAvailable: true }); } function usageByServiceForCli(value: any) { if (Array.isArray(value)) return value.slice(0, 20).map(usageServiceForCli); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, usageServiceForCli(item)])); return undefined; } function usageServiceForCli(value: any) { if (!value || typeof value !== "object") return value; return pruneUndefined({ service: text(value.service ?? value.serviceId), serviceId: text(value.serviceId ?? value.service), resourceType: text(value.resourceType), credits: value.credits ?? value.totalCredits, recordCount: value.recordCount, lastUsedAt: text(value.lastUsedAt), fullBodyAvailable: true }); } function errorForCli(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ code: text(value.code), status: text(value.status), message: text(value.message), details: value.details ? compactApiBody(redactUserQueryBody(value.details)) : undefined, fullBodyAvailable: true }); } function selfServiceAvailability(response: any, subject: string) { if (responseSucceeded(response)) return { status: "available", subject }; if (response.status === 403 || response.status === 404) { return pruneUndefined({ status: "not_available", subject, httpStatus: response.status, reason: text(response.body?.error?.code ?? response.body?.status) || "unsupported_or_forbidden", message: text(response.body?.error?.message) || undefined }); } return pruneUndefined({ status: "failed", subject, httpStatus: response.status, reason: text(response.body?.error?.code ?? response.body?.status) || undefined, message: text(response.body?.error?.message) || undefined }); } function usageRecordForCli(value: any) { return pruneUndefined({ id: text(value?.id ?? value?.usageId), service: text(value?.service ?? value?.serviceId), resourceType: text(value?.resourceType), credits: value?.credits ?? value?.deltaCredits, traceId: text(value?.traceId ?? value?.metadata?.traceId), sessionId: text(value?.sessionId ?? value?.metadata?.sessionId), conversationId: text(value?.conversationId ?? value?.metadata?.conversationId), status: text(value?.status), createdAt: text(value?.createdAt), valuesPrinted: false }); } function firstArray(...values: any[]) { for (const value of values) { if (Array.isArray(value)) return value; } return []; } function arrayOfText(value: any) { return Array.isArray(value) ? value.map(text).filter(Boolean) : undefined; } function adminAccessBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return body; const access = body?.access && typeof body.access === "object" ? body.access : null; const source = access ?? body; const users = Array.isArray(source?.users) ? source.users : []; const tuples = Array.isArray(source?.tuples) ? source.tuples : []; const openfga = source?.openfga ?? body?.openfga; return pruneUndefined({ actor: compactApiBody(source?.actor ?? body?.actor), subject: compactApiBody(body?.subject), user: compactApiBody(source?.user ?? body?.user), contractVersion: text(source?.contractVersion ?? body?.contractVersion) || undefined, openfga: openfga ? pruneUndefined({ mode: text(openfga.mode), ready: openfga.ready === true, status: text(openfga.status), configured: openfga.configured === true, storeId: text(openfga.storeId), modelId: text(openfga.modelId), degradedReason: text(openfga.degradedReason), valuesRedacted: openfga.valuesRedacted === true }) : undefined, counts: source?.counts, supported: source?.supported, users: users.length > 0 ? users.map(adminAccessUserSummaryForCli) : undefined, userCount: users.length || undefined, tools: source?.tools, tupleCount: tuples.length || source?.tupleCount, tuples: tuples.length > 0 ? tuples.map(adminAccessTupleForCli) : undefined, authorization: body?.authorization ? pruneUndefined({ mode: text(body.authorization.mode), allowed: body.authorization.allowed === true, fgaAllowed: body.authorization.fgaAllowed ?? undefined, mismatch: body.authorization.mismatch === true, decisionSource: text(body.authorization.decisionSource), relation: text(body.authorization.relation), object: text(body.authorization.object), degradedReason: text(body.authorization.degradedReason), valuesRedacted: body.authorization.valuesRedacted === true }) : undefined, result: body?.result ? compactApiBody(body.result) : undefined, error: body?.error ? compactApiBody(body.error) : undefined, changed: body?.changed === true ? true : undefined, updated: body?.updated === true ? true : undefined, ok: body?.ok, fullBodyAvailable: true }); } function adminAccessUserSummaryForCli(value: any) { return pruneUndefined({ user: compactApiBody(value?.user), tupleCount: value?.tupleCount, tools: value?.tools }); } function adminAccessTupleForCli(value: any) { return pruneUndefined({ userId: text(value?.userId), relation: text(value?.relation), object: text(value?.object), createdAt: text(value?.createdAt) }); } function providerProfileBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactSecretLike(body); const source = body?.data && typeof body.data === "object" ? body.data : body; const items = Array.isArray(body?.items) ? body.items : Array.isArray(source?.items) ? source.items : []; return pruneUndefined({ ok: body?.ok, status: text(body?.status) || text(source?.status) || undefined, contractVersion: text(body?.contractVersion) || text(source?.contractVersion) || undefined, actor: compactApiBody(body?.actor ?? source?.actor), profile: text(body?.profile ?? source?.profile) || undefined, delegation: providerProfileDelegationForCli(body?.delegation ?? source?.delegation), items: items.length > 0 ? items.map(providerProfileStatusForCli) : undefined, count: items.length || body?.count || source?.count, profileStatus: source?.profile ? providerProfileStatusForCli(source) : undefined, validation: providerProfileValidationForCli(source?.validationId ? source : body?.validationId ? body : null), error: body?.error ? compactApiBody(body.error) : undefined, valuesPrinted: false, fullBodyAvailable: true }); } function providerProfileConfigBodyForCli(body: any, parsed: ParsedArgs) { if (parsed.full === true) return redactSecretLike(body); const source = body?.data && typeof body.data === "object" ? body.data : body; const configToml = typeof source?.configToml === "string" ? source.configToml : undefined; return pruneUndefined({ ok: body?.ok, status: text(body?.status) || text(source?.status) || undefined, contractVersion: text(body?.contractVersion) || text(source?.contractVersion) || undefined, actor: compactApiBody(body?.actor ?? source?.actor), profile: text(body?.profile ?? source?.profile) || undefined, delegation: providerProfileDelegationForCli(body?.delegation ?? source?.delegation), profileStatus: source?.profile ? providerProfileStatusForCli(source) : undefined, configToml, configBytes: configToml !== undefined ? Buffer.byteLength(configToml, "utf8") : undefined, configTomlPrinted: configToml !== undefined, credentialValuesPrinted: false, error: body?.error ? compactApiBody(body.error) : undefined, valuesPrinted: false, fullBodyAvailable: true }); } function providerProfileStatusForCli(value: any) { return pruneUndefined({ profile: text(value?.profile), backendProfile: text(value?.backendProfile), backendKind: text(value?.backendKind), configured: value?.configured === true, removed: value?.removed === true ? true : undefined, alreadyAbsent: value?.alreadyAbsent === true ? true : undefined, builtinCapabilityRetained: value?.builtinCapabilityRetained === true ? true : undefined, failureKind: text(value?.failureKind) || undefined, secretRef: providerSecretRefForCli(value?.secretRef), resourceVersion: text(value?.resourceVersion) || undefined, deletedResourceVersion: text(value?.deletedResourceVersion) || undefined, credentialHashSuffix: text(value?.credentialHashSuffix) || undefined, configHashSuffix: text(value?.configHashSuffix) || undefined, updatedAt: text(value?.updatedAt) || undefined, keyPresence: value?.keyPresence, bridge: providerBridgeForCli(value?.bridge), lastValidation: providerProfileValidationForCli(value?.lastValidation), requiresExternalBridgeUpdate: value?.requiresExternalBridgeUpdate === true ? true : undefined, valuesPrinted: false }); } function providerProfileValidationForCli(value: any) { if (!value || typeof value !== "object") return undefined; const result = value.result && typeof value.result === "object" ? value.result : {}; const agentRun = result.agentRun && typeof result.agentRun === "object" ? result.agentRun : {}; const runnerJobs = Array.isArray(value.runnerJobs) ? value.runnerJobs : []; return pruneUndefined({ validationId: text(value.validationId) || undefined, profile: text(value.profile) || undefined, runId: text(value.runId) || text(agentRun.runId) || undefined, commandId: text(value.commandId) || text(agentRun.commandId) || undefined, jobName: text(runnerJobs[0]?.jobName) || text(agentRun.jobName) || undefined, traceId: text(result.traceId ?? agentRun.traceId) || undefined, status: text(value.status) || text(result.status) || undefined, terminalStatus: text(result.terminalStatus) || undefined, failureKind: text(result.failureKind) || undefined, bridge: providerBridgeForCli(value.bridge), valuesPrinted: false }); } function providerSecretRefForCli(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ namespace: text(value.namespace), name: text(value.name), keys: Array.isArray(value.keys) ? value.keys.map(text).filter(Boolean) : undefined, mountPath: text(value.mountPath) || undefined }); } function providerBridgeForCli(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ kind: text(value.kind), route: text(value.route), serviceUrl: text(value.serviceUrl), responsesPath: text(value.responsesPath), forbiddenHosts: Array.isArray(value.forbiddenHosts) ? value.forbiddenHosts.map(text).filter(Boolean) : undefined, valuesPrinted: false }); } function providerProfileDelegationForCli(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ provider: text(value.provider), requestId: text(value.requestId), agentRunBaseUrl: text(value.agentRunBaseUrl), agentRunHost: text(value.agentRunHost), traceId: text(value.traceId), httpStatus: value.httpStatus, elapsedMs: value.elapsedMs, valuesPrinted: false }); } function redactSecretLike(value: any): any { if (Array.isArray(value)) return value.map(redactSecretLike); if (value && typeof value === "object") { return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, secretLikeField(key) ? "[redacted]" : redactSecretLike(item)])); } if (typeof value === "string" && /^sk-[A-Za-z0-9_-]{10,}/u.test(value)) return "[redacted]"; return value; } function redactUserQueryBody(value: any): any { if (Array.isArray(value)) return value.map(redactUserQueryBody); if (value && typeof value === "object") { return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, userQuerySecretLikeField(key) ? "[redacted]" : redactUserQueryBody(item)])); } if (typeof value === "string" && (/^sk-[A-Za-z0-9_-]{10,}/u.test(value) || /^hwl_live_[A-Za-z0-9_-]{10,}/u.test(value))) return "[redacted]"; return value; } function secretLikeField(key: string) { const lower = key.toLowerCase(); return ["apikey", "api_key", "authorization", "auth.json", "config.toml", "authjson", "configtoml"].includes(lower) || lower.includes("password") || lower.endsWith("token"); } function userQuerySecretLikeField(key: string) { const lower = key.toLowerCase(); return secretLikeField(key) || ["key", "secret", "credential", "dsn", "database_url", "databaseurl"].includes(lower) || lower.includes("secret") || lower.includes("credential"); } function traceBodyForCli(body: any, parsed: ParsedArgs) { if (text(parsed.render) === "web") return webTraceRenderBody(body, parsed); return responseBodyForCli(body, parsed); } function webTraceRenderBody(traceObject: any, parsed: ParsedArgs) { const events = Array.isArray(traceObject?.events) ? traceObject.events : []; const rows = traceDisplayRows(traceObject ?? {}, events); const terminalRows = rows.length > 0 ? [] : terminalTraceEvidenceRows(traceObject); const allRows = rows.length > 0 ? rows : terminalRows; const noiseEventCount = traceNoiseEventCount(events); const rowLimit = Math.max(1, Math.min(numberOption(parsed.limit) ?? 80, 500)); const rowTail = parsed.full === true ? allRows : allRows.slice(-rowLimit); return pruneUndefined({ traceId: traceObject?.traceId ?? null, status: traceObject?.traceStatus ?? traceObject?.status ?? null, traceStatus: traceObject?.traceStatus ?? traceObject?.status ?? null, render: "web", renderer: "tools/src/hwlab-cli/trace-renderer:traceDisplayRows", sourceEventCount: traceObject?.eventCount ?? events.length, renderedRowCount: allRows.length, returnedRowCount: rowTail.length, noiseEventCount, omittedNoiseCount: noiseEventCount, rowLimit: parsed.full === true ? null : rowLimit, eventCountMismatch: Number.isInteger(traceObject?.eventCount) && traceObject.eventCount !== events.length ? { declared: traceObject.eventCount, loaded: events.length } : undefined, assistantText: assistantText(traceObject), rows: rowTail.map((row) => compactTraceRenderRow(row, { full: parsed.full === true })), retention: traceObject?.retention, terminalEvidence: traceObject?.terminalEvidence, finalResponse: traceObject?.finalResponse, traceSummary: traceObject?.traceSummary, fullBodyAvailable: true }); } function terminalTraceEvidenceRows(traceObject: any) { const evidence = traceObject?.terminalEvidence && typeof traceObject.terminalEvidence === "object" ? traceObject.terminalEvidence : null; const finalResponse = traceObject?.finalResponse ?? evidence?.finalResponse; const traceSummary = traceObject?.traceSummary ?? evidence?.traceSummary; const textValue = text(finalResponse?.text ?? finalResponse?.textPreview ?? traceSummary?.finalAssistantRow?.textPreview); if (!evidence?.available && !textValue && !traceSummary) return []; const body = [ textValue ? `final assistant: ${textValue}` : "final assistant: unavailable in AgentRun terminal evidence", `source: ${text(traceSummary?.source ?? evidence?.source ?? "agentrun-command-result")}`, `terminalStatus: ${text(traceSummary?.terminalStatus ?? finalResponse?.status ?? traceObject?.status ?? "unknown")}`, `sourceEventCount: ${traceSummary?.sourceEventCount ?? traceObject?.eventCount ?? 0}`, evidence?.conversationId ? `conversationId: ${evidence.conversationId}` : null, evidence?.sessionId ? `sessionId: ${evidence.sessionId}` : null, evidence?.threadId ? `threadId: ${evidence.threadId}` : null, evidence?.agentRun?.runId ? `runId: ${evidence.agentRun.runId}` : null, evidence?.agentRun?.commandId ? `commandId: ${evidence.agentRun.commandId}` : null ].filter(Boolean).join("\n"); return [{ rowId: `${traceObject?.traceId ?? "trace"}:terminal-evidence`, seq: null, tone: "info", header: "历史 trace 已过期,显示 AgentRun 终态证据", bodyFormat: "text", terminal: true, body }]; } function traceResponseAliases(traceBody: any, parsed: ParsedArgs) { if (text(parsed.render) !== "web" || !traceBody || typeof traceBody !== "object") return {}; const rendered = pruneUndefined({ traceId: traceBody.traceId ?? null, traceStatus: traceBody.status ?? null, render: traceBody.render, renderer: traceBody.renderer, sourceEventCount: traceBody.sourceEventCount, renderedRowCount: traceBody.renderedRowCount, returnedRowCount: traceBody.returnedRowCount, noiseEventCount: traceBody.noiseEventCount, omittedNoiseCount: traceBody.omittedNoiseCount, rowLimit: traceBody.rowLimit, rows: traceBody.rows, eventCountMismatch: traceBody.eventCountMismatch, assistantText: traceBody.assistantText, retention: traceBody.retention, terminalEvidence: traceBody.terminalEvidence, finalResponse: traceBody.finalResponse, traceSummary: traceBody.traceSummary }); return { traceStatus: traceBody.status ?? null, rendered, data: rendered }; } function compactTraceRenderRow(row: any, options: { full?: boolean } = {}) { const body = row?.body === undefined || row?.body === null ? undefined : String(row.body); return pruneUndefined({ rowId: row?.rowId ?? null, seq: row?.seq ?? null, tone: row?.tone ?? null, header: row?.header ?? null, bodyFormat: row?.bodyFormat ?? null, terminal: row?.terminal === true ? true : undefined, body: body ? (options.full === true || row?.terminal === true ? body : preview(body, 1200)) : undefined }); } function compactApiBody(body: any): any { if (Array.isArray(body)) return { itemCount: body.length, items: body.slice(0, 20).map(compactApiBody), fullBodyAvailable: true }; if (!body || typeof body !== "object") return body; if (body.jsonrpc === "2.0" || Object.hasOwn(body, "result")) return compactRpcBody(body); return pruneUndefined({ ...compactBody(body), assistantText: assistantText(body), reply: compactReply(body.reply ?? body.message), id: body.id, username: body.username, displayName: body.displayName, role: body.role, status: body.status, eventId: body.eventId, jobId: body.jobId, name: body.name, hwpodId: body.hwpodId, nodeId: body.nodeId, targetId: body.targetId, profile: body.profile, ts: body.ts, level: body.level, scope: body.scope, intent: body.intent, route: body.route, environment: body.environment, adapter: body.adapter, sourceKind: body.sourceKind, liveBackend: body.liveBackend, agentRun: compactAgentRun(body.agentRun), rowCount: body.rowCount, columns: Array.isArray(body.columns) ? body.columns : undefined, roles: Array.isArray(body.roles) ? body.roles : undefined, routes: body.routes && typeof body.routes === "object" ? Object.keys(body.routes) : undefined, events: Array.isArray(body.events) ? body.events.slice(0, 20).map(compactApiBody) : undefined, lines: Array.isArray(body.lines) ? body.lines.slice(0, 20) : undefined, assistantStreams: Array.isArray(body.assistantStreams) ? body.assistantStreams.slice(-3).map(compactAssistantStream) : undefined, refs: body.refs, eventCount: body.eventCount, elapsedMs: body.elapsedMs, methodsCount: Array.isArray(body.methods) ? body.methods.length : undefined, rowsCount: Array.isArray(body.rows) ? body.rows.length : undefined, eventsCount: Array.isArray(body.events) ? body.events.length : undefined, linesCount: Array.isArray(body.lines) ? body.lines.length : undefined, messagesCount: Array.isArray(body.messages) ? body.messages.length : undefined, assistantStreamsCount: Array.isArray(body.assistantStreams) ? body.assistantStreams.length : undefined, servicesCount: Array.isArray(body.services) ? body.services.length : undefined, fullBodyAvailable: true }); } function compactAgentRun(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ adapter: value.adapter, runId: value.runId, commandId: value.commandId, targetCommandId: value.targetCommandId, steerCommandId: value.steerCommandId, lastSteerCommandId: value.lastSteerCommandId, attemptId: value.attemptId, runnerId: value.runnerId, jobName: value.jobName, namespace: value.namespace, backendProfile: value.backendProfile, status: value.status, runStatus: value.runStatus, commandState: value.commandState, terminalStatus: value.terminalStatus, reuseEligible: value.reuseEligible, valuesPrinted: false }); } function assistantText(body: any) { const direct = text(body?.reply?.content ?? body?.message?.content ?? body?.assistantText ?? body?.finalResponse?.text ?? body?.terminalEvidence?.finalResponse?.text); if (direct) return direct; if (Array.isArray(body?.assistantStreams)) { for (const item of [...body.assistantStreams].reverse()) { const value = text(item?.text ?? item?.lastChunk); if (value) return value; } } return undefined; } function compactReply(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ messageId: value.messageId, role: value.role, content: value.content, createdAt: value.createdAt }); } function compactAssistantStream(value: any) { if (!value || typeof value !== "object") return undefined; return pruneUndefined({ status: value.status, chunkCount: value.chunkCount, text: value.text, lastChunk: value.lastChunk, updatedAt: value.updatedAt }); } function compactTraceBody(traceObject: any, limit: number) { const events = Array.isArray(traceObject?.events) ? traceObject.events : []; const tail = events.slice(-limit).map((event: any) => ({ at: event.createdAt ?? event.timestamp ?? null, label: event.label ?? null, status: event.status ?? null, waitingFor: event.waitingFor ?? null, message: preview(event.message ?? event.command ?? event.error ?? null, 240) })); return pruneUndefined({ traceId: traceObject?.traceId ?? null, status: traceObject?.status ?? null, waitingFor: traceObject?.waitingFor ?? traceObject?.lastEvent?.waitingFor ?? null, eventCount: traceObject?.eventCount ?? events.length, lastEvent: traceObject?.lastEvent ?? events.at(-1) ?? null, eventTail: tail, fullBodyAvailable: true }); } function auditTrace(traceObject: any) { const events = Array.isArray(traceObject?.events) ? traceObject.events : []; if (traceObject?.status === "missing" && events.length === 0) { return { status: "trace_missing", summary: { hasHwpod: false, hasApplyPatch: false, frictionCount: 0 }, frictionSignals: [], recommendation: "Trace is missing. Check the traceId and poll result/trace after submit." }; } const textValue = commandTextFromEvents(events); const signals = [ signal( "low_level_tran", /\/app\/tools\/(?:hwlab-gateway-)?tran\.mjs/u, textValue, "Use HWPOD commands through hwpod in code-agent runners instead of low-level transport." ), signal( "temporary_script_workaround", temporaryScriptPattern, textValue, "Add or use a named HWPOD operation instead of staging temporary scripts." ), signal( "workspace_put_text_edit", /(?:\S*workspace:\/\S*\s+put\s+(?!status\b)\S+|\bworkspace\s+put\s+(?!status\b)\S+)/u, textValue, "Workspace put is available, but source text edits should prefer apply-patch unless whole-file write is intentional." ), signal( "status_shell_parsing", statusShellParsingPattern, textValue, "Use compact JSON fields returned by harness result/trace instead of shell parsing." ) ].filter(Boolean); return { status: signals.length > 0 ? "friction_detected" : "clean", summary: { hasHwpod: /\bhwpod\b/u.test(textValue), hasApplyPatch: /\bapply-patch\b/u.test(textValue), frictionCount: signals.length }, frictionSignals: signals, recommendation: signals.length > 0 ? "Treat these as tool-friction signals and improve named HWPOD operations; this is not a runtime gate." : "No obvious HWPOD transport friction in retained trace events." }; } const temporaryScriptPattern = /(?:cat\s*>\s*\S+\.(?:py|ps1|js|mjs|bat)\b|upload\b[^\n]*\.(?:py|ps1|js|mjs|bat)\b|(?:python|powershell|pwsh)\s+\S+\.(?:py|ps1)\b)/iu; const statusShellParsingPattern = /(?:hwpod|hwlab-cli)\b[^\n]*(?:\bstatus\b|\btrace\b)[^\n]*(?:\|\s*(?:grep|head|python3?\s+-c|jq)\b|;\s*done)/iu; function commandTextFromEvents(events: any[]) { return events.map((event: any) => { if (typeof event?.command === "string") return event.command; const label = String(event?.label ?? ""); const isToolEvent = event?.type === "tool_call" || event?.stage === "tool_call" || label.includes("commandExecution"); return isToolEvent && typeof event?.message === "string" ? event.message : null; }).filter(Boolean).join("\n"); } function signal(kind: string, pattern: RegExp, textValue: string, hint: string) { const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`; const matches = [...textValue.matchAll(new RegExp(pattern.source, flags))]; return matches.length > 0 ? { kind, count: matches.length, examples: matches.slice(0, 3).map((match) => preview(match[0], 220)), hint } : null; } function compactRpcBody(body: any) { return pruneUndefined({ jsonrpc: body.jsonrpc, id: body.id, error: body.error, meta: body.meta, result: body.result && typeof body.result === "object" ? compactApiBody(body.result) : body.result, fullBodyAvailable: true }); } function compactBody(body: any) { if (!body || typeof body !== "object") return body; return { ok: body.ok, status: body.status, authenticated: body.authenticated, actor: body.actor, user: body.user, contractVersion: body.contractVersion, serviceId: body.serviceId, ready: body.ready, traceId: body.traceId, traceStatus: body.traceStatus, conversationId: body.conversationId, sessionId: body.sessionId, threadId: body.threadId, accepted: body.accepted, shortConnection: body.shortConnection, resultUrl: body.resultUrl, traceUrl: body.traceUrl, error: body.error, blocker: body.blocker, summary: body.summary, retention: body.retention, terminalEvidence: body.terminalEvidence, finalResponse: body.finalResponse, traceSummary: body.traceSummary, targetId: body.targetId, source: body.source }; } function authBodySummary(body: any) { if (!body || typeof body !== "object") return body; return { authenticated: body.authenticated, user: safeUser(body.user ?? body.actor), expiresAt: textOrNull(body.expiresAt), error: body.error }; } function safeUser(value: any) { if (!value || typeof value !== "object") return null; return clean({ id: text(value.id), username: text(value.username), role: text(value.role), displayName: text(value.displayName) }); } function withMeta(payload: any, now: () => string) { return { generatedAt: now(), cli: CLI_NAME, version: VERSION, ...payload }; } function ok(action: string, data: Record = {}, status = "succeeded") { return { ok: !["failed", "timeout"].includes(status), action, status, ...data }; } function failure(action: string, error: any) { return { ok: false, action, status: "failed", error: errorSummary(error), ...(error?.details ? { details: error.details } : {}) }; } function errorSummary(error: any) { return { code: error?.code ?? "hwlab_cli_error", message: error?.message ?? String(error), ...(error?.details ? { details: error.details } : {}) }; } function cliError(code: string, message: string, details: Record = {}) { return Object.assign(new Error(message), { code, details }); } function route(method: string, pathName: string) { return { method, path: pathName }; } function isHttpSuccess(response: any) { return response.status >= 200 && response.status < 300; } function text(value: unknown) { return String(value ?? "").trim(); } function messageTextValue(value: unknown): string { if (value === undefined || value === null) return ""; if (typeof value === "string") return value.trim(); if (typeof value === "number" || typeof value === "boolean") return String(value).trim(); if (Array.isArray(value)) return value.map(messageTextValue).filter(Boolean).join("\n").trim(); if (typeof value === "object") { const record = value as Record; for (const key of ["text", "content", "message", "prompt", "value", "summary", "preview", "title"]) { const extracted = messageTextValue(record[key]); if (extracted) return extracted; } } return ""; } function textOrNull(value: unknown) { const result = text(value); return result || null; } function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw cliError("missing_required_value", `${field} is required`, { field }); return result; } function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; } function boundedInteger(value: unknown, fallback: number, min: number, max: number) { const parsed = numberOption(value) ?? fallback; return Math.min(max, Math.max(min, parsed)); } function clean>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== false && item !== null)) as T; } function pruneUndefined>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T; } function parseJson(value: string) { if (!value) return null; try { return JSON.parse(value); } catch { return { rawText: value.slice(0, 2000), parseError: true }; } } function sha256(value: string) { return createHash("sha256").update(value).digest("hex"); } function makeId(prefix: string) { return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; } function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function requiredTraceId(value: unknown) { const result = requiredText(value, "traceId"); if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(result)) { throw cliError("invalid_trace_id", "traceId must start with trc_", { traceId: result }); } return result; } function requiredConversationId(value: unknown) { const result = requiredText(value, "conversationId"); if (!/^cnv_[A-Za-z0-9_.:-]+$/u.test(result)) { throw cliError("invalid_conversation_id", "conversationId must start with cnv_", { conversationId: result }); } return result; } function requiredSessionId(value: unknown) { const result = requiredText(value, "sessionId"); if (!/^ses_[A-Za-z0-9_.:-]+$/u.test(result)) { throw cliError("invalid_session_id", "sessionId must start with ses_", { sessionId: result }); } return result; } function safeOptionalConversationId(value: unknown) { const result = text(value); if (!result) return ""; if (!/^cnv_[A-Za-z0-9_.:-]+$/u.test(result)) throw cliError("invalid_conversation_id", "conversationId must start with cnv_", { conversationId: result }); return result; } function safeOptionalSessionId(value: unknown) { const result = text(value); if (!result) return ""; if (!/^ses_[A-Za-z0-9_.:-]+$/u.test(result)) throw cliError("invalid_session_id", "sessionId must start with ses_", { sessionId: result }); return result; } function preview(value: unknown, max = 500) { if (value === undefined || value === null) return null; const result = String(value).replace(/\s+/gu, " ").trim(); return result.length > max ? `${result.slice(0, max)}...` : result; } function shellArg(value: unknown) { const textValue = String(value ?? ""); return /^[A-Za-z0-9_./:@-]+$/u.test(textValue) ? textValue : `'${textValue.replace(/'/gu, `'"'"'`)}'`; }