File size: 4,309 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { z } from "zod";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import { getCliPrimaryConfigPath } from "@/shared/services/cliRuntime";
import { validateBaseUrl } from "@/lib/cli-helper/config-generator";
import {
  generateHermesAgentConfig,
  getCurrentHermesAgentRoles,
} from "@/lib/cli-helper/config-generator/hermes-agent";
import { getHermesConfigPath } from "@/lib/cli-helper/config-generator/hermesHome";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";

const hermesAgentSettingsSchema = z.object({
  baseUrl: z.string().min(1, "baseUrl is required"),
  keyId: z.string().optional().nullable(),
  apiKey: z.string().optional().nullable(),
  selections: z
    .array(
      z.object({
        role: z.string(),
        model: z.string(),
      })
    )
    .min(1, "selections must be a non-empty array of { role, model }"),
  preview: z.boolean().optional(),
});

/**
 * Dedicated endpoint for Hermes Agent (the advanced Nous Research terminal agent).
 * This is separate from the original simple "Hermes" guided tool.
 *
 * GET  -> returns current per-role configuration (default, delegation, auxiliary.*)
 * POST -> accepts { baseUrl, keyId?, apiKey?, selections: [{role, model}, ...] }
 */

// Resolved lazily so HERMES_HOME is always honoured (#3628).
const getConfigPath = () => getHermesConfigPath();

function getMetadataPath(configPath: string) {
  return path.join(path.dirname(configPath), ".first-setup.json");
}

export async function GET(request: Request) {
  // cli-tools routes touch host config files — guard every handler with the shared auth.
  const authError = await requireCliToolsAuth(request);
  if (authError) return authError;
  try {
    const roles = await getCurrentHermesAgentRoles();

    const configPath = getCliPrimaryConfigPath("hermes-agent") || getConfigPath();
    let firstSetupAt: string | null = null;

    try {
      const metaRaw = await fs.readFile(getMetadataPath(configPath), "utf8");
      const meta = JSON.parse(metaRaw);
      firstSetupAt = meta.firstSetupAt || null;
    } catch {
      // no metadata yet
    }

    return NextResponse.json({ success: true, roles, firstSetupAt });
  } catch (error) {
    return NextResponse.json(
      { success: false, error: sanitizeErrorMessage(error) },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  const authError = await requireCliToolsAuth(request);
  if (authError) return authError;

  let rawBody;
  try {
    rawBody = await request.json();
  } catch {
    return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
  }

  const parsed = hermesAgentSettingsSchema.safeParse(rawBody);
  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.issues[0]?.message ?? "Invalid request" },
      { status: 400 }
    );
  }

  const { baseUrl, keyId, apiKey, selections, preview } = parsed.data;

  if (!validateBaseUrl(baseUrl)) {
    return NextResponse.json({ error: "baseUrl must be a valid http(s) URL" }, { status: 400 });
  }

  const configPath = getCliPrimaryConfigPath("hermes-agent") || getConfigPath();
  const configDir = path.dirname(configPath);

  await fs.mkdir(configDir, { recursive: true });

  const payload = {
    baseUrl,
    keyId,
    apiKey,
    selections,
  };

  const result = await generateHermesAgentConfig(payload);

  if (result.error) {
    return NextResponse.json({ error: result.error }, { status: 400 });
  }

  // Preview mode: return the would-be YAML without writing it (Phase 5 polish)
  if (preview === true) {
    return NextResponse.json({
      success: true,
      preview: true,
      yaml: result.yaml,
      configPath,
    });
  }

  await fs.writeFile(configPath, result.yaml, "utf-8");

  // Record first setup time if this is the first save via OmniRoute
  const metaPath = getMetadataPath(configPath);
  try {
    await fs.access(metaPath);
  } catch {
    await fs.writeFile(
      metaPath,
      JSON.stringify({ firstSetupAt: new Date().toISOString() }),
      "utf8"
    );
  }

  return NextResponse.json({
    success: true,
    message: `Hermes Agent config saved to ${configPath}`,
    configPath,
  });
}