Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/lib/components/connect-agent-flow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import LoaderCircleIcon from "@lucide/svelte/icons/loader-circle";

type Target = { id: string; name: string; slug: string; type: string; baseUrl: string | null; enabled: boolean };
type AgentType = "openclaw" | "claude-code" | "custom";
type AgentType = "openclaw" | "hermes" | "claude-code" | "custom";

let {
mode,
Expand Down Expand Up @@ -45,6 +45,7 @@ let showInlineTargetCreate = $state(false);

let agentDisplayName = $derived(
selectedAgent === "openclaw" ? "OpenClaw"
: selectedAgent === "hermes" ? "Hermes"
: selectedAgent === "claude-code" ? "Claude Code"
: selectedAgent === "custom" ? "Custom"
: ""
Expand Down Expand Up @@ -125,7 +126,7 @@ async function copyToClipboard(text: string | null) {
{#if step === 1}
<div class="space-y-6">
<h2 class="text-center font-semibold">What agent are you connecting?</h2>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
<div class="grid grid-cols-2 gap-3">
<button
class="flex flex-col items-center gap-3 rounded-lg border-2 p-6 transition-colors
{selectedAgent === 'openclaw' ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/40'}"
Expand All @@ -137,6 +138,19 @@ async function copyToClipboard(text: string | null) {
<div class="text-muted-foreground text-xs">AI assistant platform</div>
</div>
</button>

<button
class="flex flex-col items-center gap-3 rounded-lg border-2 p-6 transition-colors
{selectedAgent === 'hermes' ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/40'}"
onclick={() => (selectedAgent = "hermes")}
>
<img src="/hermes.svg" alt="Hermes" class="size-10" onerror={(e) => { const el = e.currentTarget as HTMLElement; el.style.display = 'none'; (el.nextElementSibling as HTMLElement).style.display = 'flex'; }} />
<div class="size-10 items-center justify-center rounded-full bg-violet-100 dark:bg-violet-900 text-violet-600 dark:text-violet-300 font-bold text-lg hidden">H</div>
<div class="text-center">
<div class="font-semibold">Hermes</div>
<div class="text-muted-foreground text-xs">Nous Research agent</div>
</div>
</button>
<button
class="flex flex-col items-center gap-3 rounded-lg border-2 p-6 transition-colors
{selectedAgent === 'claude-code' ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/40'}"
Expand Down Expand Up @@ -364,6 +378,29 @@ async function copyToClipboard(text: string | null) {
<li>Install the Shellgate skill</li>
</ul>
</div>
{:else if selectedAgent === "hermes"}
{@const installCmd = `curl -sX POST ${gatewayUrl}/api/install/hermes -H "Content-Type: application/json" -d '{"token":"${createdToken}"}' | bash`}
<div class="space-y-2">
<p class="text-sm font-medium">Run this in your terminal:</p>
<div class="rounded-lg bg-muted p-4 font-mono text-sm">
<div class="flex items-start justify-between gap-2">
<pre class="break-all whitespace-pre-wrap">{installCmd}</pre>
<Button variant="ghost" size="sm" class="shrink-0" onclick={() => copyToClipboard(installCmd)}>
<CopyIcon class="size-3.5" />
</Button>
</div>
</div>
</div>

<div class="rounded-lg border bg-muted/30 p-4 text-sm space-y-1">
<p class="text-muted-foreground">This will:</p>
<ul class="text-muted-foreground list-disc list-inside space-y-0.5">
<li>Verify your API key works</li>
<li>Configure Hermes environment variables</li>
<li>Install the Shellgate skill</li>
<li>Restart the Hermes gateway</li>
</ul>
</div>
{:else if selectedAgent === "openclaw"}
{@const installCmd = `curl -sX POST ${gatewayUrl}/api/install/openclaw -H "Content-Type: application/json" -d '{"token":"${createdToken}"}' | bash`}
<div class="space-y-2">
Expand Down
50 changes: 50 additions & 0 deletions src/lib/server/utils/install-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,56 @@ claude "Confirm the shellgate skill is loaded, then run: curl -s -H \\"Authoriza
`;
}

export function generateHermesScript(baseUrl: string, token: string): string {
return `#!/bin/bash
set -e

SHELLGATE_URL="${baseUrl}"
SHELLGATE_API_KEY="${token}"

echo "Verifying connection..."
VERIFY=$(curl -sf -H "Authorization: Bearer $SHELLGATE_API_KEY" "$SHELLGATE_URL/verify-connection" 2>&1) || {
echo "❌ Invalid token or Shellgate unreachable"
exit 1
}

# Add environment variables to Hermes .env
mkdir -p ~/.hermes
touch ~/.hermes/.env

if [ -f ~/.hermes/.env ]; then
sed -i.bak '/^SHELLGATE_URL=/d;/^SHELLGATE_API_KEY=/d' ~/.hermes/.env
rm -f ~/.hermes/.env.bak
fi

echo "SHELLGATE_URL=$SHELLGATE_URL" >> ~/.hermes/.env
echo "SHELLGATE_API_KEY=$SHELLGATE_API_KEY" >> ~/.hermes/.env

# Install skill
mkdir -p ~/.hermes/skills/shellgate
curl -sf -H "Authorization: Bearer $SHELLGATE_API_KEY" \\
"$SHELLGATE_URL/api/skill" > ~/.hermes/skills/shellgate/SKILL.md

# Restart gateway so the new skill is picked up
if command -v hermes &> /dev/null; then
echo "Restarting Hermes gateway..."
hermes gateway restart 2>/dev/null || true
fi

PROMPT="Use the Shellgate skill to find out which targets you have access to"
WIDTH=\$(( \${#PROMPT} + 4 ))
BORDER=\$(printf '─%.0s' \$(seq 1 \$(( WIDTH - 2 ))))

echo ""
echo "🐚 Shellgate → Hermes connected"
echo ""
echo "Try it out, ask your agent:"
echo "╭\${BORDER}╮"
echo "│ \${PROMPT} │"
echo "╰\${BORDER}╯"
`;
}

export function generateOpenClawScript(baseUrl: string, token: string): string {
return `#!/bin/bash
set -e
Expand Down
20 changes: 20 additions & 0 deletions src/routes/api/install/hermes/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getBaseUrl } from "$lib/server/utils/base-url";
import { generateHermesScript } from "$lib/server/utils/install-scripts";

export const POST: RequestHandler = async ({ request, url }) => {
const body = await request.json().catch(() => null);
const token = body?.token;

if (!token?.startsWith("sg_")) {
throw error(400, "Invalid token format");
}

const baseUrl = getBaseUrl(request, url);
const script = generateHermesScript(baseUrl, token);

return new Response(script, {
headers: { "Content-Type": "text/plain" },
});
};
Loading