Refactor Hexclave skill context to use documentation instead of skill prompts

- Updated the fetch mechanism to retrieve full documentation from the new URL.
- Renamed functions and variables to reflect the change from skill to documentation context.
- Adjusted tests to validate the new documentation fetching behavior and error handling.
- Removed the obsolete llms-full.txt file as it is no longer needed.
This commit is contained in:
mantrakp04 2026-06-18 10:30:41 -07:00
parent 1836e101bb
commit ebfba5ce2b
6 changed files with 40 additions and 1186 deletions

View File

@ -1,10 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { _clearSkillCache, getMcpSkillContextPrompt } from "./mcp-skill-context";
import { _clearDocsCache, getMcpSkillContextPrompt } from "./mcp-skill-context";
describe("getMcpSkillContextPrompt", () => {
afterEach(() => {
vi.restoreAllMocks();
_clearSkillCache();
_clearDocsCache();
});
it("returns empty string for non-ask_hexclave tool names", async () => {
@ -28,41 +28,39 @@ describe("getMcpSkillContextPrompt", () => {
expect(fetchSpy).not.toHaveBeenCalled();
});
it("fetches and embeds the canonical skill for ask_hexclave requests", async () => {
it("fetches and embeds the full documentation for ask_hexclave requests", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("# Hexclave Skill\n\nUse Hexclave docs."),
new Response("# Hexclave Docs\n\nUse Hexclave docs."),
);
await expect(getMcpSkillContextPrompt("ask_hexclave")).resolves.toMatchInlineSnapshot(`
"
## MCP-Provided Hexclave Skill Context
## MCP-Provided Hexclave Documentation Context
The current request came through the public Hexclave MCP server's ask_hexclave tool.
The backend fetched the canonical Hexclave agent skill from https://skill.hexclave.com
immediately before spawning this assistant. Treat this skill content as baseline context
The backend fetched the full Hexclave documentation from https://docs.hexclave.com/llms-full.txt
immediately before spawning this assistant. Treat this documentation as baseline context
for answering the user's question, while still using documentation tools for specific
facts and citations:
# Hexclave Skill
# Hexclave Docs
Use Hexclave docs.
"
`);
expect(fetchSpy).toHaveBeenCalledWith("https://skill.hexclave.com", expect.objectContaining({
expect(fetchSpy).toHaveBeenCalledWith("https://docs.hexclave.com/llms-full.txt", expect.objectContaining({
headers: { Accept: "text/markdown" },
signal: expect.any(AbortSignal),
}));
});
it("fails loudly when the canonical skill cannot be fetched", async () => {
it("fails loudly when the documentation cannot be fetched", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("missing", { status: 503, statusText: "Service Unavailable" }),
);
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to fetch skill from https://skill.hexclave.com: 503 Service Unavailable]`,
);
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to fetch docs from https://docs.hexclave.com/llms-full.txt: 503 Service Unavailable]`);
});
it("throws a descriptive error when the fetch times out", async () => {
@ -71,14 +69,12 @@ describe("getMcpSkillContextPrompt", () => {
return Promise.reject(err);
});
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Skill fetch from https://skill.hexclave.com timed out after 5000ms]`,
);
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Docs fetch from https://docs.hexclave.com/llms-full.txt timed out after 5000ms]`);
});
it("returns cached skill on subsequent calls within TTL", async () => {
it("returns cached documentation on subsequent calls within TTL", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("# Cached Skill"),
new Response("# Cached Docs"),
);
const first = await getMcpSkillContextPrompt("ask_hexclave");

View File

@ -1,13 +1,13 @@
const HEXCLAVE_SKILL_URI = "https://skill.hexclave.com";
const HEXCLAVE_DOCS_FULL_URL = "https://docs.hexclave.com/llms-full.txt";
const FETCH_TIMEOUT_MS = 5_000;
const CACHE_TTL_MS = 5 * 60 * 1_000; // 5 minutes
let cachedSkill: { text: string, fetchedAt: number } | null = null;
let cachedDocs: { text: string, fetchedAt: number } | null = null;
async function fetchSkillText(): Promise<string> {
async function fetchDocsText(): Promise<string> {
const now = Date.now();
if (cachedSkill && now - cachedSkill.fetchedAt < CACHE_TTL_MS) {
return cachedSkill.text;
if (cachedDocs && now - cachedDocs.fetchedAt < CACHE_TTL_MS) {
return cachedDocs.text;
}
const controller = new AbortController();
@ -15,13 +15,13 @@ async function fetchSkillText(): Promise<string> {
let response: Response;
try {
response = await fetch(HEXCLAVE_SKILL_URI, {
response = await fetch(HEXCLAVE_DOCS_FULL_URL, {
headers: { Accept: "text/markdown" },
signal: controller.signal,
});
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") {
throw new Error(`Skill fetch from ${HEXCLAVE_SKILL_URI} timed out after ${FETCH_TIMEOUT_MS}ms`);
throw new Error(`Docs fetch from ${HEXCLAVE_DOCS_FULL_URL} timed out after ${FETCH_TIMEOUT_MS}ms`);
}
throw err;
} finally {
@ -30,12 +30,12 @@ async function fetchSkillText(): Promise<string> {
if (!response.ok) {
throw new Error(
`Failed to fetch skill from ${HEXCLAVE_SKILL_URI}: ${response.status} ${response.statusText}`,
`Failed to fetch docs from ${HEXCLAVE_DOCS_FULL_URL}: ${response.status} ${response.statusText}`,
);
}
const text = await response.text();
cachedSkill = { text, fetchedAt: now };
cachedDocs = { text, fetchedAt: now };
return text;
}
@ -44,24 +44,24 @@ export async function getMcpSkillContextPrompt(toolName: string | null | undefin
return "";
}
const skillContext = await fetchSkillText();
const docsContext = await fetchDocsText();
return `
## MCP-Provided Hexclave Skill Context
## MCP-Provided Hexclave Documentation Context
The current request came through the public Hexclave MCP server's ask_hexclave tool.
The backend fetched the canonical Hexclave agent skill from https://skill.hexclave.com
immediately before spawning this assistant. Treat this skill content as baseline context
The backend fetched the full Hexclave documentation from https://docs.hexclave.com/llms-full.txt
immediately before spawning this assistant. Treat this documentation as baseline context
for answering the user's question, while still using documentation tools for specific
facts and citations:
${skillContext}
${docsContext}
`;
}
/**
* Exposed for testing only clears the module-level skill cache.
* Exposed for testing only clears the module-level docs cache.
*/
export function _clearSkillCache(): void {
cachedSkill = null;
export function _clearDocsCache(): void {
cachedDocs = null;
}

View File

@ -20,6 +20,7 @@ export type ToolName = typeof TOOL_NAMES[number];
export type ToolContext = {
auth: SmartRequestAuth | null,
targetProjectId?: string | null,
mcpToolName?: string | null,
};
export async function getTools(
@ -31,6 +32,9 @@ export async function getTools(
for (const toolName of toolNames) {
switch (toolName) {
case "docs": {
if (context.mcpToolName === "ask_hexclave") {
break;
}
const docsTools = await createDocsTools();
Object.assign(tools, docsTools);
break;

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
import { ALL_APPS, AppId } from "@hexclave/shared/dist/apps/apps-config";
import { AppIcon, appSquarePaddingExpression, appSquareWidthExpression } from "@hexclave/shared/dist/apps/apps-ui";
import { BarChart3, ClipboardList, Code, CreditCard, Headset, KeyRound, Mail, Mails, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
import { BarChart3, ClipboardList, Code, CreditCard, Headset, KeyRound, Mail, Mails, MousePointerClick, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
import Link from "next/link";
import { cn } from "../../lib/cn";
@ -43,6 +43,7 @@ const APP_ICONS: Record<AppId, React.FunctionComponent<React.SVGProps<SVGSVGElem
"tanstack-start": Code,
onboarding: ClipboardList,
analytics: BarChart3,
clickmaps: MousePointerClick,
"session-replays": PlayCircle,
};

View File

@ -1,10 +1,10 @@
import path from "path";
import { readFileSync } from "fs";
import path from "path";
import { llmsTxt } from "../packages/shared/src/ai/llms/llms";
import { remindersPrompt } from "../packages/shared/src/ai/unified-prompts/reminders";
import { aiSetupPrompt, cliSetupPrompt, convexSetupPrompt, getSdkSetupPrompt, pythonBackendSetupPrompt, restApiBackendSetupPrompt, supabaseSetupPrompt } from "../packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt";
import { deindent } from "../packages/shared/src/utils/strings";
import { writeFileSyncIfChanged } from "./utils";
import { remindersPrompt } from "../packages/shared/src/ai/unified-prompts/reminders";
import { buildLlmsFullTxt, llmsTxt } from "../packages/shared/src/ai/llms/llms";
const generatedComment = "This file is auto-generated by scripts/generate-setup-prompt-docs.ts. Do not edit it manually; edit packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts instead.";
type SdkSetupToolCategory = "frontend" | "backend" | "database" | "other";
@ -590,8 +590,3 @@ writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/llms.txt"),
llmsTxt.replace(/[ \t]+$/gm, "") + "\n",
);
writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/llms-full.txt"),
buildLlmsFullTxt(docsJson).replace(/[ \t]+$/gm, "") + "\n",
);