@castlekit/castle 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +38 -1
  2. package/bin/castle.js +94 -0
  3. package/install.sh +722 -0
  4. package/next.config.ts +7 -0
  5. package/package.json +54 -5
  6. package/postcss.config.mjs +7 -0
  7. package/src/app/api/avatars/[id]/route.ts +75 -0
  8. package/src/app/api/openclaw/agents/route.ts +107 -0
  9. package/src/app/api/openclaw/config/route.ts +94 -0
  10. package/src/app/api/openclaw/events/route.ts +96 -0
  11. package/src/app/api/openclaw/logs/route.ts +59 -0
  12. package/src/app/api/openclaw/ping/route.ts +68 -0
  13. package/src/app/api/openclaw/restart/route.ts +65 -0
  14. package/src/app/api/openclaw/sessions/route.ts +62 -0
  15. package/src/app/globals.css +286 -0
  16. package/src/app/icon.png +0 -0
  17. package/src/app/layout.tsx +42 -0
  18. package/src/app/page.tsx +269 -0
  19. package/src/app/ui-kit/page.tsx +684 -0
  20. package/src/cli/onboarding.ts +576 -0
  21. package/src/components/dashboard/agent-status.tsx +107 -0
  22. package/src/components/dashboard/glass-card.tsx +28 -0
  23. package/src/components/dashboard/goal-widget.tsx +174 -0
  24. package/src/components/dashboard/greeting-widget.tsx +78 -0
  25. package/src/components/dashboard/index.ts +7 -0
  26. package/src/components/dashboard/stat-widget.tsx +61 -0
  27. package/src/components/dashboard/stock-widget.tsx +164 -0
  28. package/src/components/dashboard/weather-widget.tsx +68 -0
  29. package/src/components/icons/castle-icon.tsx +21 -0
  30. package/src/components/kanban/index.ts +3 -0
  31. package/src/components/kanban/kanban-board.tsx +391 -0
  32. package/src/components/kanban/kanban-card.tsx +137 -0
  33. package/src/components/kanban/kanban-column.tsx +98 -0
  34. package/src/components/layout/index.ts +4 -0
  35. package/src/components/layout/page-header.tsx +20 -0
  36. package/src/components/layout/sidebar.tsx +128 -0
  37. package/src/components/layout/theme-toggle.tsx +59 -0
  38. package/src/components/layout/user-menu.tsx +72 -0
  39. package/src/components/ui/alert.tsx +72 -0
  40. package/src/components/ui/avatar.tsx +87 -0
  41. package/src/components/ui/badge.tsx +39 -0
  42. package/src/components/ui/button.tsx +43 -0
  43. package/src/components/ui/card.tsx +107 -0
  44. package/src/components/ui/checkbox.tsx +56 -0
  45. package/src/components/ui/clock.tsx +171 -0
  46. package/src/components/ui/dialog.tsx +105 -0
  47. package/src/components/ui/index.ts +34 -0
  48. package/src/components/ui/input.tsx +112 -0
  49. package/src/components/ui/option-card.tsx +151 -0
  50. package/src/components/ui/progress.tsx +103 -0
  51. package/src/components/ui/radio.tsx +109 -0
  52. package/src/components/ui/select.tsx +46 -0
  53. package/src/components/ui/slider.tsx +62 -0
  54. package/src/components/ui/tabs.tsx +132 -0
  55. package/src/components/ui/toggle-group.tsx +85 -0
  56. package/src/components/ui/toggle.tsx +78 -0
  57. package/src/components/ui/tooltip.tsx +145 -0
  58. package/src/components/ui/uptime.tsx +106 -0
  59. package/src/lib/config.ts +195 -0
  60. package/src/lib/gateway-connection.ts +391 -0
  61. package/src/lib/hooks/use-openclaw.ts +163 -0
  62. package/src/lib/utils.ts +6 -0
  63. package/tsconfig.json +34 -0
