opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine
hermes --tui launches the native OpenTUI engine (Bun) when HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config); Ink stays the default and the shipping path is untouched. - _resolve_tui_engine() (env > config > ink); refuses opentui on Windows/Termux (no Bun) -> falls back to ink with a notice. - _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step). - _bun_bin() with HERMES_BUN override. - Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host must not bootstrap Node). - Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun is JSC; the V8 flag errors/ignores). Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI -> real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
+31
-259
@@ -41,54 +41,11 @@ function setSessionHeader(headers: Headers, token: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Global management-profile scope ──────────────────────────────────
|
||||
// The dashboard is a machine-level management surface: one header switcher
|
||||
// (ProfileProvider in App.tsx) decides which profile the management pages
|
||||
// read/write, and fetchJSON transparently appends ?profile=<name> to the
|
||||
// profile-scoped endpoint families below. "" = the dashboard process's own
|
||||
// profile (legacy behavior). Calls that already carry an explicit profile
|
||||
// (e.g. ProfileBuilder writes) are left untouched — explicit beats global.
|
||||
let _managementProfile = "";
|
||||
|
||||
export function setManagementProfile(name: string): void {
|
||||
_managementProfile = (name || "").trim();
|
||||
}
|
||||
|
||||
export function getManagementProfile(): string {
|
||||
return _managementProfile;
|
||||
}
|
||||
|
||||
// Endpoint families that honor ?profile= on the backend (web_server.py
|
||||
// _profile_scope). Anything else — sessions, analytics, ops, pairing,
|
||||
// channels, cron (which has its own per-job profile params), profiles
|
||||
// themselves — is machine-global or self-scoped and must NOT be rewritten.
|
||||
const PROFILE_SCOPED_PREFIXES = [
|
||||
"/api/skills",
|
||||
"/api/tools/toolsets",
|
||||
"/api/config",
|
||||
"/api/env",
|
||||
"/api/mcp",
|
||||
"/api/model/info",
|
||||
"/api/model/set",
|
||||
"/api/model/auxiliary",
|
||||
"/api/model/options",
|
||||
];
|
||||
|
||||
function withManagementProfile(url: string): string {
|
||||
if (!_managementProfile) return url;
|
||||
if (url.includes("profile=")) return url; // explicit param wins
|
||||
const path = url.split("?")[0];
|
||||
if (!PROFILE_SCOPED_PREFIXES.some((p) => path.startsWith(p))) return url;
|
||||
const sep = url.includes("?") ? "&" : "?";
|
||||
return `${url}${sep}profile=${encodeURIComponent(_managementProfile)}`;
|
||||
}
|
||||
|
||||
export async function fetchJSON<T>(
|
||||
url: string,
|
||||
init?: RequestInit,
|
||||
options?: FetchJSONOptions,
|
||||
): Promise<T> {
|
||||
url = withManagementProfile(url);
|
||||
// Inject the session token into all /api/ requests.
|
||||
const headers = new Headers(init?.headers);
|
||||
const token = window.__HERMES_SESSION_TOKEN__;
|
||||
@@ -292,14 +249,6 @@ export async function buildWsUrl(
|
||||
return `${proto}//${window.location.host}${BASE}${path}?${qs}`;
|
||||
}
|
||||
|
||||
/** Build a ``?profile=<name>`` query suffix, or "" when unset.
|
||||
*
|
||||
* Used by the skills/toolsets endpoints so the dashboard can manage a
|
||||
* profile other than the one the server process runs under. */
|
||||
function profileQuery(profile?: string): string {
|
||||
return profile ? `?profile=${encodeURIComponent(profile)}` : "";
|
||||
}
|
||||
|
||||
export const api = {
|
||||
getStatus: () => fetchJSON<StatusResponse>("/api/status"),
|
||||
/**
|
||||
@@ -376,32 +325,6 @@ export const api = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ older_than_days, source }),
|
||||
}),
|
||||
listFiles: (path?: string) => {
|
||||
const query = path ? `?path=${encodeURIComponent(path)}` : "";
|
||||
return fetchJSON<ManagedFilesResponse>(`/api/files${query}`);
|
||||
},
|
||||
readFile: (path: string) =>
|
||||
fetchJSON<ManagedFileReadResponse>(
|
||||
`/api/files/read?path=${encodeURIComponent(path)}`,
|
||||
),
|
||||
uploadFile: (path: string, dataUrl: string, overwrite = true) =>
|
||||
fetchJSON<ManagedFileWriteResponse>("/api/files/upload", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path, data_url: dataUrl, overwrite }),
|
||||
}),
|
||||
createDirectory: (path: string) =>
|
||||
fetchJSON<ManagedFileWriteResponse>("/api/files/mkdir", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path }),
|
||||
}),
|
||||
deleteFile: (path: string, recursive = false) =>
|
||||
fetchJSON<{ ok: boolean; path: string }>("/api/files", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path, recursive }),
|
||||
}),
|
||||
getLogs: (params: { file?: string; lines?: number; level?: string; component?: string }) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.file) qs.set("file", params.file);
|
||||
@@ -432,7 +355,7 @@ export const api = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ config }),
|
||||
}),
|
||||
getConfigRaw: () => fetchJSON<{ yaml: string; path?: string }>("/api/config/raw"),
|
||||
getConfigRaw: () => fetchJSON<{ yaml: string }>("/api/config/raw"),
|
||||
saveConfigRaw: (yaml_text: string) =>
|
||||
fetchJSON<{ ok: boolean }>("/api/config/raw", {
|
||||
method: "PUT",
|
||||
@@ -469,7 +392,7 @@ export const api = {
|
||||
fetchJSON<CronJob[]>(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`),
|
||||
getCronDeliveryTargets: () =>
|
||||
fetchJSON<{ targets: CronDeliveryTarget[] }>("/api/cron/delivery-targets"),
|
||||
createCronJob: (job: { prompt: string; schedule: string; name?: string; deliver?: string; skills?: string[] }, profile = "default") =>
|
||||
createCronJob: (job: { prompt: string; schedule: string; name?: string; deliver?: string }, profile = "default") =>
|
||||
fetchJSON<CronJob>(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -479,7 +402,7 @@ export const api = {
|
||||
fetchJSON<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}/pause?profile=${encodeURIComponent(profile)}`, { method: "POST" }),
|
||||
updateCronJob: (
|
||||
id: string,
|
||||
updates: { prompt?: string; schedule?: string; name?: string; deliver?: string; skills?: string[] },
|
||||
updates: { prompt?: string; schedule?: string; name?: string; deliver?: string },
|
||||
profile = "default",
|
||||
) =>
|
||||
fetchJSON<CronJob>(
|
||||
@@ -497,19 +420,6 @@ export const api = {
|
||||
deleteCronJob: (id: string, profile = "default") =>
|
||||
fetchJSON<{ ok: boolean }>(`/api/cron/jobs/${encodeURIComponent(id)}?profile=${encodeURIComponent(profile)}`, { method: "DELETE" }),
|
||||
|
||||
// Automation Blueprints — parameterized automation blueprints
|
||||
getAutomationBlueprints: () =>
|
||||
fetchJSON<{ blueprints: AutomationBlueprint[] }>("/api/cron/blueprints"),
|
||||
instantiateAutomationBlueprint: (
|
||||
body: { blueprint: string; values: Record<string, string> },
|
||||
profile = "default",
|
||||
) =>
|
||||
fetchJSON<CronJob>(`/api/cron/blueprints/instantiate?profile=${encodeURIComponent(profile)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
// Profiles
|
||||
getProfiles: () =>
|
||||
fetchJSON<{ profiles: ProfileInfo[] }>("/api/profiles"),
|
||||
@@ -529,19 +439,8 @@ export const api = {
|
||||
description?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
mcp_servers?: McpServerCreate[];
|
||||
keep_skills?: string[];
|
||||
hub_skills?: string[];
|
||||
}) =>
|
||||
fetchJSON<{
|
||||
ok: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
model_set?: boolean;
|
||||
mcp_written?: number;
|
||||
skills_disabled?: number;
|
||||
hub_installs?: Array<{ identifier: string; pid: number | null }>;
|
||||
}>("/api/profiles", {
|
||||
fetchJSON<{ ok: boolean; name: string; path: string; model_set?: boolean }>("/api/profiles", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
@@ -606,74 +505,52 @@ export const api = {
|
||||
),
|
||||
|
||||
// Skills & Toolsets
|
||||
//
|
||||
// All calls accept an optional ``profile`` so the Skills page can manage
|
||||
// any profile's skills/toolsets — not just the one the dashboard process
|
||||
// runs under. Omitted/empty profile = the dashboard's own profile.
|
||||
getSkills: (profile?: string) =>
|
||||
fetchJSON<SkillInfo[]>(`/api/skills${profileQuery(profile)}`),
|
||||
toggleSkill: (name: string, enabled: boolean, profile?: string) =>
|
||||
getSkills: () => fetchJSON<SkillInfo[]>("/api/skills"),
|
||||
toggleSkill: (name: string, enabled: boolean) =>
|
||||
fetchJSON<{ ok: boolean }>("/api/skills/toggle", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, enabled, profile: profile || undefined }),
|
||||
body: JSON.stringify({ name, enabled }),
|
||||
}),
|
||||
getSkillContent: (name: string, profile?: string) =>
|
||||
fetchJSON<SkillContent>(
|
||||
`/api/skills/content?name=${encodeURIComponent(name)}${profile ? `&profile=${encodeURIComponent(profile)}` : ""}`,
|
||||
),
|
||||
createSkill: (skill: { name: string; content: string; category?: string }, profile?: string) =>
|
||||
fetchJSON<SkillWriteResult>("/api/skills", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...skill, profile: profile || undefined }),
|
||||
}),
|
||||
updateSkillContent: (name: string, content: string, profile?: string) =>
|
||||
fetchJSON<SkillWriteResult>("/api/skills/content", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, content, profile: profile || undefined }),
|
||||
}),
|
||||
getToolsets: (profile?: string) =>
|
||||
fetchJSON<ToolsetInfo[]>(`/api/tools/toolsets${profileQuery(profile)}`),
|
||||
toggleToolset: (name: string, enabled: boolean, profile?: string) =>
|
||||
getToolsets: () => fetchJSON<ToolsetInfo[]>("/api/tools/toolsets"),
|
||||
toggleToolset: (name: string, enabled: boolean) =>
|
||||
fetchJSON<{ ok: boolean; name: string; enabled: boolean }>(
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled, profile: profile || undefined }),
|
||||
body: JSON.stringify({ enabled }),
|
||||
},
|
||||
),
|
||||
getToolsetConfig: (name: string, profile?: string) =>
|
||||
getToolsetConfig: (name: string) =>
|
||||
fetchJSON<ToolsetConfig>(
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}/config${profileQuery(profile)}`,
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}/config`,
|
||||
),
|
||||
selectToolsetProvider: (name: string, provider: string, profile?: string) =>
|
||||
selectToolsetProvider: (name: string, provider: string) =>
|
||||
fetchJSON<{ ok: boolean; name: string; provider: string }>(
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}/provider`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ provider, profile: profile || undefined }),
|
||||
body: JSON.stringify({ provider }),
|
||||
},
|
||||
),
|
||||
saveToolsetEnv: (name: string, env: Record<string, string>, profile?: string) =>
|
||||
saveToolsetEnv: (name: string, env: Record<string, string>) =>
|
||||
fetchJSON<ToolsetEnvResult>(
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}/env`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ env, profile: profile || undefined }),
|
||||
body: JSON.stringify({ env }),
|
||||
},
|
||||
),
|
||||
runToolsetPostSetup: (name: string, key: string, profile?: string) =>
|
||||
runToolsetPostSetup: (name: string, key: string) =>
|
||||
fetchJSON<ActionResponse & { key: string }>(
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}/post-setup`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, profile: profile || undefined }),
|
||||
body: JSON.stringify({ key }),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -938,8 +815,6 @@ export const api = {
|
||||
|
||||
// ── Admin: Webhooks ─────────────────────────────────────────────────
|
||||
getWebhooks: () => fetchJSON<WebhooksResponse>("/api/webhooks"),
|
||||
enableWebhooks: () =>
|
||||
fetchJSON<WebhookEnableResponse>("/api/webhooks/enable", { method: "POST" }),
|
||||
createWebhook: (body: WebhookCreate) =>
|
||||
fetchJSON<WebhookRoute & { secret: string }>("/api/webhooks", {
|
||||
method: "POST",
|
||||
@@ -1014,11 +889,11 @@ export const api = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ output }),
|
||||
}),
|
||||
runImport: (archive: string, force = false) =>
|
||||
runImport: (archive: string) =>
|
||||
fetchJSON<ActionResponse>("/api/ops/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ archive, force }),
|
||||
body: JSON.stringify({ archive }),
|
||||
}),
|
||||
getHooks: () => fetchJSON<HooksResponse>("/api/ops/hooks"),
|
||||
createHook: (body: HookCreate) =>
|
||||
@@ -1074,34 +949,26 @@ export const api = {
|
||||
fetchJSON<ActionResponse>("/api/ops/checkpoints/prune", { method: "POST" }),
|
||||
|
||||
// ── Admin: Skills hub ───────────────────────────────────────────────
|
||||
// ``profile`` scopes install/uninstall/update and the installed-state
|
||||
// annotations to that profile (omitted = the dashboard's own profile).
|
||||
installSkillFromHub: (identifier: string, profile?: string) =>
|
||||
installSkillFromHub: (identifier: string) =>
|
||||
fetchJSON<ActionResponse>("/api/skills/hub/install", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identifier, profile: profile || undefined }),
|
||||
body: JSON.stringify({ identifier }),
|
||||
}),
|
||||
uninstallSkillFromHub: (name: string, profile?: string) =>
|
||||
uninstallSkillFromHub: (name: string) =>
|
||||
fetchJSON<ActionResponse>("/api/skills/hub/uninstall", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, profile: profile || undefined }),
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
updateSkillsFromHub: (profile?: string) =>
|
||||
fetchJSON<ActionResponse>("/api/skills/hub/update", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: profile || undefined }),
|
||||
}),
|
||||
searchSkillsHub: (q: string, source = "all", limit = 20, profile?: string) =>
|
||||
updateSkillsFromHub: () =>
|
||||
fetchJSON<ActionResponse>("/api/skills/hub/update", { method: "POST" }),
|
||||
searchSkillsHub: (q: string, source = "all", limit = 20) =>
|
||||
fetchJSON<SkillHubSearchResponse>(
|
||||
`/api/skills/hub/search?q=${encodeURIComponent(q)}&source=${encodeURIComponent(source)}&limit=${limit}${profile ? `&profile=${encodeURIComponent(profile)}` : ""}`,
|
||||
),
|
||||
getSkillHubSources: (profile?: string) =>
|
||||
fetchJSON<SkillHubSourcesResponse>(
|
||||
`/api/skills/hub/sources${profileQuery(profile)}`,
|
||||
`/api/skills/hub/search?q=${encodeURIComponent(q)}&source=${encodeURIComponent(source)}&limit=${limit}`,
|
||||
),
|
||||
getSkillHubSources: () =>
|
||||
fetchJSON<SkillHubSourcesResponse>("/api/skills/hub/sources"),
|
||||
previewSkillFromHub: (identifier: string) =>
|
||||
fetchJSON<SkillHubPreview>(
|
||||
`/api/skills/hub/preview?identifier=${encodeURIComponent(identifier)}`,
|
||||
@@ -1362,17 +1229,6 @@ export interface WebhooksResponse {
|
||||
subscriptions: WebhookRoute[];
|
||||
}
|
||||
|
||||
export interface WebhookEnableResponse {
|
||||
ok: boolean;
|
||||
platform: "webhook";
|
||||
enabled: true;
|
||||
needs_restart: boolean;
|
||||
restart_started?: boolean;
|
||||
restart_action?: string;
|
||||
restart_pid?: number | null;
|
||||
restart_error?: string;
|
||||
}
|
||||
|
||||
export interface WebhookCreate {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -1619,11 +1475,7 @@ export interface TelegramOnboardingApplyResponse {
|
||||
ok: boolean;
|
||||
platform: "telegram";
|
||||
bot_username?: string;
|
||||
needs_restart: boolean;
|
||||
restart_started?: boolean;
|
||||
restart_action?: string;
|
||||
restart_pid?: number | null;
|
||||
restart_error?: string;
|
||||
needs_restart: true;
|
||||
}
|
||||
|
||||
export interface SessionMessage {
|
||||
@@ -1648,44 +1500,6 @@ export interface LogsResponse {
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
export interface ManagedFileEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
is_directory: boolean;
|
||||
size: number | null;
|
||||
mtime: number;
|
||||
mime_type: string | null;
|
||||
}
|
||||
|
||||
export interface ManagedFilesResponse {
|
||||
root: string | null;
|
||||
path: string;
|
||||
parent: string | null;
|
||||
locked_root: string | null;
|
||||
can_change_path: boolean;
|
||||
entries: ManagedFileEntry[];
|
||||
}
|
||||
|
||||
export interface ManagedFileReadResponse {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
mime_type: string;
|
||||
data_url: string;
|
||||
root: string | null;
|
||||
locked_root: string | null;
|
||||
can_change_path: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFileWriteResponse {
|
||||
ok: boolean;
|
||||
path: string;
|
||||
entry: ManagedFileEntry;
|
||||
root: string | null;
|
||||
locked_root: string | null;
|
||||
can_change_path: boolean;
|
||||
}
|
||||
|
||||
export interface AnalyticsDailyEntry {
|
||||
day: string;
|
||||
input_tokens: number;
|
||||
@@ -1820,7 +1634,6 @@ export interface CronJob {
|
||||
name?: string | null;
|
||||
prompt?: string | null;
|
||||
script?: string | null;
|
||||
skills?: string[] | null;
|
||||
schedule?: { kind?: string; expr?: string; display?: string };
|
||||
schedule_display?: string | null;
|
||||
enabled: boolean;
|
||||
@@ -1838,29 +1651,6 @@ export interface CronDeliveryTarget {
|
||||
home_env_var: string | null;
|
||||
}
|
||||
|
||||
export interface AutomationBlueprintField {
|
||||
name: string;
|
||||
type: "time" | "enum" | "text" | "weekdays";
|
||||
label: string;
|
||||
default: string | null;
|
||||
options: string[];
|
||||
optional: boolean;
|
||||
/** When false, options are suggestions — any value is accepted. */
|
||||
strict?: boolean;
|
||||
help: string;
|
||||
}
|
||||
|
||||
export interface AutomationBlueprint {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
fields: AutomationBlueprintField[];
|
||||
command: string;
|
||||
appUrl: string;
|
||||
}
|
||||
|
||||
export interface SkillInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -1868,19 +1658,6 @@ export interface SkillInfo {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SkillContent {
|
||||
name: string;
|
||||
content: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface SkillWriteResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ToolsetInfo {
|
||||
name: string;
|
||||
label: string;
|
||||
@@ -1986,12 +1763,9 @@ export interface AuxiliaryModelsResponse {
|
||||
}
|
||||
|
||||
export interface ModelAssignmentRequest {
|
||||
confirm_expensive_model?: boolean;
|
||||
scope: "main" | "auxiliary";
|
||||
provider: string;
|
||||
model: string;
|
||||
/** Optional OpenAI-compatible endpoint URL for custom/local main providers. */
|
||||
base_url?: string;
|
||||
/** For auxiliary: task slot name, "" for all, "__reset__" to reset all. */
|
||||
task?: string;
|
||||
}
|
||||
@@ -2005,8 +1779,6 @@ export interface StaleAuxAssignment {
|
||||
}
|
||||
|
||||
export interface ModelAssignmentResponse {
|
||||
confirm_message?: string;
|
||||
confirm_required?: boolean;
|
||||
ok: boolean;
|
||||
scope?: string;
|
||||
provider?: string;
|
||||
|
||||
Reference in New Issue
Block a user