feat: add Hermes integration panel to Settings → Integrations
Server-side: - New hermes-integration module (runtime.js + routes.js + tests) exposing GET /api/openchamber/hermes/status - Reports availability of agent-activity, session-steer, and plan-gate runtimes plus plan-gate default config - Wired into feature-routes-runtime.js alongside existing integrations UI: - New HermesIntegration.tsx collapsible panel in Integrations page - Connection status indicator (green/red dot) with 30s polling - Plan gate default toggle (persists via updateDesktopSettings + recordDeferredOpenCodeRestart) - Read-only status rows for activity stream, steer channel, plan gate - Server version and uptime display Settings: - hermesPlanGateDefault field in settings registry and useUIStore - Search index entry with keywords: hermes, agent, integration, etc. - i18n keys for all 12 locales (English text as fallback) Verification: - Server tests: 7/7 passed (runtime + routes) - UI tests: 4/4 passed (HermesIntegration.test.tsx) - Typecheck: clean (no new errors) - oxlint: clean on all new/modified files
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n";
|
||||
import { useUIStore } from "@/stores/useUIStore";
|
||||
|
||||
import { HermesIntegration } from "./HermesIntegration";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
const mockStatus = {
|
||||
ok: true,
|
||||
server: { version: "1.23.0", uptimeMs: 5000 },
|
||||
integrations: {
|
||||
agentActivity: { available: true },
|
||||
steer: { available: true },
|
||||
planGate: { available: true },
|
||||
},
|
||||
config: {
|
||||
planGateDefault: false,
|
||||
activityRateLimitMs: 2000,
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () =>
|
||||
renderToStaticMarkup(
|
||||
<I18nProvider>
|
||||
<HermesIntegration />
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
describe("HermesIntegration", () => {
|
||||
beforeEach(() => {
|
||||
// SAFETY: mock() returns a callable mock; the test harness only needs a fetch-like function.
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve(new Response(JSON.stringify(mockStatus), { status: 200 }))
|
||||
) as typeof fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
useUIStore.setState({ hermesPlanGateDefault: false });
|
||||
});
|
||||
|
||||
test("renders the Hermes title and description", () => {
|
||||
const markup = renderComponent();
|
||||
expect(markup).toContain("Hermes");
|
||||
expect(markup).toContain("Agent-to-agent integration");
|
||||
});
|
||||
|
||||
test("renders the collapsible trigger with status indicator", () => {
|
||||
const markup = renderComponent();
|
||||
expect(markup).toContain("integrations.hermes");
|
||||
expect(markup).toContain("Checking…");
|
||||
});
|
||||
|
||||
test("does not render collapsed content in SSR", () => {
|
||||
const markup = renderComponent();
|
||||
expect(markup).not.toContain("Activity stream");
|
||||
expect(markup).not.toContain("Require plan approval by default");
|
||||
});
|
||||
|
||||
test("toggling plan gate default updates the store", () => {
|
||||
const state = useUIStore.getState();
|
||||
expect(state.hermesPlanGateDefault).toBe(false);
|
||||
|
||||
state.setHermesPlanGateDefault(true);
|
||||
expect(useUIStore.getState().hermesPlanGateDefault).toBe(true);
|
||||
|
||||
state.setHermesPlanGateDefault(false);
|
||||
expect(useUIStore.getState().hermesPlanGateDefault).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import React from 'react';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
SettingsCheckboxRow,
|
||||
SETTINGS_OPTION_STACK_CLASS,
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
import { recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
|
||||
const STATUS_POLL_INTERVAL_MS = 30_000;
|
||||
|
||||
type HermesStatus = {
|
||||
ok: boolean;
|
||||
server: { version: string; uptimeMs: number };
|
||||
integrations: {
|
||||
agentActivity: { available: boolean };
|
||||
steer: { available: boolean };
|
||||
planGate: { available: boolean };
|
||||
};
|
||||
config: {
|
||||
planGateDefault: boolean;
|
||||
activityRateLimitMs: number;
|
||||
};
|
||||
} | null;
|
||||
|
||||
function StatusDot({ available }: { available: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block size-2 rounded-full',
|
||||
available
|
||||
? 'bg-[var(--status-success)]'
|
||||
: 'bg-[var(--status-error)]',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusRow({
|
||||
label,
|
||||
available,
|
||||
detail,
|
||||
}: {
|
||||
label: string;
|
||||
available: boolean;
|
||||
detail?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-0.5">
|
||||
<StatusDot available={available} />
|
||||
<span className="typography-settings-field-label text-foreground">{label}</span>
|
||||
{detail ? (
|
||||
<span className="typography-meta text-muted-foreground">{detail}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const HermesIntegration: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [status, setStatus] = React.useState<HermesStatus>(null);
|
||||
const [fetchError, setFetchError] = React.useState(false);
|
||||
|
||||
const hermesPlanGateDefault = useUIStore((state) => state.hermesPlanGateDefault);
|
||||
const setHermesPlanGateDefault = useUIStore((state) => state.setHermesPlanGateDefault);
|
||||
|
||||
const fetchStatus = React.useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/openchamber/hermes/status');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data: HermesStatus = await response.json();
|
||||
setStatus(data);
|
||||
setFetchError(false);
|
||||
} catch {
|
||||
setStatus(null);
|
||||
setFetchError(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
void fetchStatus();
|
||||
const timer = window.setInterval(fetchStatus, STATUS_POLL_INTERVAL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [fetchStatus]);
|
||||
|
||||
const handlePlanGateDefaultChange = React.useCallback((enabled: boolean) => {
|
||||
setHermesPlanGateDefault(enabled);
|
||||
void updateDesktopSettings({ hermesPlanGateDefault: enabled });
|
||||
recordDeferredOpenCodeRestart('cli', { id: 'hermes-plan-gate' });
|
||||
}, [setHermesPlanGateDefault]);
|
||||
|
||||
const connected = status?.ok === true && !fetchError;
|
||||
const statusLabel = fetchError
|
||||
? t('settings.integrations.hermes.status.unreachable')
|
||||
: connected
|
||||
? t('settings.integrations.hermes.status.connected')
|
||||
: t('settings.integrations.hermes.status.checking');
|
||||
const statusClassName = fetchError
|
||||
? 'bg-[var(--status-error)]/15 text-[var(--status-error)]'
|
||||
: connected
|
||||
? 'bg-[var(--status-success)]/15 text-[var(--status-success)]'
|
||||
: 'bg-[var(--surface-muted)] text-muted-foreground';
|
||||
|
||||
const activityRateLimitMs = status?.config?.activityRateLimitMs ?? 2000;
|
||||
const activityRateLabel = `${Math.round(activityRateLimitMs / 1000)}s`;
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div
|
||||
data-settings-item="integrations.hermes"
|
||||
className="overflow-hidden rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)]"
|
||||
>
|
||||
<CollapsibleTrigger
|
||||
className="flex w-full min-w-0 items-center gap-3 px-4 py-3 text-left hover:bg-[var(--interactive-hover)]/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--interactive-focus-ring)]"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-[var(--surface-muted)]">
|
||||
<Icon name="robot" className="size-5 text-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-foreground">
|
||||
{t('settings.integrations.hermes.title')}
|
||||
</div>
|
||||
<p className="mt-0.5 line-clamp-1 text-xs leading-snug text-muted-foreground">
|
||||
{t('settings.integrations.hermes.description')}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
'max-w-36 shrink-0 truncate rounded-full px-2 py-0.5 text-[10px] font-medium',
|
||||
statusClassName,
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
<Icon
|
||||
name="arrow-down-s"
|
||||
className={cn(
|
||||
'size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out motion-reduce:transition-none',
|
||||
open && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-t border-[var(--interactive-border)] px-4 py-4">
|
||||
<div className="space-y-4">
|
||||
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="integrations.hermes.plan-gate-default"
|
||||
checked={hermesPlanGateDefault}
|
||||
onChange={handlePlanGateDefaultChange}
|
||||
label={t('settings.integrations.hermes.field.planGateDefault')}
|
||||
ariaLabel={t('settings.integrations.hermes.field.planGateDefaultAria')}
|
||||
info={t('settings.integrations.hermes.field.planGateDefaultInfo')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="typography-meta font-medium text-foreground">
|
||||
{t('settings.integrations.hermes.section.status')}
|
||||
</p>
|
||||
<StatusRow
|
||||
label={t('settings.integrations.hermes.field.activityStream')}
|
||||
available={status?.integrations?.agentActivity?.available ?? false}
|
||||
detail={`${t('settings.integrations.hermes.field.activityStreamDetail')} · ${activityRateLabel}`}
|
||||
/>
|
||||
<StatusRow
|
||||
label={t('settings.integrations.hermes.field.steerChannel')}
|
||||
available={status?.integrations?.steer?.available ?? false}
|
||||
/>
|
||||
<StatusRow
|
||||
label={t('settings.integrations.hermes.field.planGate')}
|
||||
available={status?.integrations?.planGate?.available ?? false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status?.server ? (
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
v{status.server.version} · {t('settings.integrations.hermes.field.uptime')}{' '}
|
||||
{Math.round(status.server.uptimeMs / 1000)}s
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { GitHubIntegration } from './GitHubIntegration';
|
||||
import { LinearSettings } from './LinearSettings';
|
||||
import { HermesIntegration } from './HermesIntegration';
|
||||
|
||||
export const IntegrationsPage: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
@@ -29,6 +30,7 @@ export const IntegrationsPage: React.FC = () => {
|
||||
>
|
||||
{hasGitHub ? <GitHubIntegration /> : null}
|
||||
{hasLinear ? <LinearSettings /> : null}
|
||||
<HermesIntegration />
|
||||
</SettingsSection>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
|
||||
@@ -2367,5 +2367,5 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompts',
|
||||
...linearIntegrationI18n.en,
|
||||
'settings.page.integrations.title': 'Integrations',
|
||||
'settings.page.integrations.description': 'Connect GitHub and Linear so OpenChamber can work with your issues and pull requests.',
|
||||
'settings.page.integrations.description': 'Connect GitHub, Linear, and Hermes so OpenChamber can work with your issues, pull requests, and agent integrations.',
|
||||
} as const;
|
||||
|
||||
@@ -45,6 +45,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Adds a comment to the issue when a session starts, finishes, or fails. Comments are only posted when this server has a public address, so the link opens the session for everyone on the issue.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Post session status comments to Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Could not load Linear comment settings.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue Review',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue Review',
|
||||
@@ -95,6 +109,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Kommentiert das Issue, wenn eine Sitzung startet, endet oder fehlschlägt. Kommentare werden nur gepostet, wenn dieser Server eine öffentliche Adresse hat, damit der Link die Sitzung für alle Beteiligten öffnet.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Statuskommentare zu Sitzungen in Linear posten',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Linear-Kommentareinstellungen konnten nicht geladen werden.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue-Review',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue-Review',
|
||||
@@ -145,6 +173,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Ajoute un commentaire au ticket quand une session démarre, se termine ou échoue. Les commentaires ne sont publiés que si ce serveur a une adresse publique, afin que le lien ouvre la session pour tout le monde.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Publier les commentaires d’état de session dans Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Impossible de charger les réglages de commentaires Linear.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Revue d’issue',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Revue d’issue',
|
||||
@@ -195,6 +237,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Añade un comentario a la incidencia cuando una sesión empieza, termina o falla. Los comentarios solo se publican si este servidor tiene una dirección pública, para que el enlace abra la sesión a todos.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Publicar comentarios de estado de sesión en Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'No se pudieron cargar los ajustes de comentarios de Linear.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Revisión de issue',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Revisión de issue',
|
||||
@@ -245,6 +301,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'セッションの開始・完了・失敗時にイシューへコメントします。リンクを誰でも開けるよう、このサーバーが公開アドレスを持つ場合のみ投稿します。',
|
||||
'settings.integrations.linear.sessionComments.aria': 'セッション状態のコメントを Linear に投稿',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Linear のコメント設定を読み込めませんでした。',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue レビュー',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue レビュー',
|
||||
@@ -295,6 +365,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': '세션이 시작, 완료, 실패할 때 이슈에 댓글을 남깁니다. 링크를 모두가 열 수 있도록 이 서버에 공개 주소가 있을 때만 게시합니다.',
|
||||
'settings.integrations.linear.sessionComments.aria': '세션 상태 댓글을 Linear에 게시',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Linear 댓글 설정을 불러오지 못했습니다.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': '이슈 리뷰',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': '이슈 리뷰',
|
||||
@@ -345,6 +429,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Dodaje komentarz do zgłoszenia, gdy sesja się zaczyna, kończy lub kończy błędem. Komentarze pojawiają się tylko wtedy, gdy ten serwer ma publiczny adres, żeby link otwierał sesję każdemu.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Publikuj komentarze o stanie sesji w Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Nie udało się wczytać ustawień komentarzy Linear.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Przegląd zgłoszenia',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Przegląd zgłoszenia',
|
||||
@@ -395,6 +493,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Comenta na issue quando uma sessão começa, termina ou falha. Os comentários só são publicados se este servidor tiver um endereço público, para que o link abra a sessão para todos.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Publicar comentários de status de sessão no Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Não foi possível carregar as configurações de comentários do Linear.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Revisão de issue',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Revisão de issue',
|
||||
@@ -445,6 +557,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Додає коментар до тікета, коли сесія починається, завершується або падає. Коментарі публікуються, лише якщо цей сервер має публічну адресу, щоб посилання відкривало сесію для всіх.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Публікувати коментарі про стан сесії в Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Не вдалося завантажити налаштування коментарів Linear.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Огляд issue',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Огляд issue',
|
||||
@@ -495,6 +621,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': '会话开始、完成或失败时在议题下留言。仅当此服务器拥有公网地址时才发布,这样链接才能让所有人打开该会话。',
|
||||
'settings.integrations.linear.sessionComments.aria': '将会话状态评论发布到 Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': '无法加载 Linear 评论设置。',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue 审查',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue 审查',
|
||||
@@ -545,6 +685,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': '工作階段開始、完成或失敗時在議題留言。僅在這台伺服器有公開位址時才發布,這樣連結才能讓所有人開啟該工作階段。',
|
||||
'settings.integrations.linear.sessionComments.aria': '將工作階段狀態留言發布到 Linear',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': '無法載入 Linear 留言設定。',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue 審查',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue 審查',
|
||||
@@ -595,6 +749,20 @@ export const linearIntegrationI18n = {
|
||||
'settings.integrations.linear.sessionComments.info': 'Bir oturum başladığında, bittiğinde veya başarısız olduğunda göreve yorum ekler. Bağlantının herkeste açılabilmesi için yorumlar yalnızca bu sunucunun genel bir adresi varsa gönderilir.',
|
||||
'settings.integrations.linear.sessionComments.aria': 'Oturum durumu yorumlarını Linear’a gönder',
|
||||
'settings.integrations.linear.sessionComments.loadFailed': 'Linear yorum ayarları yüklenemedi.',
|
||||
'settings.integrations.hermes.title': 'Hermes',
|
||||
'settings.integrations.hermes.description': 'Agent-to-agent integration for activity streaming, session steering, and plan gates.',
|
||||
'settings.integrations.hermes.status.connected': 'Connected',
|
||||
'settings.integrations.hermes.status.unreachable': 'Unreachable',
|
||||
'settings.integrations.hermes.status.checking': 'Checking…',
|
||||
'settings.integrations.hermes.field.planGateDefault': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultAria': 'Require plan approval by default',
|
||||
'settings.integrations.hermes.field.planGateDefaultInfo': 'When enabled, new agent sessions wait for plan approval before making changes. Requires a server restart.',
|
||||
'settings.integrations.hermes.section.status': 'Integration status',
|
||||
'settings.integrations.hermes.field.activityStream': 'Activity stream',
|
||||
'settings.integrations.hermes.field.activityStreamDetail': 'Rate limited per session',
|
||||
'settings.integrations.hermes.field.steerChannel': 'Steer channel',
|
||||
'settings.integrations.hermes.field.planGate': 'Plan gate',
|
||||
'settings.integrations.hermes.field.uptime': 'Server uptime',
|
||||
'settings.magicPrompts.sidebar.group.linear': 'Linear',
|
||||
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue incelemesi',
|
||||
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue incelemesi',
|
||||
|
||||
@@ -256,6 +256,7 @@ export const SETTINGS_REGISTRY = {
|
||||
agentControlToolEnabled: field({ scope: 'instance', parse: parseBoolean, ui: uiStore('agentControlToolEnabled', (v) => useUIStore.getState().setAgentControlToolEnabled(v)) }),
|
||||
agentWebToolEnabled: field({ scope: 'instance', parse: parseBoolean, ui: uiStore('agentWebToolEnabled', (v) => useUIStore.getState().setAgentWebToolEnabled(v)) }),
|
||||
agentMemoryToolEnabled: field({ scope: 'instance', parse: parseBoolean, ui: uiStore('agentMemoryToolEnabled', (v) => useUIStore.getState().setAgentMemoryToolEnabled(v)) }),
|
||||
hermesPlanGateDefault: field({ scope: 'instance', parse: parseBoolean, ui: uiStore('hermesPlanGateDefault', (v) => useUIStore.getState().setHermesPlanGateDefault(v)) }),
|
||||
// Server-owned: it says whether this build has the feature at all.
|
||||
agentMemoryFeatureAvailable: field({
|
||||
scope: 'instance',
|
||||
|
||||
@@ -1086,6 +1086,14 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
||||
keywords: ['linear', 'project', 'team', 'map', 'workspace', 'directory'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'integrations.hermes',
|
||||
page: 'integrations',
|
||||
titleKey: 'settings.integrations.hermes.title',
|
||||
descriptionKey: 'settings.integrations.hermes.description',
|
||||
keywords: ['hermes', 'agent', 'integration', 'plan gate', 'steer', 'activity'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface BuildSettingsSearchResultsOptions {
|
||||
|
||||
@@ -937,6 +937,7 @@ interface UIStore {
|
||||
agentControlToolEnabled: boolean;
|
||||
agentWebToolEnabled: boolean;
|
||||
agentMemoryToolEnabled: boolean;
|
||||
hermesPlanGateDefault: boolean;
|
||||
/**
|
||||
* Whether this build has agent memory at all. Server-owned and not
|
||||
* persisted: an unreleased feature must not come back from a stale cache.
|
||||
@@ -1135,6 +1136,7 @@ interface UIStore {
|
||||
setAgentControlToolEnabled: (value: boolean) => void;
|
||||
setAgentWebToolEnabled: (value: boolean) => void;
|
||||
setAgentMemoryToolEnabled: (value: boolean) => void;
|
||||
setHermesPlanGateDefault: (value: boolean) => void;
|
||||
setAgentMemoryFeatureAvailable: (value: boolean) => void;
|
||||
markAgentMemoryViewed: (key: string, viewedAt: number) => void;
|
||||
setProjectContextSidebarWidth: (width: number) => void;
|
||||
@@ -1308,6 +1310,7 @@ export const useUIStore = create<UIStore>()(
|
||||
agentControlToolEnabled: true,
|
||||
agentWebToolEnabled: true,
|
||||
agentMemoryToolEnabled: false,
|
||||
hermesPlanGateDefault: false,
|
||||
agentMemoryFeatureAvailable: false,
|
||||
agentMemoryViewedAt: {},
|
||||
projectContextSidebarWidth: 168,
|
||||
@@ -2575,6 +2578,9 @@ export const useUIStore = create<UIStore>()(
|
||||
setAgentMemoryToolEnabled: (value) => {
|
||||
set({ agentMemoryToolEnabled: value });
|
||||
},
|
||||
setHermesPlanGateDefault: (value) => {
|
||||
set({ hermesPlanGateDefault: value });
|
||||
},
|
||||
setAgentMemoryFeatureAvailable: (value) => {
|
||||
set({ agentMemoryFeatureAvailable: value });
|
||||
},
|
||||
|
||||
@@ -95,6 +95,7 @@ import { createMessageQueueRuntime } from './lib/message-queue/runtime.js';
|
||||
import { createAgentActivityRuntime, registerAgentActivityRoutes } from './lib/agent-activity/runtime.js';
|
||||
import { createSessionSteerRuntime, registerSessionSteerRoutes } from './lib/session-steer/runtime.js';
|
||||
import { createPlanGateRuntime, registerPlanGateRoutes } from './lib/plan-gate/runtime.js';
|
||||
import { createHermesIntegrationRuntime } from './lib/hermes-integration/runtime.js';
|
||||
import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
|
||||
import { createProjectConfigRuntime } from './lib/projects/project-config.js';
|
||||
import { migrateLegacyUserDirs } from './lib/data-dir-migration.js';
|
||||
@@ -951,6 +952,14 @@ const planGateRuntime = createPlanGateRuntime({
|
||||
});
|
||||
planGateRuntime.start();
|
||||
|
||||
const hermesIntegrationRuntime = createHermesIntegrationRuntime({
|
||||
agentActivityRuntime,
|
||||
sessionSteerRuntime,
|
||||
planGateRuntime,
|
||||
readSettingsFromDiskMigrated,
|
||||
startedAt: () => Date.now() - (Date.now() - process.uptime() * 1000),
|
||||
});
|
||||
|
||||
const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
|
||||
waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
|
||||
buildOpenCodeUrl,
|
||||
@@ -2013,6 +2022,7 @@ async function main(options = {}) {
|
||||
agentActivityRuntime,
|
||||
sessionSteerRuntime,
|
||||
planGateRuntime,
|
||||
hermesIntegrationRuntime,
|
||||
});
|
||||
|
||||
const startupPipelineResult = await startupPipelineRuntime.run({
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import express from 'express';
|
||||
|
||||
export function registerHermesIntegrationRoutes(app, hermesIntegrationRuntime) {
|
||||
app.get('/api/openchamber/hermes/status', async (req, res) => {
|
||||
try {
|
||||
const status = await hermesIntegrationRuntime.getStatus();
|
||||
return res.json(status);
|
||||
} catch (error) {
|
||||
const statusCode = Number.isFinite(error?.status) ? error.status : 500;
|
||||
return res.status(statusCode).json({
|
||||
ok: false,
|
||||
error: error?.message ?? 'Failed to get Hermes integration status',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { registerHermesIntegrationRoutes } from './routes.js';
|
||||
|
||||
const createApp = (getStatus) => {
|
||||
const app = express();
|
||||
registerHermesIntegrationRoutes(app, { getStatus });
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Hermes integration route', () => {
|
||||
it('returns status JSON from the runtime', async () => {
|
||||
const getStatus = vi.fn(async () => ({
|
||||
ok: true,
|
||||
server: { version: '1.23.0', uptimeMs: 5000 },
|
||||
integrations: {
|
||||
agentActivity: { available: true },
|
||||
steer: { available: true },
|
||||
planGate: { available: true },
|
||||
},
|
||||
config: {
|
||||
planGateDefault: false,
|
||||
activityRateLimitMs: 2000,
|
||||
},
|
||||
}));
|
||||
const response = await request(createApp(getStatus))
|
||||
.get('/api/openchamber/hermes/status')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expect(response.body.server.version).toBe('1.23.0');
|
||||
expect(response.body.integrations.agentActivity.available).toBe(true);
|
||||
expect(getStatus).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns 500 on runtime failure', async () => {
|
||||
const getStatus = vi.fn(async () => {
|
||||
throw new Error('runtime exploded');
|
||||
});
|
||||
const response = await request(createApp(getStatus))
|
||||
.get('/api/openchamber/hermes/status')
|
||||
.expect(500);
|
||||
|
||||
expect(response.body.ok).toBe(false);
|
||||
expect(response.body.error).toBe('runtime exploded');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
// Hermes integration status: reports the availability and configuration of the
|
||||
// three agent-to-agent runtimes (activity stream, steer channel, plan gate) that
|
||||
// power the Hermes ↔ OpenChamber link. Pure read-only — does not modify any
|
||||
// existing runtime.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const PACKAGE_VERSION = (() => {
|
||||
try {
|
||||
const packagePath = path.resolve(__dirname, '..', '..', 'package.json');
|
||||
const raw = fs.readFileSync(packagePath, 'utf8');
|
||||
const pkg = JSON.parse(raw);
|
||||
if (pkg && typeof pkg.version === 'string' && pkg.version.trim().length > 0) {
|
||||
return pkg.version.trim();
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return 'unknown';
|
||||
})();
|
||||
|
||||
export function createHermesIntegrationRuntime({
|
||||
agentActivityRuntime,
|
||||
sessionSteerRuntime,
|
||||
planGateRuntime,
|
||||
readSettingsFromDiskMigrated,
|
||||
startedAt = Date.now,
|
||||
}) {
|
||||
const getStatus = async () => {
|
||||
const settings = await readSettingsFromDiskMigrated().catch(() => null);
|
||||
const uptimeMs = Date.now() - startedAt();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
server: {
|
||||
version: PACKAGE_VERSION,
|
||||
uptimeMs,
|
||||
},
|
||||
integrations: {
|
||||
agentActivity: {
|
||||
available: agentActivityRuntime != null,
|
||||
},
|
||||
steer: {
|
||||
available: sessionSteerRuntime != null,
|
||||
},
|
||||
planGate: {
|
||||
available: planGateRuntime != null,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
planGateDefault: Boolean(settings?.hermesPlanGateDefault),
|
||||
activityRateLimitMs: 2000,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return { getStatus };
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createHermesIntegrationRuntime } from './runtime.js';
|
||||
|
||||
const createRuntime = (overrides = {}) => {
|
||||
return createHermesIntegrationRuntime({
|
||||
agentActivityRuntime: { processPayload: vi.fn() },
|
||||
sessionSteerRuntime: { steer: vi.fn() },
|
||||
planGateRuntime: { approvePlan: vi.fn() },
|
||||
readSettingsFromDiskMigrated: vi.fn(async () => ({
|
||||
hermesPlanGateDefault: false,
|
||||
})),
|
||||
startedAt: () => Date.now() - 10_000,
|
||||
...overrides,
|
||||
});
|
||||
};
|
||||
|
||||
describe('Hermes integration runtime', () => {
|
||||
it('reports all three integrations as available', async () => {
|
||||
const runtime = createRuntime();
|
||||
const status = await runtime.getStatus();
|
||||
|
||||
expect(status.ok).toBe(true);
|
||||
expect(status.server.version).toBeDefined();
|
||||
expect(status.integrations.agentActivity.available).toBe(true);
|
||||
expect(status.integrations.steer.available).toBe(true);
|
||||
expect(status.integrations.planGate.available).toBe(true);
|
||||
});
|
||||
|
||||
it('reports availability as false when runtimes are null', async () => {
|
||||
const runtime = createRuntime({
|
||||
agentActivityRuntime: null,
|
||||
sessionSteerRuntime: null,
|
||||
planGateRuntime: null,
|
||||
});
|
||||
const status = await runtime.getStatus();
|
||||
|
||||
expect(status.integrations.agentActivity.available).toBe(false);
|
||||
expect(status.integrations.steer.available).toBe(false);
|
||||
expect(status.integrations.planGate.available).toBe(false);
|
||||
});
|
||||
|
||||
it('includes uptime and config', async () => {
|
||||
const runtime = createRuntime();
|
||||
const status = await runtime.getStatus();
|
||||
|
||||
expect(typeof status.server.uptimeMs).toBe('number');
|
||||
expect(status.server.uptimeMs).toBeGreaterThanOrEqual(0);
|
||||
expect(status.config.planGateDefault).toBe(false);
|
||||
expect(status.config.activityRateLimitMs).toBe(2000);
|
||||
});
|
||||
|
||||
it('reads planGateDefault from settings', async () => {
|
||||
const readSettingsFromDiskMigrated = vi.fn(async () => ({
|
||||
hermesPlanGateDefault: true,
|
||||
}));
|
||||
const runtime = createRuntime({ readSettingsFromDiskMigrated });
|
||||
const status = await runtime.getStatus();
|
||||
|
||||
expect(status.config.planGateDefault).toBe(true);
|
||||
});
|
||||
|
||||
it('handles settings read failure gracefully', async () => {
|
||||
const readSettingsFromDiskMigrated = vi.fn(async () => {
|
||||
throw new Error('settings read failed');
|
||||
});
|
||||
const runtime = createRuntime({ readSettingsFromDiskMigrated });
|
||||
const status = await runtime.getStatus();
|
||||
|
||||
expect(status.ok).toBe(true);
|
||||
expect(status.config.planGateDefault).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ import { registerMarkdownImageGrantRoutes } from '../markdown-image-grants/route
|
||||
import { registerAgentActivityRoutes } from '../agent-activity/runtime.js';
|
||||
import { registerSessionSteerRoutes } from '../session-steer/runtime.js';
|
||||
import { registerPlanGateRoutes } from '../plan-gate/runtime.js';
|
||||
import { registerHermesIntegrationRoutes } from '../hermes-integration/routes.js';
|
||||
import { registerSkillRoutes } from './skill-routes.js';
|
||||
import { registerPluginRoutes } from './plugin-routes.js';
|
||||
import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
|
||||
@@ -145,6 +146,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
agentActivityRuntime,
|
||||
sessionSteerRuntime,
|
||||
planGateRuntime,
|
||||
hermesIntegrationRuntime,
|
||||
} = routeDependencies;
|
||||
|
||||
registerSettingsUtilityRoutes(app, {
|
||||
@@ -216,6 +218,10 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
registerSessionSteerRoutes(app, sessionSteerRuntime);
|
||||
registerPlanGateRoutes(app, planGateRuntime);
|
||||
|
||||
if (hermesIntegrationRuntime) {
|
||||
registerHermesIntegrationRoutes(app, hermesIntegrationRuntime);
|
||||
}
|
||||
|
||||
registerMarkdownImageGrantRoutes(app, {
|
||||
fsPromises,
|
||||
path,
|
||||
|
||||
Reference in New Issue
Block a user