import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; const VERSION = "0.2.0-client"; const CLI_NAME = "hwlab-cli"; const DEFAULT_BASE_URL = "http://74.48.78.17:19666"; const DEFAULT_TIMEOUT_MS = 30000; const DEFAULT_AGENT_TIMEOUT_MS = 120000; const DEFAULT_POLL_INTERVAL_MS = 1000; 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; }; 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 }; const payload = target === "client" ? await clientCommand({ ...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 (group === "auth") return authCommand(next); if (group === "device-pods" || group === "device-pod") return devicePodsCommand(next); if (group === "runtime") return runtimeCommand(next); if (group === "agent") return agentCommand(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 help() { return ok("help", { version: VERSION, contractVersion: "hwlab-web-equivalent-client-v1", mode: "short-connection-client", defaultBaseUrl: DEFAULT_BASE_URL, stateFile: ".state/hwlab-cli/session.json", serviceRuntime: false, imagePublished: false, jobTemplate: false, usage: [ "hwlab-cli client auth login --base-url URL --username USER --password-env HWLAB_PASSWORD", "hwlab-cli client auth session", "hwlab-cli client device-pods list", "hwlab-cli client device-pods status device-pod-71-freq", "hwlab-cli client runtime routes", "hwlab-cli client workbench summary --pod-id device-pod-71-freq", "hwlab-cli client request GET /v1/access/status [--full]", "hwlab-cli client rpc system.health [--full]", "hwlab-cli client agent send --message TEXT --provider-profile deepseek --timeout-ms 120000", "hwlab-cli client agent trace TRACE_ID", "hwlab-cli client agent cancel TRACE_ID", "hwlab-cli client harness submit --message TEXT --provider-profile deepseek", "hwlab-cli client harness wait TRACE_ID --timeout-ms 55000", "hwlab-cli client harness audit TRACE_ID --require-bootsharp" ] }); } async function authCommand(context: any) { const subcommand = context.rest[0] || "session"; if (subcommand === "login") return authLogin(context); if (subcommand === "session") { const response = await requestJson({ ...context, method: "GET", path: "/auth/session" }); return responsePayload("client.auth.session", response, context, { route: route("GET", "/auth/session") }); } if (subcommand === "logout") { const response = await requestJson({ ...context, method: "POST", path: "/auth/logout" }); 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 }); } 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: 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 devicePodsCommand(context: any) { const subcommand = context.rest[0] || "list"; if (subcommand === "list") { const response = await requestJson({ ...context, method: "GET", path: "/v1/device-pods" }); return responsePayload("client.device-pods.list", response, context, { route: route("GET", "/v1/device-pods"), body: responseBodyForCli(response.body, context.parsed) }); } const podId = requiredText(context.parsed.podId ?? context.rest[1], "podId"); if (subcommand === "status" || subcommand === "show") { const pathName = `/v1/device-pods/${encodeURIComponent(podId)}/status`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.device-pods.status", response, context, { route: route("GET", pathName), devicePodId: podId, body: responseBodyForCli(response.body, context.parsed) }); } if (subcommand === "events") { const limit = numberOption(context.parsed.limit) ?? 120; const pathName = `/v1/device-pods/${encodeURIComponent(podId)}/events?limit=${encodeURIComponent(String(limit))}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.device-pods.events", response, context, { route: route("GET", pathName), devicePodId: podId, body: responseBodyForCli(response.body, context.parsed) }); } if (subcommand === "probe") return devicePodProbe(context, podId); throw cliError("unsupported_device_pods_command", `unsupported device-pods command: ${subcommand}`, { subcommand }); } async function runtimeCommand(context: any) { const subcommand = context.rest[0] || "routes"; 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 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-device-pod", "hwlab-agent-mgr", "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 devicePodProbe(context: any, podId: string) { const encoded = encodeURIComponent(podId); const paths = [ `/v1/device-pods/${encoded}/debug-probe/chip-id`, `/v1/device-pods/${encoded}/io-probe/uart/1`, `/v1/device-pods/${encoded}/io-probe/uart/1/tail?maxBytes=${encodeURIComponent(String(numberOption(context.parsed.maxBytes) ?? 12000))}` ]; const probes = await Promise.all(paths.map(async (pathName) => { try { const response = await requestJson({ ...context, method: "GET", path: 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); return ok("client.device-pods.probe", { status: failed ? "degraded" : "succeeded", devicePodId: podId, probes }, failed ? "degraded" : "succeeded"); } async function agentCommand(context: any) { const subcommand = context.rest[0] || "send"; if (subcommand === "send") return agentSend(context); if (subcommand === "trace") { const traceId = requiredText(context.rest[1] ?? context.parsed.traceId, "traceId"); const pathName = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); return responsePayload("client.agent.trace", response, context, { route: route("GET", pathName), traceId, body: responseBodyForCli(response.body, context.parsed) }); } 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, conversationId: text(context.parsed.conversationId), sessionId: text(context.parsed.sessionId) }), 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 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 [--conversation-id ID] [--trace-id ID] [--provider-profile deepseek|codex-api]", "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] [--require-bootsharp]" ] }); } 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/chat/trace/${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/chat/trace/${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 timeoutMs = Math.min(numberOption(context.parsed.timeoutMs) ?? 55000, 60000); const pollIntervalMs = Math.max(numberOption(context.parsed.pollIntervalMs) ?? 1000, 250); 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, 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, timeoutMs, body: lastResponse ? responseBodyForCli(lastResponse.body, context.parsed) : null }, "timeout"); } 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/chat/trace/${encodeURIComponent(traceId)}` }); traceObject = response.body; } const report = auditTrace(traceObject, { requireBootsharp: context.parsed.requireBootsharp === true }); 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 = text(parsed.message ?? parsed.text) || text(context.stdinText); if (!message) throw cliError("message_required", "client agent send requires --message or stdin text"); const traceId = text(parsed.traceId) || makeId("trc"); const conversationId = text(parsed.conversationId) || makeId("cnv"); const requestBody = clean({ message, conversationId, sessionId: text(parsed.sessionId), threadId: text(parsed.threadId), traceId, providerProfile: text(parsed.providerProfile) || "deepseek", gatewayShellTimeoutMs: numberOption(parsed.gatewayShellTimeoutMs), shortConnection: true, projectId: text(parsed.projectId) || "prj_device_pod_workbench" }); 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, conversationId }); } if (parsed.noWait === true) { return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, waited: false }); } const result = await pollAgentResult(context, traceId, accepted.body); return ok("client.agent.send", { status: result.final ? "succeeded" : "timeout", route: route("POST", "/v1/agent/chat"), traceId, conversationId, accepted: compactBody(accepted.body), result: result.response ? compactResponse(result.response) : null, polls: result.polls, timeoutMs: result.timeoutMs, resultUrl: result.resultPath }, result.final ? "succeeded" : "timeout"); } 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?.resultUrl) || `/v1/agent/chat/result/${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?.status && response.body.status !== "running") { return { final: true, response, polls, timeoutMs, resultPath }; } await context.sleep(pollIntervalMs); } return { final: false, response: lastResponse, polls, timeoutMs, resultPath }; } async function workbenchCommand(context: any) { const subcommand = context.rest[0] || "summary"; if (subcommand !== "summary") throw cliError("unsupported_workbench_command", `unsupported workbench command: ${subcommand}`, { subcommand }); const podId = text(context.parsed.podId) || "device-pod-71-freq"; const encodedPodId = encodeURIComponent(podId); const paths = [ "/health/live", "/v1", "/v1/live-builds", "/v1/device-pods", `/v1/device-pods/${encodedPodId}/status`, `/v1/device-pods/${encodedPodId}/events?limit=${encodeURIComponent(String(numberOption(context.parsed.limit) ?? 120))}`, `/v1/device-pods/${encodedPodId}/debug-probe/chip-id`, `/v1/device-pods/${encodedPodId}/io-probe/uart/1`, `/v1/device-pods/${encodedPodId}/io-probe/uart/1/tail?maxBytes=${encodeURIComponent(String(numberOption(context.parsed.maxBytes) ?? 12000))}` ]; 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"].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); return ok("client.workbench.summary", { status: failed ? "degraded" : "succeeded", baseUrl: baseUrl(context.parsed, context.env), devicePodId: podId, probes }, failed ? "degraded" : "succeeded"); } async function requestCommand(context: any) { 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) }); } async function rpcCommand(context: any) { 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) }); } 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({ parsed, env, fetchImpl, cwd, method, path: pathName, body, auth = true, extraHeaders = {}, timeoutMs }: any) { const url = `${baseUrl(parsed, env)}${pathName}`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs ?? numberOption(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS); try { const cookie = auth ? await authCookie({ parsed, env, cwd: cwd ?? process.cwd() }) : null; const headers = clean({ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...(cookie ? { cookie } : {}), ...extraHeaders }); const response = await fetchImpl(url, { method, headers, body: body ? JSON.stringify(body) : undefined, signal: controller.signal }); const textBody = await response.text(); return { status: response.status, headers: response.headers, body: parseJson(textBody), url, method, path: pathName }; } finally { clearTimeout(timer); } } 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) { if (typeof parsed.passwordEnv === "string") return requiredText(env[parsed.passwordEnv], parsed.passwordEnv); if (parsed.passwordStdin === true) return stdinText !== undefined ? stdinText.trimEnd() : await readStdin(); return requiredText(parsed.password ?? env.HWLAB_PASSWORD, "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(); } function baseUrl(parsed: ParsedArgs, env: EnvLike) { return text(parsed.baseUrl ?? env.HWLAB_CLIENT_BASE_URL ?? env.HWLAB_CLOUD_WEB_URL ?? env.HWLAB_CLI_BASE_URL) || DEFAULT_BASE_URL; } async function authCookie({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) { const explicit = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE); 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; } async function loadSession({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) { try { const raw = await readFile(stateFile(parsed, cwd), "utf8"); const session = JSON.parse(raw); if (session?.baseUrl !== baseUrl(parsed, env)) return null; if (!text(session.cookie)) return null; return session; } catch { return null; } } async function saveSession(context: any, session: Record) { const file = stateFile(context.parsed, context.cwd); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(session, null, 2)}\n`, "utf8"); } async function clearSession(context: any) { await rm(stateFile(context.parsed, context.cwd), { force: true }); } function stateFile(parsed: ParsedArgs, cwd: string) { return path.resolve(cwd, text(parsed.stateFile) || ".state/hwlab-cli/session.json"); } 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 = isHttpSuccess(response) && response.body?.ok !== false; return { ok: success, action, status: success ? "succeeded" : "failed", baseUrl: baseUrl(context.parsed, context.env), httpStatus: response.status, route: extra.route ?? route(response.method, response.path), ...extra, body: extra.body ?? response.body }; } 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 responseBodyForCli(body: any, parsed: ParsedArgs) { return parsed.full === true ? body : compactApiBody(body); } 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, eventId: body.eventId, jobId: body.jobId, name: body.name, devicePodId: body.devicePodId, 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, 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, devicePods: Array.isArray(body.devicePods) ? body.devicePods.slice(0, 20).map(compactApiBody) : 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, devicePodsCount: Array.isArray(body.devicePods) ? body.devicePods.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 assistantText(body: any) { const direct = text(body?.reply?.content ?? body?.message?.content ?? body?.assistantText); 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, { requireBootsharp = false } = {}) { const events = Array.isArray(traceObject?.events) ? traceObject.events : []; if (traceObject?.status === "missing" && events.length === 0) { return { status: "trace_missing", summary: { hasHwpod: false, hasBootsharp: 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 named device-pod operations through the CLI instead of low-level transport." ), signal( "long_device_pod_cli_path", /node\s+\/app\/(?:tools\/device-pod-cli\.mjs|skills\/device-pod-cli\/scripts\/device-pod-cli\.mjs)/u, textValue, "Prefer the main hwlab-cli client entry or short documented device-pod command." ), signal( "temporary_script_workaround", temporaryScriptPattern, textValue, "Add or use a named device-pod 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." ), requireBootsharp && !/\bbootsharp\b/u.test(textValue) ? { kind: "missing_bootsharp", count: 1, examples: [], hint: "No bootsharp/bootstrap device-pod context call was found in retained trace events." } : null ].filter(Boolean); return { status: signals.length > 0 ? "friction_detected" : "clean", summary: { hasHwpod: /\bhwpod\b/u.test(textValue), hasBootsharp: /\bbootsharp\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 CLI/device-pod operations; this is not a runtime gate." : "No obvious device-pod 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 = /(?:device-pod-cli|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, selectedDevicePodId: body.selectedDevicePodId, devicePodCount: Array.isArray(body.devicePods) ? body.devicePods.length : undefined, traceId: body.traceId, 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, freshness: body.freshness, profileHash: body.profileHash, 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 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 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 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 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; }