From 4332ca22eeeae09eb21456307a92662aed7a0fc0 Mon Sep 17 00:00:00 2001
From: gsknnft
Date: Wed, 1 Apr 2026 01:24:23 -0400
Subject: [PATCH] fixed - onboarding and hermes calls
---
server/hermes-gateway-adapter.js | 167 +++++++++++++++++-
.../components/CompanyBuilderModal.tsx | 22 ++-
src/features/company-builder/planning.ts | 2 +-
src/features/office/screens/OfficeScreen.tsx | 8 +-
.../onboarding/components/CompanyStep.tsx | 6 +-
.../onboarding/components/CompleteStep.tsx | 2 +-
.../onboarding/components/WelcomeStep.tsx | 4 +-
src/features/onboarding/types.ts | 2 +-
8 files changed, 189 insertions(+), 24 deletions(-)
diff --git a/server/hermes-gateway-adapter.js b/server/hermes-gateway-adapter.js
index 4d9fa3c..ddd1226 100644
--- a/server/hermes-gateway-adapter.js
+++ b/server/hermes-gateway-adapter.js
@@ -285,6 +285,141 @@ function hermesPost(path, body) {
});
}
+function hermesGet(path) {
+ return new Promise((resolve, reject) => {
+ const urlStr = HERMES_API_URL + path;
+ let url;
+ try { url = new URL(urlStr); } catch { reject(new Error(`Invalid URL: ${urlStr}`)); return; }
+ const transport = url.protocol === "https:" ? https : http;
+ const headers = {};
+ if (HERMES_API_KEY) headers["Authorization"] = `Bearer ${HERMES_API_KEY}`;
+ const req = transport.request(
+ { hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : (url.protocol === "https:" ? 443 : 80),
+ path: url.pathname + (url.search || ""), method: "GET", headers },
+ resolve
+ );
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+async function readJsonBody(res) {
+ const chunks = [];
+ for await (const chunk of res) chunks.push(Buffer.from(chunk));
+ const raw = Buffer.concat(chunks).toString("utf8");
+ if (!raw.trim()) return {};
+ return JSON.parse(raw);
+}
+
+function extractOpenAiStyleError(payload, fallbackMessage) {
+ if (payload && typeof payload === "object") {
+ const message =
+ typeof payload?.error?.message === "string"
+ ? payload.error.message.trim()
+ : "";
+ if (message) return message;
+ }
+ return fallbackMessage;
+}
+
+let cachedHermesModels = null;
+let cachedHermesModelsAt = 0;
+
+async function fetchHermesModels() {
+ const now = Date.now();
+ if (cachedHermesModels && now - cachedHermesModelsAt < 30_000) {
+ return cachedHermesModels;
+ }
+ const res = await hermesGet("/v1/models");
+ if (res.statusCode >= 400) {
+ res.resume();
+ throw new Error(`Hermes models API HTTP ${res.statusCode}`);
+ }
+ const payload = await readJsonBody(res);
+ const models = Array.isArray(payload?.data)
+ ? payload.data
+ .map((entry) => (typeof entry?.id === "string" ? entry.id.trim() : ""))
+ .filter(Boolean)
+ : [];
+ cachedHermesModels = models;
+ cachedHermesModelsAt = now;
+ return models;
+}
+
+async function resolveHermesModel(requestedModel) {
+ const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
+ const normalized = trimmed.includes("/") ? trimmed.split("/").pop().trim() : trimmed;
+ try {
+ const models = await fetchHermesModels();
+ if (models.length === 0) {
+ return normalized || trimmed || HERMES_MODEL;
+ }
+ const candidates = [trimmed, normalized, HERMES_MODEL]
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
+ .filter(Boolean);
+ for (const candidate of candidates) {
+ const exact = models.find((modelId) => modelId === candidate);
+ if (exact) return exact;
+ }
+ for (const candidate of candidates) {
+ const suffix = models.find((modelId) => modelId.endsWith(`/${candidate}`));
+ if (suffix) return suffix;
+ }
+ return models[0];
+ } catch {
+ return normalized || trimmed || HERMES_MODEL;
+ }
+}
+
+async function completeOneTurn(messages, model, tools) {
+ const resolvedModel = await resolveHermesModel(model);
+ const body = { model: resolvedModel, messages, stream: false };
+ if (tools && tools.length > 0) {
+ body.tools = tools;
+ body.tool_choice = "auto";
+ }
+ const res = await hermesPost("/v1/chat/completions", body);
+ const payload = await readJsonBody(res);
+ if (res.statusCode >= 400) {
+ throw new Error(
+ extractOpenAiStyleError(payload, `Hermes API HTTP ${res.statusCode}`)
+ );
+ }
+ const choice = Array.isArray(payload?.choices) ? payload.choices[0] : null;
+ const message = choice?.message || {};
+ const textContent =
+ typeof message?.content === "string"
+ ? message.content
+ : Array.isArray(message?.content)
+ ? message.content
+ .map((part) => (typeof part?.text === "string" ? part.text : ""))
+ .join("")
+ : "";
+ const finishReason =
+ typeof choice?.finish_reason === "string" && choice.finish_reason
+ ? choice.finish_reason
+ : "stop";
+ const toolCalls = Array.isArray(message?.tool_calls)
+ ? message.tool_calls.map((tc) => {
+ let args = {};
+ const rawArgs = tc?.function?.arguments;
+ if (typeof rawArgs === "string" && rawArgs.trim()) {
+ try {
+ args = JSON.parse(rawArgs);
+ } catch {
+ args = { _raw: rawArgs };
+ }
+ }
+ return {
+ id: typeof tc?.id === "string" ? tc.id : randomId(),
+ name: typeof tc?.function?.name === "string" ? tc.function.name : "",
+ args,
+ };
+ })
+ : [];
+ return { textContent, toolCalls, finishReason, resolvedModel };
+}
+
// ---------------------------------------------------------------------------
// SSE streaming — handles both text deltas and tool calls
// ---------------------------------------------------------------------------
@@ -297,6 +432,8 @@ async function streamOneTurn(messages, model, tools, onTextDelta, abortCheck) {
const body = { model, messages, stream: true };
if (tools && tools.length > 0) { body.tools = tools; body.tool_choice = "auto"; }
+ const resolvedModel = await resolveHermesModel(model);
+ body.model = resolvedModel;
const res = await hermesPost("/v1/chat/completions", body);
if (res.statusCode >= 400) {
res.resume();
@@ -355,6 +492,15 @@ async function streamOneTurn(messages, model, tools, onTextDelta, abortCheck) {
return { id: tc.id, name: tc.name, args };
});
+ if (!textContent.trim() && toolCalls.length === 0 && finishReason === "stop") {
+ const fallback = await completeOneTurn(messages, resolvedModel, tools);
+ return {
+ textContent: fallback.textContent,
+ toolCalls: fallback.toolCalls,
+ finishReason: fallback.finishReason,
+ };
+ }
+
return { textContent, toolCalls, finishReason };
}
@@ -743,14 +889,15 @@ async function handleMethod(method, params, id, sendEvent) {
const key = typeof p.key === "string" ? p.key : MAIN_SESSION_KEY;
const current = sessionSettings.get(key) || {};
const next = { ...current };
- if (p.model !== undefined) next.model = p.model;
+ if (p.model !== undefined) next.model = typeof p.model === "string" ? p.model.trim() : p.model;
if (p.thinkingLevel !== undefined) next.thinkingLevel = p.thinkingLevel;
if (p.execHost !== undefined) next.execHost = p.execHost;
if (p.execSecurity !== undefined) next.execSecurity = p.execSecurity;
if (p.execAsk !== undefined) next.execAsk = p.execAsk;
sessionSettings.set(key, next);
+ const resolvedModel = await resolveHermesModel(next.model || HERMES_MODEL);
return resOk(id, { ok: true, key, entry: { thinkingLevel: next.thinkingLevel },
- resolved: { model: next.model || HERMES_MODEL, modelProvider: "hermes" } });
+ resolved: { model: resolvedModel, modelProvider: "hermes" } });
}
case "sessions.reset": {
@@ -896,7 +1043,20 @@ async function handleMethod(method, params, id, sendEvent) {
return resOk(id, { skills: [] });
case "models.list":
- return resOk(id, { models: [{ id: HERMES_MODEL, name: HERMES_AGENT_NAME }] });
+ try {
+ const models = await fetchHermesModels();
+ return resOk(id, {
+ models: (models.length > 0 ? models : [HERMES_MODEL]).map((modelId) => ({
+ id: modelId,
+ name: modelId,
+ })),
+ });
+ } catch {
+ return resOk(id, { models: [{ id: HERMES_MODEL, name: HERMES_MODEL }] });
+ }
+
+ case "tasks.list":
+ return resOk(id, { tasks: [] });
// --- Cron jobs ----------------------------------------------------------
@@ -1019,6 +1179,7 @@ function startAdapter() {
"agents.files.get","agents.files.set",
"exec.approvals.get","exec.approvals.set","exec.approval.resolve",
"wake","skills.status","models.list",
+ "tasks.list",
"cron.list","cron.add","cron.remove","cron.patch","cron.run"],
events: ["chat","presence","heartbeat","cron"] },
snapshot: { health: { agents: allAgents, defaultAgentId: AGENT_ID },
diff --git a/src/features/company-builder/components/CompanyBuilderModal.tsx b/src/features/company-builder/components/CompanyBuilderModal.tsx
index 142b695..d582093 100644
--- a/src/features/company-builder/components/CompanyBuilderModal.tsx
+++ b/src/features/company-builder/components/CompanyBuilderModal.tsx
@@ -194,7 +194,7 @@ export function CompanyBuilderModal({
Design an AI company from one prompt
- Uses your connected OpenClaw runtime
+ Uses your connected runtime
{plannerAgentName ? ` via ${plannerAgentName}.` : "."}
@@ -329,7 +329,7 @@ export function CompanyBuilderModal({
Company Actions
- Generate the org, then create it in OpenClaw.
+ Generate the org, then create it in your connected runtime.
{replacesExistingAgents ? (
@@ -341,7 +341,7 @@ export function CompanyBuilderModal({
) : null}
{!canUseAi ? (
- Connect to OpenClaw and keep at least one available planning agent in the fleet
+ Connect to a runtime and keep at least one available planning agent in the fleet
to use AI suggestions.
) : null}
@@ -416,7 +416,7 @@ export function CompanyBuilderModal({
Org structure
- Edit the team before creating agents in OpenClaw.
+ Edit the team before creating agents in your connected runtime.
- Claw3D is using your OpenClaw runtime right now. Please wait until this finishes.
+ Claw3D is using your connected runtime right now. Please wait until this finishes.
{Array.from({ length: 4 }, (_, index) => (
@@ -897,7 +897,8 @@ export function CompanyBuilderModal({
What should the company do?
- As soon as you submit this, OpenClaw will improve the brief automatically.
+ As soon as you submit this, Claw3D will improve the brief using your connected
+ runtime.
-
- The improved brief becomes the main editable input for generation.
-
+
+
+ The improved brief becomes the main editable input for generation.
+
+ {error ?
{error}
: null}
+
[
"You are helping a user describe the company they want to build inside Claw3D.",
- "Rewrite their brief so another OpenClaw agent can generate a clean org structure from it.",
+ "Rewrite their brief so another connected runtime agent can generate a clean org structure from it.",
"Keep the answer short, concrete, and useful.",
"Return markdown with these sections only:",
"## Company",
diff --git a/src/features/office/screens/OfficeScreen.tsx b/src/features/office/screens/OfficeScreen.tsx
index 73a8b05..6bb2ca3 100644
--- a/src/features/office/screens/OfficeScreen.tsx
+++ b/src/features/office/screens/OfficeScreen.tsx
@@ -1580,7 +1580,7 @@ export function OfficeScreen({
const runCompanyBuilderAiTask = useCallback(
async (prompt: string, statusText: string) => {
if (status !== "connected") {
- throw new Error("Connect to OpenClaw before using the company builder.");
+ throw new Error("Connect to a runtime before using the company builder.");
}
const livePlannerAgent = resolveCompanyPlanningAgent({
agents: stateRef.current.agents,
@@ -1608,7 +1608,7 @@ export function OfficeScreen({
try {
const improvedBrief = await runCompanyBuilderAiTask(
buildImproveCompanyBriefPrompt(brief),
- "Improving your company brief with OpenClaw.",
+ "Improving your company brief with the connected runtime.",
);
setCompanyBuilderInput((current) => ({
...current,
@@ -1635,7 +1635,7 @@ export function OfficeScreen({
try {
const response = await runCompanyBuilderAiTask(
buildGenerateCompanyPlanPrompt(brief),
- "Generating your AI company structure with OpenClaw.",
+ "Generating your AI company structure with the connected runtime.",
);
const parsedPlan = parseCompanyPlanFromAssistantText(response);
const nextInput: CompanyBuilderInput = {
@@ -1678,7 +1678,7 @@ export function OfficeScreen({
const handleCreateCompanyFromPlan = useCallback(
async (params: { input: CompanyBuilderInput; plan: CompanyBuilderPlan }) => {
if (status !== "connected") {
- const message = "Connect to OpenClaw before creating the company.";
+ const message = "Connect to a runtime before creating the company.";
setCompanyBuilderError(message);
throw new Error(message);
}
diff --git a/src/features/onboarding/components/CompanyStep.tsx b/src/features/onboarding/components/CompanyStep.tsx
index a155d08..b4eadf7 100644
--- a/src/features/onboarding/components/CompanyStep.tsx
+++ b/src/features/onboarding/components/CompanyStep.tsx
@@ -35,7 +35,7 @@ export const CompanyStep = ({
{
icon: Sparkles,
title: "Improve the brief",
- description: "Use your connected OpenClaw runtime to sharpen the company prompt.",
+ description: "Use your connected runtime to sharpen the company prompt.",
},
{
icon: Users,
@@ -45,7 +45,7 @@ export const CompanyStep = ({
{
icon: Wand2,
title: "Create everything",
- description: "Write agent files and create the team directly in OpenClaw.",
+ description: "Write agent files and create the team directly in the connected runtime.",
},
].map(({ icon: Icon, title, description }) => (
) : (
- Connect to OpenClaw and keep at least one planning agent available to generate the
+ Connect to a runtime and keep at least one planning agent available to generate the
company with AI.
)}
diff --git a/src/features/onboarding/components/CompleteStep.tsx b/src/features/onboarding/components/CompleteStep.tsx
index 2f9ff5a..a795c95 100644
--- a/src/features/onboarding/components/CompleteStep.tsx
+++ b/src/features/onboarding/components/CompleteStep.tsx
@@ -62,7 +62,7 @@ export const CompleteStep = ({
{companyCreated
- ? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in OpenClaw and placed into the office.`
+ ? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in the connected runtime and placed into the office.`
: "Your gateway is connected and your agents are ready. Step inside and explore the 3D workspace where your AI team operates."}
diff --git a/src/features/onboarding/components/WelcomeStep.tsx b/src/features/onboarding/components/WelcomeStep.tsx
index d2e6fe5..8294c55 100644
--- a/src/features/onboarding/components/WelcomeStep.tsx
+++ b/src/features/onboarding/components/WelcomeStep.tsx
@@ -32,11 +32,11 @@ export const WelcomeStep = () => (
Claw3D turns your AI automation into a{" "}
visual workplace — an
- office where your OpenClaw agents collaborate, code, test, and execute
+ office where your AI agents collaborate, code, test, and execute
tasks in a shared 3D environment.
- This wizard will help you connect to your OpenClaw gateway and get
+ This wizard will help you connect to your runtime gateway and get
started in about two minutes.
diff --git a/src/features/onboarding/types.ts b/src/features/onboarding/types.ts
index afeac63..73e4a5f 100644
--- a/src/features/onboarding/types.ts
+++ b/src/features/onboarding/types.ts
@@ -49,7 +49,7 @@ export const ONBOARDING_STEPS: OnboardingStep[] = [
{
id: "connect",
title: "Connect Your Gateway",
- description: "Link to your OpenClaw instance",
+ description: "Link to your runtime instance",
skippable: false,
},
{