package/next.config.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ devIndicators: false,
5
+ };
6
+
7
+ export default nextConfig;
package/package.json CHANGED
@@ -1,17 +1,66 @@
1
1
  {
2
2
  "name": "@castlekit/castle",
3
- "version": "0.0.1",
4
- "description": "The extensible kit for AI agents",
5
- "main": "index.js",
3
+ "version": "0.1.0",
4
+ "description": "The multi-agent workspace",
5
+ "type": "module",
6
+ "bin": {
7
+ "castle": "./bin/castle.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "next dev -p 3333",
11
+ "build": "next build",
12
+ "start": "next start -p 3333",
13
+ "lint": "next lint"
14
+ },
6
15
  "repository": {
7
16
  "type": "git",
8
17
  "url": "git+https://github.com/castlekit/castle.git"
9
18
  },
10
- "keywords": ["ai", "agents", "openclaw", "multi-agent"],
19
+ "keywords": [
20
+ "ai",
21
+ "agents",
22
+ "openclaw",
23
+ "multi-agent",
24
+ "local-first"
25
+ ],
11
26
  "author": "Castle Kit Inc.",
12
27
  "license": "MIT",
13
28
  "bugs": {
14
29
  "url": "https://github.com/castlekit/castle/issues"
15
30
  },
16
- "homepage": "https://github.com/castlekit/castle#readme"
31
+ "homepage": "https://castlekit.com",
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "dependencies": {
36
+ "@clack/prompts": "^1.0.0",
37
+ "@dnd-kit/core": "^6.3.1",
38
+ "@dnd-kit/sortable": "^10.0.0",
39
+ "@dnd-kit/utilities": "^3.2.2",
40
+ "@radix-ui/react-slider": "^1.3.6",
41
+ "@tailwindcss/postcss": "^4.1.18",
42
+ "@types/node": "^25.2.1",
43
+ "@types/react": "^19.2.13",
44
+ "@types/react-dom": "^19.2.3",
45
+ "clsx": "^2.1.1",
46
+ "commander": "^14.0.3",
47
+ "json5": "^2.2.3",
48
+ "lucide-react": "^0.563.0",
49
+ "next": "^16.1.6",
50
+ "next-themes": "^0.4.6",
51
+ "open": "^11.0.0",
52
+ "picocolors": "^1.1.1",
53
+ "react": "^19.2.4",
54
+ "react-dom": "^19.2.4",
55
+ "recharts": "^3.7.0",
56
+ "swr": "^2.4.0",
57
+ "tailwind-merge": "^3.4.0",
58
+ "tailwindcss": "^4.1.18",
59
+ "tsx": "^4.21.0",
60
+ "typescript": "^5.9.3",
61
+ "ws": "^8.19.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/ws": "^8.18.1"
65
+ }
17
66
  }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,75 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ const AVATAR_DIRS = [
9
+ join(homedir(), ".castle", "avatars"),
10
+ join(homedir(), ".openclaw", "avatars"),
11
+ ];
12
+
13
+ const MIME_TYPES: Record<string, string> = {
14
+ ".png": "image/png",
15
+ ".jpg": "image/jpeg",
16
+ ".jpeg": "image/jpeg",
17
+ ".gif": "image/gif",
18
+ ".webp": "image/webp",
19
+ ".svg": "image/svg+xml",
20
+ };
21
+
22
+ /**
23
+ * GET /api/avatars/[id]
24
+ * Serves avatar images from ~/.castle/avatars/ or ~/.openclaw/avatars/
25
+ * Supports IDs with or without extension (tries .png, .jpg, .webp)
26
+ */
27
+ export async function GET(
28
+ _request: NextRequest,
29
+ { params }: { params: Promise<{ id: string }> }
30
+ ) {
31
+ const { id } = await params;
32
+
33
+ // Sanitize -- prevent path traversal
34
+ const safeId = id.replace(/[^a-zA-Z0-9._-]/g, "");
35
+ if (!safeId || safeId.includes("..")) {
36
+ return new NextResponse("Not found", { status: 404 });
37
+ }
38
+
39
+ // Try each avatar directory
40
+ for (const dir of AVATAR_DIRS) {
41
+ if (!existsSync(dir)) continue;
42
+
43
+ // Try exact filename first (if it has an extension)
44
+ const hasExtension = /\.\w+$/.test(safeId);
45
+ if (hasExtension) {
46
+ const filePath = join(dir, safeId);
47
+ if (existsSync(filePath)) {
48
+ return serveFile(filePath);
49
+ }
50
+ }
51
+
52
+ // Try common extensions
53
+ for (const ext of [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"]) {
54
+ const filePath = join(dir, `${safeId}${ext}`);
55
+ if (existsSync(filePath)) {
56
+ return serveFile(filePath);
57
+ }
58
+ }
59
+ }
60
+
61
+ return new NextResponse("Not found", { status: 404 });
62
+ }
63
+
64
+ function serveFile(filePath: string): NextResponse {
65
+ const data = readFileSync(filePath);
66
+ const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
67
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
68
+
69
+ return new NextResponse(data, {
70
+ headers: {
71
+ "Content-Type": contentType,
72
+ "Cache-Control": "public, max-age=86400, immutable",
73
+ },
74
+ });
75
+ }
@@ -0,0 +1,107 @@
1
+ import { NextResponse } from "next/server";
2
+ import { ensureGateway } from "@/lib/gateway-connection";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ interface AgentIdentity {
7
+ name?: string;
8
+ theme?: string;
9
+ emoji?: string;
10
+ avatar?: string;
11
+ avatarUrl?: string;
12
+ }
13
+
14
+ interface GatewayAgent {
15
+ id: string;
16
+ name?: string;
17
+ identity?: AgentIdentity;
18
+ }
19
+
20
+ interface AgentsListPayload {
21
+ defaultId?: string;
22
+ mainKey?: string;
23
+ scope?: string;
24
+ agents: GatewayAgent[];
25
+ }
26
+
27
+ /**
28
+ * Rewrite avatar URLs to local /api/avatars/ endpoint.
29
+ * Handles URLs like "http://localhost:8787/api/v1/avatars/HASH" -> "/api/avatars/HASH"
30
+ */
31
+ function rewriteAvatarUrl(url: string | null): string | null {
32
+ if (!url) return null;
33
+
34
+ // Extract hash from known avatar URL patterns
35
+ const patterns = [
36
+ /\/api\/v\d+\/avatars\/([a-f0-9]+)/i,
37
+ /\/avatars\/([a-f0-9]+)/i,
38
+ ];
39
+
40
+ for (const pattern of patterns) {
41
+ const match = url.match(pattern);
42
+ if (match) {
43
+ return `/api/avatars/${match[1]}`;
44
+ }
45
+ }
46
+
47
+ // If it's already a relative path or data URI, pass through
48
+ if (url.startsWith("/") || url.startsWith("data:")) {
49
+ return url;
50
+ }
51
+
52
+ // Only allow http/https URLs through; reject file:, javascript:, etc.
53
+ if (url.startsWith("http://") || url.startsWith("https://")) {
54
+ return url;
55
+ }
56
+
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * GET /api/openclaw/agents
62
+ * Discover agents from OpenClaw Gateway via agents.list
63
+ */
64
+ export async function GET() {
65
+ const gw = ensureGateway();
66
+
67
+ if (!gw.isConnected) {
68
+ return NextResponse.json(
69
+ { error: "Gateway not connected", state: gw.state, agents: [] },
70
+ { status: 503 }
71
+ );
72
+ }
73
+
74
+ try {
75
+ const result = await gw.request<AgentsListPayload>("agents.list", {});
76
+
77
+ const agents = (result?.agents || []).map((agent: GatewayAgent) => {
78
+ const name =
79
+ agent.identity?.name || agent.name || agent.id;
80
+ const rawAvatar =
81
+ agent.identity?.avatarUrl || agent.identity?.avatar || null;
82
+ const emoji = agent.identity?.emoji || null;
83
+ const description = agent.identity?.theme || null;
84
+
85
+ // Rewrite external avatar URLs to our local /api/avatars/ endpoint
86
+ const avatar = rewriteAvatarUrl(rawAvatar);
87
+
88
+ return {
89
+ id: agent.id,
90
+ name,
91
+ description,
92
+ avatar,
93
+ emoji,
94
+ };
95
+ });
96
+
97
+ return NextResponse.json({
98
+ agents,
99
+ defaultId: result?.defaultId,
100
+ });
101
+ } catch (err) {
102
+ return NextResponse.json(
103
+ { error: err instanceof Error ? err.message : "Failed to list agents", agents: [] },
104
+ { status: 500 }
105
+ );
106
+ }
107
+ }
@@ -0,0 +1,94 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { readFileSync, writeFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import JSON5 from "json5";
5
+ import { getOpenClawDir } from "@/lib/config";
6
+ import { ensureGateway } from "@/lib/gateway-connection";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ /**
11
+ * GET /api/openclaw/config
12
+ * God mode: reads OpenClaw config from filesystem
13
+ */
14
+ export async function GET() {
15
+ const configPath = join(getOpenClawDir(), "openclaw.json");
16
+
17
+ if (!existsSync(configPath)) {
18
+ // Try json5
19
+ const json5Path = join(getOpenClawDir(), "openclaw.json5");
20
+ if (!existsSync(json5Path)) {
21
+ return NextResponse.json(
22
+ { error: "OpenClaw config not found" },
23
+ { status: 404 }
24
+ );
25
+ }
26
+
27
+ try {
28
+ const raw = readFileSync(json5Path, "utf-8");
29
+ const config = JSON5.parse(raw);
30
+ return NextResponse.json({ config, format: "json5" });
31
+ } catch (err) {
32
+ return NextResponse.json(
33
+ { error: err instanceof Error ? err.message : "Failed to parse config" },
34
+ { status: 500 }
35
+ );
36
+ }
37
+ }
38
+
39
+ try {
40
+ const raw = readFileSync(configPath, "utf-8");
41
+ const config = JSON5.parse(raw);
42
+ return NextResponse.json({ config, format: "json" });
43
+ } catch (err) {
44
+ return NextResponse.json(
45
+ { error: err instanceof Error ? err.message : "Failed to parse config" },
46
+ { status: 500 }
47
+ );
48
+ }
49
+ }
50
+
51
+ /**
52
+ * PATCH /api/openclaw/config
53
+ * Update OpenClaw config via Gateway's config.patch method.
54
+ * Body: { patch: { ... } } -- the patch to apply
55
+ */
56
+ export async function PATCH(request: NextRequest) {
57
+ const gw = ensureGateway();
58
+
59
+ if (!gw.isConnected) {
60
+ return NextResponse.json(
61
+ { error: "Gateway not connected" },
62
+ { status: 503 }
63
+ );
64
+ }
65
+
66
+ try {
67
+ let body: unknown;
68
+ try {
69
+ body = await request.json();
70
+ } catch {
71
+ return NextResponse.json(
72
+ { error: "Invalid JSON in request body" },
73
+ { status: 400 }
74
+ );
75
+ }
76
+
77
+ const { patch } = body as { patch?: unknown };
78
+
79
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
80
+ return NextResponse.json(
81
+ { error: "Missing or invalid 'patch' field — must be a JSON object" },
82
+ { status: 400 }
83
+ );
84
+ }
85
+
86
+ await gw.request("config.patch", patch);
87
+ return NextResponse.json({ ok: true });
88
+ } catch (err) {
89
+ return NextResponse.json(
90
+ { error: err instanceof Error ? err.message : "Config patch failed" },
91
+ { status: 500 }
92
+ );
93
+ }
94
+ }
@@ -0,0 +1,96 @@
1
+ import { ensureGateway, type GatewayEvent } from "@/lib/gateway-connection";
2
+
3
+ export const dynamic = "force-dynamic";
4
+ export const runtime = "nodejs";
5
+
6
+ /**
7
+ * GET /api/openclaw/events
8
+ * SSE endpoint -- streams OpenClaw Gateway events to the browser in real-time.
9
+ * Browser connects once via EventSource and receives push updates.
10
+ */
11
+ export async function GET() {
12
+ const gw = ensureGateway();
13
+
14
+ const encoder = new TextEncoder();
15
+ let closed = false;
16
+
17
+ const stream = new ReadableStream({
18
+ start(controller) {
19
+ // Send initial state
20
+ const initial = {
21
+ event: "castle.state",
22
+ payload: {
23
+ state: gw.state,
24
+ isConnected: gw.isConnected,
25
+ server: gw.serverInfo,
26
+ },
27
+ };
28
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(initial)}\n\n`));
29
+
30
+ // Forward gateway events
31
+ const onGatewayEvent = (evt: GatewayEvent) => {
32
+ if (closed) return;
33
+ try {
34
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(evt)}\n\n`));
35
+ } catch {
36
+ // Stream may have closed
37
+ }
38
+ };
39
+
40
+ // Forward state changes
41
+ const onStateChange = (state: string) => {
42
+ if (closed) return;
43
+ try {
44
+ const msg = {
45
+ event: "castle.state",
46
+ payload: {
47
+ state,
48
+ isConnected: gw.isConnected,
49
+ server: gw.serverInfo,
50
+ },
51
+ };
52
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(msg)}\n\n`));
53
+ } catch {
54
+ // Stream may have closed
55
+ }
56
+ };
57
+
58
+ // Heartbeat to keep connection alive
59
+ const heartbeat = setInterval(() => {
60
+ if (closed) return;
61
+ try {
62
+ controller.enqueue(encoder.encode(`: heartbeat\n\n`));
63
+ } catch {
64
+ // Stream may have closed
65
+ }
66
+ }, 30000);
67
+
68
+ gw.on("gatewayEvent", onGatewayEvent);
69
+ gw.on("stateChange", onStateChange);
70
+
71
+ // Cleanup when the client disconnects
72
+ const cleanup = () => {
73
+ closed = true;
74
+ clearInterval(heartbeat);
75
+ gw.off("gatewayEvent", onGatewayEvent);
76
+ gw.off("stateChange", onStateChange);
77
+ };
78
+
79
+ // The stream's cancel is called when the client disconnects
80
+ // We store cleanup for the cancel callback
81
+ (controller as unknown as { _cleanup: () => void })._cleanup = cleanup;
82
+ },
83
+ cancel(controller) {
84
+ const cleanup = (controller as unknown as { _cleanup: () => void })._cleanup;
85
+ if (cleanup) cleanup();
86
+ },
87
+ });
88
+
89
+ return new Response(stream, {
90
+ headers: {
91
+ "Content-Type": "text/event-stream",
92
+ "Cache-Control": "no-cache, no-transform",
93
+ Connection: "keep-alive",
94
+ },
95
+ });
96
+ }
@@ -0,0 +1,59 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { readFileSync, existsSync, readdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { getOpenClawDir } from "@/lib/config";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ /**
9
+ * GET /api/openclaw/logs?lines=100&file=gateway
10
+ * God mode: reads log files from ~/.openclaw/logs/
11
+ */
12
+ export async function GET(request: NextRequest) {
13
+ const { searchParams } = new URL(request.url);
14
+ const rawLines = parseInt(searchParams.get("lines") || "100", 10);
15
+ const lines = Math.min(Math.max(1, Number.isFinite(rawLines) ? rawLines : 100), 10000);
16
+ const file = searchParams.get("file")?.trim() || "gateway";
17
+
18
+ const logsDir = join(getOpenClawDir(), "logs");
19
+
20
+ if (!existsSync(logsDir)) {
21
+ return NextResponse.json({ logs: [], error: "No logs directory found" });
22
+ }
23
+
24
+ // List available log files
25
+ const availableFiles = existsSync(logsDir)
26
+ ? readdirSync(logsDir).filter((f) => f.endsWith(".log"))
27
+ : [];
28
+
29
+ // Find matching log file
30
+ const logFile = availableFiles.find((f) => f.startsWith(file));
31
+ if (!logFile) {
32
+ return NextResponse.json({
33
+ logs: [],
34
+ available: availableFiles,
35
+ error: `Log file '${file}' not found`,
36
+ });
37
+ }
38
+
39
+ try {
40
+ const logPath = join(logsDir, logFile);
41
+ const content = readFileSync(logPath, "utf-8");
42
+ const allLines = content.split("\n").filter(Boolean);
43
+
44
+ // Return last N lines
45
+ const tailLines = allLines.slice(-lines);
46
+
47
+ return NextResponse.json({
48
+ logs: tailLines,
49
+ file: logFile,
50
+ totalLines: allLines.length,
51
+ available: availableFiles,
52
+ });
53
+ } catch (err) {
54
+ return NextResponse.json(
55
+ { error: err instanceof Error ? err.message : "Failed to read logs" },
56
+ { status: 500 }
57
+ );
58
+ }
59
+ }
@@ -0,0 +1,68 @@
1
+ import { NextResponse } from "next/server";
2
+ import { ensureGateway } from "@/lib/gateway-connection";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ /**
7
+ * POST /api/openclaw/ping
8
+ * Health check -- tests connection to OpenClaw Gateway
9
+ */
10
+ export async function POST() {
11
+ const gw = ensureGateway();
12
+
13
+ if (!gw.isConfigured) {
14
+ return NextResponse.json({
15
+ ok: false,
16
+ configured: false,
17
+ error: "No OpenClaw token found. Run 'castle setup' or check ~/.openclaw/openclaw.json",
18
+ });
19
+ }
20
+
21
+ // If not connected yet, give it a moment to complete handshake
22
+ if (gw.state === "connecting") {
23
+ await new Promise<void>((resolve) => {
24
+ const timeout = setTimeout(resolve, 5000);
25
+ const onState = () => {
26
+ clearTimeout(timeout);
27
+ gw.off("stateChange", onState);
28
+ resolve();
29
+ };
30
+ gw.on("stateChange", onState);
31
+ });
32
+ }
33
+
34
+ if (!gw.isConnected) {
35
+ return NextResponse.json({
36
+ ok: false,
37
+ configured: true,
38
+ state: gw.state,
39
+ error: gw.state === "error"
40
+ ? "Failed to connect to OpenClaw Gateway. Is it running?"
41
+ : "Connecting to OpenClaw Gateway...",
42
+ });
43
+ }
44
+
45
+ try {
46
+ const start = Date.now();
47
+ await gw.request("health", {});
48
+ const latency = Date.now() - start;
49
+
50
+ return NextResponse.json({
51
+ ok: true,
52
+ configured: true,
53
+ latency_ms: latency,
54
+ server: gw.serverInfo,
55
+ });
56
+ } catch (err) {
57
+ return NextResponse.json({
58
+ ok: false,
59
+ configured: true,
60
+ error: err instanceof Error ? err.message : "Health check failed",
61
+ });
62
+ }
63
+ }
64
+
65
+ // Allow GET for easy status checks
66
+ export async function GET() {
67
+ return POST();
68
+ }
@@ -0,0 +1,65 @@
1
+ import { NextResponse } from "next/server";
2
+ import { execSync } from "child_process";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ /**
7
+ * POST /api/openclaw/restart
8
+ * God mode: restart the OpenClaw Gateway process.
9
+ * Tries common methods to restart the gateway.
10
+ */
11
+ export async function POST() {
12
+ try {
13
+ // Try openclaw CLI restart first
14
+ try {
15
+ execSync("openclaw gateway restart", {
16
+ timeout: 10000,
17
+ stdio: "pipe",
18
+ });
19
+ return NextResponse.json({
20
+ ok: true,
21
+ method: "openclaw gateway restart",
22
+ });
23
+ } catch {
24
+ // openclaw CLI not available or restart command failed
25
+ }
26
+
27
+ // Try stopping and starting via process signal
28
+ try {
29
+ const pids = execSync("pgrep -f 'openclaw.*gateway'", {
30
+ timeout: 5000,
31
+ stdio: "pipe",
32
+ })
33
+ .toString()
34
+ .trim()
35
+ .split(/\s+/)
36
+ .filter((p) => /^\d+$/.test(p));
37
+
38
+ const pid = pids[0];
39
+ if (pid) {
40
+ execSync(`kill -HUP ${pid}`, { timeout: 5000, stdio: "pipe" });
41
+ return NextResponse.json({
42
+ ok: true,
43
+ method: "SIGHUP",
44
+ pid: parseInt(pid, 10),
45
+ });
46
+ }
47
+ } catch {
48
+ // pgrep not available or no process found
49
+ }
50
+
51
+ return NextResponse.json(
52
+ {
53
+ ok: false,
54
+ error:
55
+ "Could not restart gateway. Try manually: openclaw gateway restart",
56
+ },
57
+ { status: 500 }
58
+ );
59
+ } catch (err) {
60
+ return NextResponse.json(
61
+ { ok: false, error: err instanceof Error ? err.message : "Restart failed" },
62
+ { status: 500 }
63
+ );
64
+ }
65
+ }