Add event-driven tasks via Gitea webhooks
Webhook endpoint at /api/webhooks/gitea receives Gitea status events,
matches them against configurable event triggers with conditions
(event type, repo glob, state, context), renders task templates with
{{variable}} substitution, and creates harness tasks automatically.
Includes circuit breaker: after N consecutive task failures from the
same trigger (default 3), the trigger auto-disables. Re-enable
manually via PATCH /api/event-triggers/:id.
New tables: harness_event_triggers (rules + circuit breaker state),
harness_event_log (audit trail + dedup via X-Gitea-Delivery).
This commit is contained in:
@@ -4,13 +4,14 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3100",
|
||||
"build": "next build",
|
||||
"build": "node scripts/build-mcp.mjs && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "echo \"no tests yet\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@homelab/db": "workspace:^",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"drizzle-orm": "^0.36.0",
|
||||
@@ -19,14 +20,17 @@
|
||||
"postgres": "^3.4.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"ws": "^8.20.0",
|
||||
"yaml": "^2.7.0"
|
||||
"yaml": "^2.7.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.27.4",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
19
apps/harness/scripts/build-mcp.mjs
Normal file
19
apps/harness/scripts/build-mcp.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { build } from "esbuild";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
await build({
|
||||
entryPoints: [resolve(__dirname, "../src/mcp-server.ts")],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
format: "esm",
|
||||
outfile: resolve(__dirname, "../dist/mcp-server.mjs"),
|
||||
external: [],
|
||||
banner: { js: "#!/usr/bin/env node" },
|
||||
// Inline all deps — the output is a self-contained script
|
||||
});
|
||||
|
||||
console.log("Built dist/mcp-server.mjs");
|
||||
64
apps/harness/src/app/api/event-triggers/[id]/route.ts
Normal file
64
apps/harness/src/app/api/event-triggers/[id]/route.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getEventTrigger,
|
||||
updateEventTrigger,
|
||||
deleteEventTrigger,
|
||||
getEventLogByTrigger,
|
||||
} from "@/lib/event-store";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const trigger = await getEventTrigger(id);
|
||||
if (!trigger) {
|
||||
return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
}
|
||||
const recentLogs = await getEventLogByTrigger(id, 20);
|
||||
return NextResponse.json({ ...trigger, recentLogs });
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Allow re-enabling a circuit-broken trigger
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (body.name !== undefined) updates.name = body.name;
|
||||
if (body.enabled !== undefined) updates.enabled = body.enabled;
|
||||
if (body.eventType !== undefined) updates.eventType = body.eventType;
|
||||
if (body.repoFilter !== undefined) updates.repoFilter = body.repoFilter;
|
||||
if (body.stateFilter !== undefined) updates.stateFilter = body.stateFilter;
|
||||
if (body.contextFilter !== undefined) updates.contextFilter = body.contextFilter;
|
||||
if (body.taskTemplate !== undefined) updates.taskTemplate = body.taskTemplate;
|
||||
if (body.maxConsecutiveFailures !== undefined) updates.maxConsecutiveFailures = body.maxConsecutiveFailures;
|
||||
if (body.webhookSecret !== undefined) updates.webhookSecret = body.webhookSecret;
|
||||
|
||||
// Re-enable: reset circuit breaker
|
||||
if (body.enabled === true) {
|
||||
updates.consecutiveFailures = 0;
|
||||
updates.disabledReason = null;
|
||||
}
|
||||
|
||||
const updated = await updateEventTrigger(id, updates);
|
||||
if (!updated) {
|
||||
return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const deleted = await deleteEventTrigger(id);
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
53
apps/harness/src/app/api/event-triggers/route.ts
Normal file
53
apps/harness/src/app/api/event-triggers/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getAllEventTriggers,
|
||||
createEventTrigger,
|
||||
} from "@/lib/event-store";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(await getAllEventTriggers());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name || !body.eventType || !body.taskTemplate) {
|
||||
return NextResponse.json(
|
||||
{ error: "name, eventType, and taskTemplate are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.taskTemplate.agentId || !body.taskTemplate.goal) {
|
||||
return NextResponse.json(
|
||||
{ error: "taskTemplate must include agentId and goal" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const trigger = await createEventTrigger({
|
||||
id: body.id || `evt-${Date.now()}`,
|
||||
name: body.name,
|
||||
enabled: body.enabled ?? true,
|
||||
eventType: body.eventType,
|
||||
repoFilter: body.repoFilter || null,
|
||||
stateFilter: body.stateFilter || null,
|
||||
contextFilter: body.contextFilter || null,
|
||||
taskTemplate: {
|
||||
slug: body.taskTemplate.slug || "event-{{sha_short}}",
|
||||
goal: body.taskTemplate.goal,
|
||||
project: body.taskTemplate.project || "{{repo}}",
|
||||
gitProvider: body.taskTemplate.gitProvider,
|
||||
gitBaseUrl: body.taskTemplate.gitBaseUrl,
|
||||
agentId: body.taskTemplate.agentId,
|
||||
maxIterations: body.taskTemplate.maxIterations || 6,
|
||||
criteria: body.taskTemplate.criteria || [],
|
||||
constraints: body.taskTemplate.constraints || [],
|
||||
knowledgeRefs: body.taskTemplate.knowledgeRefs || [],
|
||||
},
|
||||
maxConsecutiveFailures: body.maxConsecutiveFailures ?? 3,
|
||||
webhookSecret: body.webhookSecret || null,
|
||||
});
|
||||
|
||||
return NextResponse.json(trigger, { status: 201 });
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
running: isRunning(),
|
||||
currentTaskId: currentRunningTaskId(),
|
||||
running: await isRunning(),
|
||||
currentTaskId: await currentRunningTaskId(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ export async function POST(request: NextRequest) {
|
||||
const action = body.action as string;
|
||||
|
||||
if (action === "start") {
|
||||
startOrchestrator();
|
||||
await startOrchestrator();
|
||||
return NextResponse.json({ ok: true, running: true });
|
||||
}
|
||||
|
||||
if (action === "stop") {
|
||||
stopOrchestrator();
|
||||
await stopOrchestrator();
|
||||
return NextResponse.json({ ok: true, running: false });
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
startOrchestrator();
|
||||
await startOrchestrator();
|
||||
|
||||
return NextResponse.json({ ok: true, message: "Orchestrator started, task will be picked up" });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getTask } from "@/lib/store";
|
||||
import { cancelTask } from "@/lib/orchestrator";
|
||||
import { getTask, requestTaskCancel } from "@/lib/store";
|
||||
|
||||
export async function POST(
|
||||
_request: NextRequest,
|
||||
@@ -20,11 +19,11 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
const cancelled = cancelTask(id);
|
||||
const cancelled = await requestTaskCancel(id);
|
||||
if (!cancelled) {
|
||||
return NextResponse.json(
|
||||
{ error: "Task is not the currently executing task" },
|
||||
{ status: 400 },
|
||||
{ error: "Failed to request cancellation" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
158
apps/harness/src/app/api/webhooks/gitea/route.ts
Normal file
158
apps/harness/src/app/api/webhooks/gitea/route.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import { createTask } from "@/lib/store";
|
||||
import { extractVariables, renderTaskTemplate } from "@/lib/template";
|
||||
import { findMatchingTriggers } from "@/lib/event-matching";
|
||||
import {
|
||||
createEventLogEntry,
|
||||
getEventLogByDeliveryId,
|
||||
getEventTrigger,
|
||||
} from "@/lib/event-store";
|
||||
import { startOrchestrator } from "@/lib/orchestrator";
|
||||
import type { Task } from "@/lib/types";
|
||||
|
||||
function verifySignature(
|
||||
rawBody: string,
|
||||
signature: string,
|
||||
secret: string,
|
||||
): boolean {
|
||||
const computed = createHmac("sha256", secret).update(rawBody).digest("hex");
|
||||
try {
|
||||
return timingSafeEqual(
|
||||
Buffer.from(computed, "hex"),
|
||||
Buffer.from(signature, "hex"),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const deliveryId = request.headers.get("x-gitea-delivery") || "";
|
||||
const eventType = request.headers.get("x-gitea-event") || "";
|
||||
const signature = request.headers.get("x-gitea-signature") || "";
|
||||
|
||||
const rawBody = await request.text();
|
||||
let payload: Record<string, unknown>;
|
||||
try {
|
||||
payload = JSON.parse(rawBody);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
// HMAC validation
|
||||
const globalSecret = process.env.GITEA_WEBHOOK_SECRET;
|
||||
if (globalSecret && signature) {
|
||||
if (!verifySignature(rawBody, signature, globalSecret)) {
|
||||
return NextResponse.json({ error: "invalid signature" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
// Dedup
|
||||
if (deliveryId) {
|
||||
const existing = await getEventLogByDeliveryId(deliveryId);
|
||||
if (existing) {
|
||||
return NextResponse.json({ status: "duplicate", deliveryId });
|
||||
}
|
||||
}
|
||||
|
||||
// Parse event
|
||||
const event = extractVariables(eventType, payload);
|
||||
|
||||
// Find matching triggers
|
||||
const triggers = await findMatchingTriggers(event);
|
||||
|
||||
if (triggers.length === 0) {
|
||||
return NextResponse.json({
|
||||
status: "no_match",
|
||||
eventType,
|
||||
repo: event.repo,
|
||||
state: event.state,
|
||||
});
|
||||
}
|
||||
|
||||
const results: { triggerId: string; taskId: string | null; status: string }[] = [];
|
||||
|
||||
for (const trigger of triggers) {
|
||||
const logDeliveryId = triggers.length > 1
|
||||
? `${deliveryId || Date.now()}-${trigger.id}`
|
||||
: deliveryId || String(Date.now());
|
||||
|
||||
try {
|
||||
// Check per-trigger secret if set
|
||||
if (trigger.webhookSecret && signature) {
|
||||
if (!verifySignature(rawBody, signature, trigger.webhookSecret)) {
|
||||
await createEventLogEntry({
|
||||
triggerId: trigger.id,
|
||||
deliveryId: logDeliveryId,
|
||||
eventType,
|
||||
repo: event.repo,
|
||||
commitSha: event.sha,
|
||||
branch: event.branch,
|
||||
status: "skipped",
|
||||
skipReason: "Per-trigger signature validation failed",
|
||||
payload,
|
||||
});
|
||||
results.push({ triggerId: trigger.id, taskId: null, status: "skipped" });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Render task spec from template
|
||||
const spec = renderTaskTemplate(trigger.taskTemplate, event);
|
||||
const taskId = `task-${Date.now()}-${trigger.id.slice(-4)}`;
|
||||
|
||||
const task: Task = {
|
||||
id: taskId,
|
||||
slug: spec.slug,
|
||||
goal: spec.goal,
|
||||
project: spec.project,
|
||||
status: "pending",
|
||||
iteration: 0,
|
||||
maxIterations: spec.maxIterations,
|
||||
startedAt: null,
|
||||
evals: {},
|
||||
iterations: [],
|
||||
spec,
|
||||
};
|
||||
|
||||
await createTask(task);
|
||||
|
||||
await createEventLogEntry({
|
||||
triggerId: trigger.id,
|
||||
deliveryId: logDeliveryId,
|
||||
eventType,
|
||||
repo: event.repo,
|
||||
commitSha: event.sha,
|
||||
branch: event.branch,
|
||||
status: "task_created",
|
||||
taskId,
|
||||
payload,
|
||||
});
|
||||
|
||||
results.push({ triggerId: trigger.id, taskId, status: "task_created" });
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
await createEventLogEntry({
|
||||
triggerId: trigger.id,
|
||||
deliveryId: logDeliveryId,
|
||||
eventType,
|
||||
repo: event.repo,
|
||||
commitSha: event.sha,
|
||||
branch: event.branch,
|
||||
status: "error",
|
||||
error: errorMsg,
|
||||
payload,
|
||||
}).catch(() => {});
|
||||
|
||||
results.push({ triggerId: trigger.id, taskId: null, status: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure orchestrator is running to pick up new tasks
|
||||
if (results.some(r => r.status === "task_created")) {
|
||||
startOrchestrator();
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: "processed", results });
|
||||
}
|
||||
34
apps/harness/src/lib/event-matching.ts
Normal file
34
apps/harness/src/lib/event-matching.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Match webhook events against event trigger conditions.
|
||||
|
||||
import { getEnabledEventTriggers, type EventTrigger } from "./event-store";
|
||||
import { type ParsedEvent } from "./template";
|
||||
|
||||
function escapeRegex(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function globMatch(pattern: string, value: string): boolean {
|
||||
const regex = new RegExp(
|
||||
"^" + pattern.split("*").map(escapeRegex).join(".*") + "$",
|
||||
);
|
||||
return regex.test(value);
|
||||
}
|
||||
|
||||
export function matchesTrigger(trigger: EventTrigger, event: ParsedEvent): boolean {
|
||||
if (!trigger.enabled) return false;
|
||||
|
||||
if (trigger.eventType !== "*" && trigger.eventType !== event.eventType) return false;
|
||||
|
||||
if (trigger.repoFilter && !globMatch(trigger.repoFilter, event.repo)) return false;
|
||||
|
||||
if (trigger.stateFilter && trigger.stateFilter !== event.state) return false;
|
||||
|
||||
if (trigger.contextFilter && !event.context.includes(trigger.contextFilter)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function findMatchingTriggers(event: ParsedEvent): Promise<EventTrigger[]> {
|
||||
const triggers = await getEnabledEventTriggers();
|
||||
return triggers.filter(t => matchesTrigger(t, event));
|
||||
}
|
||||
123
apps/harness/src/lib/event-store.ts
Normal file
123
apps/harness/src/lib/event-store.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import { db } from "./db";
|
||||
import { eventTriggers, eventLog } from "@homelab/db";
|
||||
|
||||
// ─── TYPES ──────────────────────────────────────────────────
|
||||
|
||||
export type EventTrigger = typeof eventTriggers.$inferSelect;
|
||||
export type EventTriggerInsert = typeof eventTriggers.$inferInsert;
|
||||
export type EventLogEntry = typeof eventLog.$inferSelect;
|
||||
|
||||
// ─── EVENT TRIGGERS ─────────────────────────────────────────
|
||||
|
||||
export async function getAllEventTriggers(): Promise<EventTrigger[]> {
|
||||
return db.select().from(eventTriggers);
|
||||
}
|
||||
|
||||
export async function getEnabledEventTriggers(): Promise<EventTrigger[]> {
|
||||
return db.select().from(eventTriggers).where(eq(eventTriggers.enabled, true));
|
||||
}
|
||||
|
||||
export async function getEventTrigger(id: string): Promise<EventTrigger | undefined> {
|
||||
const [row] = await db.select().from(eventTriggers).where(eq(eventTriggers.id, id));
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function createEventTrigger(trigger: EventTriggerInsert): Promise<EventTrigger> {
|
||||
const [row] = await db.insert(eventTriggers).values(trigger).returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function updateEventTrigger(
|
||||
id: string,
|
||||
updates: Partial<EventTriggerInsert>,
|
||||
): Promise<EventTrigger | undefined> {
|
||||
const [row] = await db
|
||||
.update(eventTriggers)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(eventTriggers.id, id))
|
||||
.returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function deleteEventTrigger(id: string): Promise<boolean> {
|
||||
await db.delete(eventLog).where(eq(eventLog.triggerId, id));
|
||||
const result = await db.delete(eventTriggers).where(eq(eventTriggers.id, id));
|
||||
return (result as unknown as { rowCount: number }).rowCount > 0;
|
||||
}
|
||||
|
||||
// ─── EVENT LOG ──────────────────────────────────────────────
|
||||
|
||||
export async function createEventLogEntry(
|
||||
entry: typeof eventLog.$inferInsert,
|
||||
): Promise<EventLogEntry> {
|
||||
const [row] = await db.insert(eventLog).values(entry).returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function getEventLogByDeliveryId(
|
||||
deliveryId: string,
|
||||
): Promise<EventLogEntry | undefined> {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(eventLog)
|
||||
.where(eq(eventLog.deliveryId, deliveryId));
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function getEventLogByTaskId(
|
||||
taskId: string,
|
||||
): Promise<EventLogEntry | undefined> {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(eventLog)
|
||||
.where(eq(eventLog.taskId, taskId));
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function getEventLogByTrigger(
|
||||
triggerId: string,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
): Promise<EventLogEntry[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(eventLog)
|
||||
.where(eq(eventLog.triggerId, triggerId))
|
||||
.orderBy(desc(eventLog.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
}
|
||||
|
||||
// ─── CIRCUIT BREAKER ────────────────────────────────────────
|
||||
|
||||
export async function recordTaskOutcome(
|
||||
triggerId: string,
|
||||
passed: boolean,
|
||||
): Promise<void> {
|
||||
const trigger = await getEventTrigger(triggerId);
|
||||
if (!trigger) return;
|
||||
|
||||
if (passed) {
|
||||
await db
|
||||
.update(eventTriggers)
|
||||
.set({ consecutiveFailures: 0, updatedAt: new Date() })
|
||||
.where(eq(eventTriggers.id, triggerId));
|
||||
return;
|
||||
}
|
||||
|
||||
const newCount = trigger.consecutiveFailures + 1;
|
||||
const shouldDisable = newCount >= trigger.maxConsecutiveFailures;
|
||||
|
||||
await db
|
||||
.update(eventTriggers)
|
||||
.set({
|
||||
consecutiveFailures: newCount,
|
||||
enabled: shouldDisable ? false : trigger.enabled,
|
||||
disabledReason: shouldDisable
|
||||
? `Circuit breaker: ${newCount} consecutive failures`
|
||||
: trigger.disabledReason,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(eventTriggers.id, triggerId));
|
||||
}
|
||||
@@ -5,9 +5,15 @@ import {
|
||||
updateIteration,
|
||||
getFirstPendingTask,
|
||||
getRunningTasks,
|
||||
getOrchestratorState,
|
||||
setOrchestratorRunning,
|
||||
setOrchestratorCurrentTask,
|
||||
updateOrchestratorHeartbeat,
|
||||
isTaskCancelRequested,
|
||||
} from "./store";
|
||||
import { recordUsage } from "./model-store";
|
||||
import { getAgentConfig } from "./agents";
|
||||
import { getEventLogByTaskId, recordTaskOutcome } from "./event-store";
|
||||
import { getRawCredentialsByProvider } from "./credentials";
|
||||
import {
|
||||
ensureBareClone,
|
||||
@@ -25,47 +31,48 @@ import { evaluate } from "./evaluator";
|
||||
import { Task, Iteration } from "./types";
|
||||
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const CANCEL_CHECK_INTERVAL_MS = 3000;
|
||||
|
||||
// Local process state — only the AbortController and poll timer live in memory.
|
||||
// Everything else (running, currentTaskId) is in Postgres.
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let running = false;
|
||||
let currentTaskId: string | null = null;
|
||||
let currentAbort: AbortController | null = null;
|
||||
|
||||
export function isRunning(): boolean {
|
||||
return running;
|
||||
// ─── Public API (all DB-backed) ─────────────────────────────
|
||||
|
||||
export async function isRunning(): Promise<boolean> {
|
||||
const state = await getOrchestratorState();
|
||||
return state.running;
|
||||
}
|
||||
|
||||
export function currentRunningTaskId(): string | null {
|
||||
return currentTaskId;
|
||||
export async function currentRunningTaskId(): Promise<string | null> {
|
||||
const state = await getOrchestratorState();
|
||||
return state.currentTaskId;
|
||||
}
|
||||
|
||||
export function startOrchestrator(): void {
|
||||
if (running) return;
|
||||
running = true;
|
||||
export async function startOrchestrator(): Promise<void> {
|
||||
const state = await getOrchestratorState();
|
||||
if (state.running && pollTimer) return;
|
||||
|
||||
recoverCrashedTasks();
|
||||
await setOrchestratorRunning(true);
|
||||
await recoverCrashedTasks();
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
if (currentTaskId) return;
|
||||
poll();
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
poll();
|
||||
}
|
||||
|
||||
export function stopOrchestrator(): void {
|
||||
running = false;
|
||||
export async function stopOrchestrator(): Promise<void> {
|
||||
await setOrchestratorRunning(false);
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelTask(taskId: string): boolean {
|
||||
if (currentTaskId !== taskId) return false;
|
||||
currentAbort?.abort();
|
||||
return true;
|
||||
}
|
||||
// ─── Internals ──────────────────────────────────────────────
|
||||
|
||||
async function recoverCrashedTasks(): Promise<void> {
|
||||
const runningTasks = await getRunningTasks();
|
||||
@@ -84,15 +91,17 @@ async function recoverCrashedTasks(): Promise<void> {
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
await setOrchestratorCurrentTask(null);
|
||||
}
|
||||
|
||||
async function poll(): Promise<void> {
|
||||
if (!running || currentTaskId) return;
|
||||
const state = await getOrchestratorState();
|
||||
if (!state.running || state.currentTaskId) return;
|
||||
|
||||
const task = await getFirstPendingTask();
|
||||
if (!task) return;
|
||||
|
||||
currentTaskId = task.id;
|
||||
await setOrchestratorCurrentTask(task.id);
|
||||
currentAbort = new AbortController();
|
||||
|
||||
try {
|
||||
@@ -104,11 +113,43 @@ async function poll(): Promise<void> {
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
currentTaskId = null;
|
||||
// Circuit breaker: update event trigger if this task was event-driven
|
||||
try {
|
||||
const finalTask = await getTask(task.id);
|
||||
if (finalTask && (finalTask.status === "completed" || finalTask.status === "failed")) {
|
||||
const logEntry = await getEventLogByTaskId(task.id);
|
||||
if (logEntry) {
|
||||
await recordTaskOutcome(logEntry.triggerId, finalTask.status === "completed");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[orchestrator] Circuit breaker update failed:", err);
|
||||
}
|
||||
|
||||
await setOrchestratorCurrentTask(null);
|
||||
currentAbort = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically checks the cancel_requested flag in Postgres and triggers
|
||||
* the local AbortController if set. Returns a cleanup function.
|
||||
*/
|
||||
function startCancelWatcher(taskId: string, abort: AbortController): () => void {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
if (await isTaskCancelRequested(taskId)) {
|
||||
abort.abort();
|
||||
clearInterval(timer);
|
||||
}
|
||||
} catch {
|
||||
// DB read failure — don't crash the watcher, just retry next tick
|
||||
}
|
||||
}, CANCEL_CHECK_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
|
||||
async function runTask(task: Task): Promise<void> {
|
||||
const agentConfig = await getAgentConfig(task.spec.agentId);
|
||||
if (!agentConfig) {
|
||||
@@ -138,11 +179,15 @@ async function runTask(task: Task): Promise<void> {
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
|
||||
// Start watching for cancel_requested in DB
|
||||
const stopCancelWatcher = startCancelWatcher(task.id, currentAbort!);
|
||||
|
||||
let bareClone: string;
|
||||
try {
|
||||
bareClone = await ensureBareClone(repoUrl, task.slug);
|
||||
} catch (err) {
|
||||
console.error(`[orchestrator] Failed to clone repo for task ${task.id}:`, err);
|
||||
stopCancelWatcher();
|
||||
await updateTask(task.id, {
|
||||
status: "failed",
|
||||
completedAt: Date.now(),
|
||||
@@ -159,11 +204,17 @@ async function runTask(task: Task): Promise<void> {
|
||||
status: "failed",
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
stopCancelWatcher();
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOrchestratorHeartbeat();
|
||||
|
||||
const result = await runIteration(task, n, bareClone, branchName);
|
||||
if (!result) return;
|
||||
if (!result) {
|
||||
stopCancelWatcher();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.allPassed) {
|
||||
converged = true;
|
||||
@@ -171,6 +222,8 @@ async function runTask(task: Task): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
stopCancelWatcher();
|
||||
|
||||
if (converged) {
|
||||
try {
|
||||
const finalTask = await getTask(task.id);
|
||||
|
||||
@@ -1,8 +1,77 @@
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { db } from "./db";
|
||||
import { tasks as tasksTable, iterations as iterationsTable } from "@homelab/db";
|
||||
import {
|
||||
tasks as tasksTable,
|
||||
iterations as iterationsTable,
|
||||
orchestratorState as orchTable,
|
||||
} from "@homelab/db";
|
||||
import { Task, Iteration } from "./types";
|
||||
|
||||
// ─── ORCHESTRATOR STATE ─────────────────────────────────────
|
||||
|
||||
export interface OrchestratorState {
|
||||
running: boolean;
|
||||
currentTaskId: string | null;
|
||||
heartbeat: number | null;
|
||||
}
|
||||
|
||||
async function ensureOrchestratorRow(): Promise<void> {
|
||||
await db
|
||||
.insert(orchTable)
|
||||
.values({ id: "singleton", running: false })
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
export async function getOrchestratorState(): Promise<OrchestratorState> {
|
||||
await ensureOrchestratorRow();
|
||||
const [row] = await db.select().from(orchTable).where(eq(orchTable.id, "singleton"));
|
||||
return {
|
||||
running: row.running,
|
||||
currentTaskId: row.currentTaskId ?? null,
|
||||
heartbeat: row.heartbeat ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setOrchestratorRunning(running: boolean): Promise<void> {
|
||||
await ensureOrchestratorRow();
|
||||
await db
|
||||
.update(orchTable)
|
||||
.set({ running, updatedAt: new Date() })
|
||||
.where(eq(orchTable.id, "singleton"));
|
||||
}
|
||||
|
||||
export async function setOrchestratorCurrentTask(taskId: string | null): Promise<void> {
|
||||
await db
|
||||
.update(orchTable)
|
||||
.set({ currentTaskId: taskId, heartbeat: Date.now(), updatedAt: new Date() })
|
||||
.where(eq(orchTable.id, "singleton"));
|
||||
}
|
||||
|
||||
export async function updateOrchestratorHeartbeat(): Promise<void> {
|
||||
await db
|
||||
.update(orchTable)
|
||||
.set({ heartbeat: Date.now(), updatedAt: new Date() })
|
||||
.where(eq(orchTable.id, "singleton"));
|
||||
}
|
||||
|
||||
// ─── TASK CANCEL FLAG ───────────────────────────────────────
|
||||
|
||||
export async function requestTaskCancel(taskId: string): Promise<boolean> {
|
||||
const result = await db
|
||||
.update(tasksTable)
|
||||
.set({ cancelRequested: true, updatedAt: new Date() })
|
||||
.where(and(eq(tasksTable.id, taskId), eq(tasksTable.status, "running")));
|
||||
return (result as unknown as { rowCount: number }).rowCount > 0;
|
||||
}
|
||||
|
||||
export async function isTaskCancelRequested(taskId: string): Promise<boolean> {
|
||||
const [row] = await db
|
||||
.select({ cancelRequested: tasksTable.cancelRequested })
|
||||
.from(tasksTable)
|
||||
.where(eq(tasksTable.id, taskId));
|
||||
return row?.cancelRequested ?? false;
|
||||
}
|
||||
|
||||
function rowToTask(
|
||||
row: typeof tasksTable.$inferSelect,
|
||||
iters: (typeof iterationsTable.$inferSelect)[],
|
||||
|
||||
95
apps/harness/src/lib/template.ts
Normal file
95
apps/harness/src/lib/template.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
// Template variable extraction and substitution for event-driven tasks.
|
||||
|
||||
export interface ParsedEvent {
|
||||
eventType: string;
|
||||
repo: string;
|
||||
repoName: string;
|
||||
owner: string;
|
||||
sha: string;
|
||||
shaShort: string;
|
||||
branch: string;
|
||||
state: string;
|
||||
context: string;
|
||||
targetUrl: string;
|
||||
commitMessage: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export function extractVariables(
|
||||
eventType: string,
|
||||
payload: Record<string, unknown>,
|
||||
): ParsedEvent {
|
||||
const repository = payload.repository as Record<string, unknown> | undefined;
|
||||
const owner = repository?.owner as Record<string, unknown> | undefined;
|
||||
const commit = payload.commit as Record<string, unknown> | undefined;
|
||||
const branches = payload.branches as { name: string }[] | undefined;
|
||||
const sha = String(payload.sha || "");
|
||||
|
||||
return {
|
||||
eventType,
|
||||
repo: String(repository?.full_name || ""),
|
||||
repoName: String(repository?.name || ""),
|
||||
owner: String(owner?.login || ""),
|
||||
sha,
|
||||
shaShort: sha.slice(0, 7),
|
||||
branch: branches?.[0]?.name || "",
|
||||
state: String(payload.state || ""),
|
||||
context: String(payload.context || ""),
|
||||
targetUrl: String(payload.target_url || ""),
|
||||
commitMessage: String(commit?.message || payload.description || ""),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderTemplate(
|
||||
template: string,
|
||||
vars: ParsedEvent,
|
||||
): string {
|
||||
const map: Record<string, string> = {
|
||||
repo: vars.repo,
|
||||
repo_name: vars.repoName,
|
||||
owner: vars.owner,
|
||||
sha: vars.sha,
|
||||
sha_short: vars.shaShort,
|
||||
branch: vars.branch,
|
||||
state: vars.state,
|
||||
context: vars.context,
|
||||
target_url: vars.targetUrl,
|
||||
commit_message: vars.commitMessage,
|
||||
timestamp: vars.timestamp,
|
||||
};
|
||||
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => map[key] ?? match);
|
||||
}
|
||||
|
||||
export function renderTaskTemplate(
|
||||
template: {
|
||||
slug: string;
|
||||
goal: string;
|
||||
project: string;
|
||||
gitProvider?: string;
|
||||
gitBaseUrl?: string;
|
||||
agentId: string;
|
||||
maxIterations: number;
|
||||
criteria: { label: string; target: string }[];
|
||||
constraints: string[];
|
||||
knowledgeRefs: string[];
|
||||
},
|
||||
vars: ParsedEvent,
|
||||
) {
|
||||
return {
|
||||
slug: renderTemplate(template.slug, vars),
|
||||
goal: renderTemplate(template.goal, vars),
|
||||
project: renderTemplate(template.project, vars),
|
||||
gitProvider: template.gitProvider as "github" | "gitlab" | "gitea" | undefined,
|
||||
gitBaseUrl: template.gitBaseUrl,
|
||||
agentId: template.agentId,
|
||||
maxIterations: template.maxIterations,
|
||||
criteria: template.criteria.map(c => ({
|
||||
label: renderTemplate(c.label, vars),
|
||||
target: renderTemplate(c.target, vars),
|
||||
})),
|
||||
constraints: template.constraints.map(c => renderTemplate(c, vars)),
|
||||
knowledgeRefs: template.knowledgeRefs.map(k => renderTemplate(k, vars)),
|
||||
};
|
||||
}
|
||||
383
apps/harness/src/mcp-server.ts
Normal file
383
apps/harness/src/mcp-server.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Harness MCP Server
|
||||
*
|
||||
* Stdio MCP server that exposes harness knowledge management and task
|
||||
* orchestration tools to agents spawned by the harness orchestrator.
|
||||
*
|
||||
* Environment variables:
|
||||
* DATABASE_URL — Postgres connection string (required)
|
||||
* HARNESS_KNOWLEDGE_DIR — Path to knowledge documents directory
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import postgres from "postgres";
|
||||
import {
|
||||
tasks as tasksTable,
|
||||
iterations as iterationsTable,
|
||||
agentConfigs as agentTable,
|
||||
curatedModels as modelsTable,
|
||||
orchestratorState as orchTable,
|
||||
} from "@homelab/db";
|
||||
import { readFile, writeFile, readdir, mkdir, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
// ── Database ────────────────────────────────────────────────────
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
console.error("DATABASE_URL is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = postgres(connectionString);
|
||||
const db = drizzle(client);
|
||||
|
||||
// ── Knowledge dir ───────────────────────────────────────────────
|
||||
|
||||
const KNOWLEDGE_DIR = process.env.HARNESS_KNOWLEDGE_DIR || "";
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
function taskSummary(row: typeof tasksTable.$inferSelect) {
|
||||
return {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
goal: row.goal,
|
||||
status: row.status,
|
||||
project: row.project,
|
||||
iteration: row.iteration,
|
||||
maxIterations: row.maxIterations,
|
||||
startedAt: row.startedAt,
|
||||
completedAt: row.completedAt,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Server ──────────────────────────────────────────────────────
|
||||
|
||||
const server = new McpServer({
|
||||
name: "harness",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// ── Knowledge Tools ─────────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
"knowledge_list",
|
||||
"List all knowledge documents available in the harness knowledge base",
|
||||
async () => {
|
||||
if (!KNOWLEDGE_DIR) {
|
||||
return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }] };
|
||||
}
|
||||
try {
|
||||
const entries = await readdir(KNOWLEDGE_DIR, { withFileTypes: true });
|
||||
const files = entries
|
||||
.filter((e) => e.isFile())
|
||||
.map((e) => e.name);
|
||||
return {
|
||||
content: [{ type: "text", text: files.length > 0 ? files.join("\n") : "(empty)" }],
|
||||
};
|
||||
} catch (err) {
|
||||
return { content: [{ type: "text", text: `Error listing knowledge dir: ${err}` }], isError: true };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"knowledge_read",
|
||||
"Read a specific knowledge document by filename",
|
||||
{ filename: z.string().describe("Filename of the knowledge document to read") },
|
||||
async ({ filename }) => {
|
||||
if (!KNOWLEDGE_DIR) {
|
||||
return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }], isError: true };
|
||||
}
|
||||
const filePath = path.resolve(KNOWLEDGE_DIR, filename);
|
||||
// Prevent path traversal
|
||||
if (!filePath.startsWith(path.resolve(KNOWLEDGE_DIR))) {
|
||||
return { content: [{ type: "text", text: "Invalid path" }], isError: true };
|
||||
}
|
||||
try {
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
return { content: [{ type: "text", text: content }] };
|
||||
} catch (err) {
|
||||
return { content: [{ type: "text", text: `Error reading ${filename}: ${err}` }], isError: true };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"knowledge_write",
|
||||
"Create or update a knowledge document in the harness knowledge base",
|
||||
{
|
||||
filename: z.string().describe("Filename for the knowledge document (e.g. 'findings.md')"),
|
||||
content: z.string().describe("Content to write to the knowledge document"),
|
||||
},
|
||||
async ({ filename, content }) => {
|
||||
if (!KNOWLEDGE_DIR) {
|
||||
return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }], isError: true };
|
||||
}
|
||||
const filePath = path.resolve(KNOWLEDGE_DIR, filename);
|
||||
if (!filePath.startsWith(path.resolve(KNOWLEDGE_DIR))) {
|
||||
return { content: [{ type: "text", text: "Invalid path" }], isError: true };
|
||||
}
|
||||
try {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, content, "utf-8");
|
||||
return { content: [{ type: "text", text: `Wrote ${filename} (${content.length} bytes)` }] };
|
||||
} catch (err) {
|
||||
return { content: [{ type: "text", text: `Error writing ${filename}: ${err}` }], isError: true };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"knowledge_search",
|
||||
"Search across all knowledge documents for a text pattern (case-insensitive substring match)",
|
||||
{ query: z.string().describe("Text to search for across all knowledge documents") },
|
||||
async ({ query }) => {
|
||||
if (!KNOWLEDGE_DIR) {
|
||||
return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }], isError: true };
|
||||
}
|
||||
try {
|
||||
const entries = await readdir(KNOWLEDGE_DIR, { withFileTypes: true });
|
||||
const files = entries.filter((e) => e.isFile());
|
||||
const results: string[] = [];
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(KNOWLEDGE_DIR, file.name);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const matches = lines
|
||||
.map((line, i) => ({ line, lineNum: i + 1 }))
|
||||
.filter(({ line }) => line.toLowerCase().includes(lowerQuery));
|
||||
|
||||
if (matches.length > 0) {
|
||||
results.push(
|
||||
`## ${file.name}\n` +
|
||||
matches
|
||||
.slice(0, 10)
|
||||
.map(({ line, lineNum }) => ` L${lineNum}: ${line.trim()}`)
|
||||
.join("\n") +
|
||||
(matches.length > 10 ? `\n ... and ${matches.length - 10} more matches` : ""),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: results.length > 0 ? results.join("\n\n") : `No matches for "${query}"`,
|
||||
}],
|
||||
};
|
||||
} catch (err) {
|
||||
return { content: [{ type: "text", text: `Error searching: ${err}` }], isError: true };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ── Task Tools ──────────────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
"task_list",
|
||||
"List all harness tasks with their current status and evaluation results",
|
||||
async () => {
|
||||
const rows = await db.select().from(tasksTable);
|
||||
const tasks = rows.map(taskSummary);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"task_get",
|
||||
"Get full details for a harness task including iteration history and evaluations",
|
||||
{ taskId: z.string().describe("Task ID to look up") },
|
||||
async ({ taskId }) => {
|
||||
const [taskRow] = await db.select().from(tasksTable).where(eq(tasksTable.id, taskId));
|
||||
if (!taskRow) {
|
||||
return { content: [{ type: "text", text: `Task ${taskId} not found` }], isError: true };
|
||||
}
|
||||
const iters = await db
|
||||
.select()
|
||||
.from(iterationsTable)
|
||||
.where(eq(iterationsTable.taskId, taskId));
|
||||
|
||||
const result = {
|
||||
...taskSummary(taskRow),
|
||||
spec: taskRow.spec,
|
||||
evals: taskRow.evals,
|
||||
pr: taskRow.pr,
|
||||
iterations: iters
|
||||
.sort((a, b) => a.n - b.n)
|
||||
.map((i) => ({
|
||||
n: i.n,
|
||||
status: i.status,
|
||||
diagnosis: i.diagnosis,
|
||||
evals: i.evals,
|
||||
diffStats: i.diffStats,
|
||||
agentOutput: i.agentOutput ? i.agentOutput.slice(-4000) : null,
|
||||
startedAt: i.startedAt,
|
||||
completedAt: i.completedAt,
|
||||
})),
|
||||
};
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"task_create",
|
||||
"Create a new harness task. The task will be created in 'pending' status and can be started with task_start.",
|
||||
{
|
||||
slug: z.string().describe("Unique short identifier for the task (e.g. 'fix-auth-bug')"),
|
||||
goal: z.string().describe("High-level description of what the task should accomplish"),
|
||||
project: z.string().describe("Repository in 'owner/repo' format"),
|
||||
agentId: z.string().describe("ID of the agent configuration to use"),
|
||||
maxIterations: z.number().optional().describe("Maximum iterations before giving up (default: 6)"),
|
||||
criteria: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().describe("Criterion name"),
|
||||
target: z.string().describe("Evaluation target DSL (e.g. 'exitCode:0', 'filesChanged:>0')"),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe("Success criteria for evaluation"),
|
||||
constraints: z.array(z.string()).optional().describe("Implementation constraints"),
|
||||
knowledgeRefs: z.array(z.string()).optional().describe("Knowledge document filenames to include in prompt"),
|
||||
gitProvider: z.enum(["github", "gitlab", "gitea"]).optional().describe("Git provider (default: github)"),
|
||||
gitBaseUrl: z.string().optional().describe("Base URL for the git provider API"),
|
||||
},
|
||||
async (args) => {
|
||||
const spec = {
|
||||
slug: args.slug,
|
||||
goal: args.goal,
|
||||
project: args.project,
|
||||
agentId: args.agentId,
|
||||
maxIterations: args.maxIterations ?? 6,
|
||||
criteria: args.criteria ?? [],
|
||||
constraints: args.constraints ?? [],
|
||||
knowledgeRefs: args.knowledgeRefs ?? [],
|
||||
gitProvider: args.gitProvider,
|
||||
gitBaseUrl: args.gitBaseUrl,
|
||||
};
|
||||
|
||||
const taskId = `task-${Date.now()}`;
|
||||
await db.insert(tasksTable).values({
|
||||
id: taskId,
|
||||
slug: spec.slug,
|
||||
goal: spec.goal,
|
||||
project: spec.project,
|
||||
status: "pending",
|
||||
iteration: 0,
|
||||
maxIterations: spec.maxIterations,
|
||||
startedAt: null,
|
||||
evals: {},
|
||||
spec,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ id: taskId, status: "pending", slug: spec.slug }) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"task_start",
|
||||
"Ensure the orchestrator is running so it will pick up pending tasks. Sets orchestrator state to running in the database.",
|
||||
async () => {
|
||||
// Ensure the singleton row exists
|
||||
await db
|
||||
.insert(orchTable)
|
||||
.values({ id: "singleton", running: false })
|
||||
.onConflictDoNothing();
|
||||
// Set running
|
||||
await db
|
||||
.update(orchTable)
|
||||
.set({ running: true, updatedAt: new Date() })
|
||||
.where(eq(orchTable.id, "singleton"));
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Orchestrator set to running — pending tasks will be picked up" }) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"task_stop",
|
||||
"Request cancellation of a running harness task. Sets cancel_requested flag which the orchestrator polls.",
|
||||
{ taskId: z.string().describe("ID of the running task to cancel") },
|
||||
async ({ taskId }) => {
|
||||
const result = await db
|
||||
.update(tasksTable)
|
||||
.set({ cancelRequested: true, updatedAt: new Date() })
|
||||
.where(and(eq(tasksTable.id, taskId), eq(tasksTable.status, "running")));
|
||||
|
||||
const rowCount = (result as unknown as { rowCount: number }).rowCount;
|
||||
if (rowCount === 0) {
|
||||
return { content: [{ type: "text", text: `Task ${taskId} is not running or not found` }], isError: true };
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Cancellation requested" }) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── Agent & Model Tools ─────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
"agent_list",
|
||||
"List all configured agent runtimes (agent configs with runtime, model, and provider)",
|
||||
async () => {
|
||||
const rows = await db.select().from(agentTable);
|
||||
const agents = rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
runtime: r.runtime,
|
||||
modelId: r.modelId,
|
||||
provider: r.provider,
|
||||
}));
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(agents, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"model_list",
|
||||
"List available AI models with pricing information",
|
||||
async () => {
|
||||
const rows = await db.select().from(modelsTable).where(eq(modelsTable.enabled, true));
|
||||
const models = rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
provider: r.provider,
|
||||
contextWindow: r.contextWindow,
|
||||
costPer1kInput: r.costPer1kInput,
|
||||
costPer1kOutput: r.costPer1kOutput,
|
||||
}));
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(models, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── Start ───────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Harness MCP server failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
43
packages/db/drizzle/0001_stale_pride.sql
Normal file
43
packages/db/drizzle/0001_stale_pride.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
CREATE TABLE IF NOT EXISTS "harness_event_log" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"trigger_id" text NOT NULL,
|
||||
"delivery_id" text NOT NULL,
|
||||
"event_type" text NOT NULL,
|
||||
"repo" text NOT NULL,
|
||||
"commit_sha" text,
|
||||
"branch" text,
|
||||
"status" text DEFAULT 'received' NOT NULL,
|
||||
"task_id" text,
|
||||
"skip_reason" text,
|
||||
"error" text,
|
||||
"payload" jsonb NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "harness_event_log_delivery_id_unique" UNIQUE("delivery_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "harness_event_triggers" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"event_type" text NOT NULL,
|
||||
"repo_filter" text,
|
||||
"state_filter" text,
|
||||
"context_filter" text,
|
||||
"task_template" jsonb NOT NULL,
|
||||
"consecutive_failures" integer DEFAULT 0 NOT NULL,
|
||||
"max_consecutive_failures" integer DEFAULT 3 NOT NULL,
|
||||
"disabled_reason" text,
|
||||
"webhook_secret" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "harness_orchestrator" (
|
||||
"id" text PRIMARY KEY DEFAULT 'singleton' NOT NULL,
|
||||
"running" boolean DEFAULT false NOT NULL,
|
||||
"current_task_id" text,
|
||||
"heartbeat" bigint,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "harness_tasks" ADD COLUMN "cancel_requested" boolean DEFAULT false NOT NULL;
|
||||
775
packages/db/drizzle/meta/0001_snapshot.json
Normal file
775
packages/db/drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,775 @@
|
||||
{
|
||||
"id": "e84e33a6-8185-4839-ad18-37149f19eb32",
|
||||
"prevId": "629b632c-cf1f-46ff-bb2f-e47d2343ddbd",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.harness_agent_configs": {
|
||||
"name": "harness_agent_configs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"runtime": {
|
||||
"name": "runtime",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"model_id": {
|
||||
"name": "model_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "max_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"env": {
|
||||
"name": "env",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_credentials": {
|
||||
"name": "harness_credentials",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_curated_models": {
|
||||
"name": "harness_curated_models",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"context_window": {
|
||||
"name": "context_window",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost_per_1k_input": {
|
||||
"name": "cost_per_1k_input",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost_per_1k_output": {
|
||||
"name": "cost_per_1k_output",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_event_log": {
|
||||
"name": "harness_event_log",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"trigger_id": {
|
||||
"name": "trigger_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"delivery_id": {
|
||||
"name": "delivery_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"event_type": {
|
||||
"name": "event_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"repo": {
|
||||
"name": "repo",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"commit_sha": {
|
||||
"name": "commit_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'received'"
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"skip_reason": {
|
||||
"name": "skip_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"harness_event_log_delivery_id_unique": {
|
||||
"name": "harness_event_log_delivery_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"delivery_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_event_triggers": {
|
||||
"name": "harness_event_triggers",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"event_type": {
|
||||
"name": "event_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"repo_filter": {
|
||||
"name": "repo_filter",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state_filter": {
|
||||
"name": "state_filter",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"context_filter": {
|
||||
"name": "context_filter",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_template": {
|
||||
"name": "task_template",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"consecutive_failures": {
|
||||
"name": "consecutive_failures",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"max_consecutive_failures": {
|
||||
"name": "max_consecutive_failures",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 3
|
||||
},
|
||||
"disabled_reason": {
|
||||
"name": "disabled_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"webhook_secret": {
|
||||
"name": "webhook_secret",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_iterations": {
|
||||
"name": "harness_iterations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"n": {
|
||||
"name": "n",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"diagnosis": {
|
||||
"name": "diagnosis",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"agent_output": {
|
||||
"name": "agent_output",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"evals": {
|
||||
"name": "evals",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"diff_stats": {
|
||||
"name": "diff_stats",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_model_usage": {
|
||||
"name": "harness_model_usage",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"model_id": {
|
||||
"name": "model_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_slug": {
|
||||
"name": "task_slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration_ms": {
|
||||
"name": "duration_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_orchestrator": {
|
||||
"name": "harness_orchestrator",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "'singleton'"
|
||||
},
|
||||
"running": {
|
||||
"name": "running",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"current_task_id": {
|
||||
"name": "current_task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"heartbeat": {
|
||||
"name": "heartbeat",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_tasks": {
|
||||
"name": "harness_tasks",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"goal": {
|
||||
"name": "goal",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"max_iterations": {
|
||||
"name": "max_iterations",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 6
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"project": {
|
||||
"name": "project",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'—'"
|
||||
},
|
||||
"evals": {
|
||||
"name": "evals",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"pr": {
|
||||
"name": "pr",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cancel_requested": {
|
||||
"name": "cancel_requested",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1774124029586,
|
||||
"tag": "0000_sparkling_gressill",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1774127485727,
|
||||
"tag": "0001_stale_pride",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -75,6 +75,16 @@ export const agentConfigs = pgTable("harness_agent_configs", {
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ─── HARNESS: ORCHESTRATOR STATE ──────────────────────────
|
||||
|
||||
export const orchestratorState = pgTable("harness_orchestrator", {
|
||||
id: text("id").primaryKey().default("singleton"),
|
||||
running: boolean("running").default(false).notNull(),
|
||||
currentTaskId: text("current_task_id"),
|
||||
heartbeat: bigint("heartbeat", { mode: "number" }),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ─── HARNESS: TASKS ────────────────────────────────────────
|
||||
|
||||
export const tasks = pgTable("harness_tasks", {
|
||||
@@ -89,6 +99,7 @@ export const tasks = pgTable("harness_tasks", {
|
||||
project: text("project").notNull().default("—"),
|
||||
evals: jsonb("evals").$type<Record<string, unknown>>().default({}).notNull(),
|
||||
pr: jsonb("pr").$type<{ number: number; title: string; status: string }>(),
|
||||
cancelRequested: boolean("cancel_requested").default(false).notNull(),
|
||||
spec: jsonb("spec").$type<{
|
||||
slug: string;
|
||||
goal: string;
|
||||
@@ -117,3 +128,60 @@ export const iterations = pgTable("harness_iterations", {
|
||||
startedAt: bigint("started_at", { mode: "number" }),
|
||||
completedAt: bigint("completed_at", { mode: "number" }),
|
||||
});
|
||||
|
||||
// ─── HARNESS: EVENT TRIGGERS ───────────────────────────────
|
||||
|
||||
export const eventTriggers = pgTable("harness_event_triggers", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
enabled: boolean("enabled").default(true).notNull(),
|
||||
|
||||
// Matching conditions (all AND'd, null = any)
|
||||
eventType: text("event_type").notNull(), // "status" | "*"
|
||||
repoFilter: text("repo_filter"), // glob: "lazorgurl/*" or exact "lazorgurl/homelab"
|
||||
stateFilter: text("state_filter"), // "failure" | "error" | null
|
||||
contextFilter: text("context_filter"), // substring match on CI context, null = any
|
||||
|
||||
// Task template with {{variable}} placeholders
|
||||
taskTemplate: jsonb("task_template").$type<{
|
||||
slug: string;
|
||||
goal: string;
|
||||
project: string;
|
||||
gitProvider?: string;
|
||||
gitBaseUrl?: string;
|
||||
agentId: string;
|
||||
maxIterations: number;
|
||||
criteria: { label: string; target: string }[];
|
||||
constraints: string[];
|
||||
knowledgeRefs: string[];
|
||||
}>().notNull(),
|
||||
|
||||
// Circuit breaker
|
||||
consecutiveFailures: integer("consecutive_failures").default(0).notNull(),
|
||||
maxConsecutiveFailures: integer("max_consecutive_failures").default(3).notNull(),
|
||||
disabledReason: text("disabled_reason"),
|
||||
|
||||
// Webhook auth (per-trigger override, falls back to GITEA_WEBHOOK_SECRET env)
|
||||
webhookSecret: text("webhook_secret"),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ─── HARNESS: EVENT LOG ────────────────────────────────────
|
||||
|
||||
export const eventLog = pgTable("harness_event_log", {
|
||||
id: serial("id").primaryKey(),
|
||||
triggerId: text("trigger_id").notNull(),
|
||||
deliveryId: text("delivery_id").notNull().unique(), // X-Gitea-Delivery header (dedup)
|
||||
eventType: text("event_type").notNull(),
|
||||
repo: text("repo").notNull(),
|
||||
commitSha: text("commit_sha"),
|
||||
branch: text("branch"),
|
||||
status: text("status").notNull().default("received"), // received | task_created | skipped | error
|
||||
taskId: text("task_id"),
|
||||
skipReason: text("skip_reason"),
|
||||
error: text("error"),
|
||||
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
386
pnpm-lock.yaml
generated
386
pnpm-lock.yaml
generated
@@ -75,6 +75,9 @@ importers:
|
||||
'@homelab/db':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/db
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.27.1
|
||||
version: 1.27.1(zod@4.3.6)
|
||||
'@xterm/addon-fit':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0
|
||||
@@ -99,12 +102,18 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
tsx:
|
||||
specifier: ^4.19.0
|
||||
version: 4.21.0
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
yaml:
|
||||
specifier: ^2.7.0
|
||||
version: 2.8.2
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.10.0
|
||||
@@ -118,6 +127,9 @@ importers:
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
esbuild:
|
||||
specifier: ^0.27.4
|
||||
version: 0.27.4
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
@@ -698,6 +710,12 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
'@hono/node-server@1.19.11':
|
||||
resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -870,6 +888,16 @@ packages:
|
||||
'@js-sdsl/ordered-map@4.4.2':
|
||||
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.27.1':
|
||||
resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@cfworker/json-schema': ^4.1.1
|
||||
zod: ^3.25 || ^4.0
|
||||
peerDependenciesMeta:
|
||||
'@cfworker/json-schema':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@@ -1937,6 +1965,10 @@ packages:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
acorn-import-attributes@1.9.5:
|
||||
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
||||
peerDependencies:
|
||||
@@ -1956,9 +1988,20 @@ packages:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-formats@3.0.1:
|
||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
|
||||
ajv@6.14.0:
|
||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2049,6 +2092,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
||||
body-parser@2.2.2:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
@@ -2139,6 +2186,10 @@ packages:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
content-disposition@1.0.1:
|
||||
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
content-type@1.0.5:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2146,6 +2197,10 @@ packages:
|
||||
cookie-signature@1.0.7:
|
||||
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
|
||||
cookie@0.7.2:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2542,10 +2597,28 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
eventsource-parser@3.0.6:
|
||||
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
eventsource@3.0.7:
|
||||
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
express-rate-limit@8.3.1:
|
||||
resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
|
||||
express@4.22.1:
|
||||
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
express@5.2.1:
|
||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
@@ -2562,6 +2635,9 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
fastq@1.20.1:
|
||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||
|
||||
@@ -2590,6 +2666,10 @@ packages:
|
||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
finalhandler@2.1.1:
|
||||
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
|
||||
find-up@5.0.0:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2623,6 +2703,10 @@ packages:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -2723,6 +2807,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hono@4.12.8:
|
||||
resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2735,6 +2823,10 @@ packages:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.7.2:
|
||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -2762,6 +2854,10 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -2837,6 +2933,9 @@ packages:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
is-regex@1.2.1:
|
||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2887,6 +2986,9 @@ packages:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jose@6.2.2:
|
||||
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2907,6 +3009,12 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema-typed@8.0.2:
|
||||
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -3041,9 +3149,17 @@ packages:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
merge-descriptors@1.0.3:
|
||||
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
||||
|
||||
merge-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -3060,10 +3176,18 @@ packages:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.2:
|
||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mime@1.6.0:
|
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3111,6 +3235,10 @@ packages:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
next@15.5.14:
|
||||
resolution: {integrity: sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
@@ -3191,6 +3319,9 @@ packages:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -3229,6 +3360,9 @@ packages:
|
||||
path-to-regexp@0.1.12:
|
||||
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
|
||||
|
||||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -3271,6 +3405,10 @@ packages:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pkce-challenge@5.0.1:
|
||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
@@ -3369,6 +3507,10 @@ packages:
|
||||
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
raw-body@3.0.2:
|
||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
react-dom@19.2.4:
|
||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||
peerDependencies:
|
||||
@@ -3401,6 +3543,10 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-in-the-middle@8.0.1:
|
||||
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||
@@ -3435,6 +3581,10 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
router@2.2.0:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
@@ -3476,10 +3626,18 @@ packages:
|
||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
send@1.2.1:
|
||||
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
serve-static@1.16.3:
|
||||
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
serve-static@2.2.1:
|
||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3709,6 +3867,10 @@ packages:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
type-is@2.0.1:
|
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3791,6 +3953,9 @@ packages:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
ws@8.20.0:
|
||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -3828,6 +3993,14 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
@@ -4131,6 +4304,10 @@ snapshots:
|
||||
protobufjs: 7.5.4
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hono/node-server@1.19.11(hono@4.12.8)':
|
||||
dependencies:
|
||||
hono: 4.12.8
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
@@ -4260,6 +4437,28 @@ snapshots:
|
||||
|
||||
'@js-sdsl/ordered-map@4.4.2': {}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.11(hono@4.12.8)
|
||||
ajv: 8.18.0
|
||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.6
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.3.1(express@5.2.1)
|
||||
hono: 4.12.8
|
||||
jose: 6.2.2
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
raw-body: 3.0.2
|
||||
zod: 4.3.6
|
||||
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.9.1
|
||||
@@ -5453,6 +5652,11 @@ snapshots:
|
||||
mime-types: 2.1.35
|
||||
negotiator: 0.6.3
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
negotiator: 1.0.0
|
||||
|
||||
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
@@ -5465,6 +5669,10 @@ snapshots:
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-formats@3.0.1(ajv@8.18.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.18.0
|
||||
|
||||
ajv@6.14.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -5472,6 +5680,13 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ajv@8.18.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
@@ -5590,6 +5805,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
body-parser@2.2.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 4.4.3
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
on-finished: 2.4.1
|
||||
qs: 6.14.2
|
||||
raw-body: 3.0.2
|
||||
type-is: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@@ -5672,10 +5901,14 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
content-disposition@1.0.1: {}
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
cookie-signature@1.0.7: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
cors@2.8.6:
|
||||
@@ -6184,6 +6417,17 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
|
||||
eventsource@3.0.7:
|
||||
dependencies:
|
||||
eventsource-parser: 3.0.6
|
||||
|
||||
express-rate-limit@8.3.1(express@5.2.1):
|
||||
dependencies:
|
||||
express: 5.2.1
|
||||
ip-address: 10.1.0
|
||||
|
||||
express@4.22.1:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
@@ -6220,6 +6464,39 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
express@5.2.1:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
body-parser: 2.2.2
|
||||
content-disposition: 1.0.1
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.2.2
|
||||
debug: 4.4.3
|
||||
depd: 2.0.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 2.1.1
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.1
|
||||
merge-descriptors: 2.0.0
|
||||
mime-types: 3.0.2
|
||||
on-finished: 2.4.1
|
||||
once: 1.4.0
|
||||
parseurl: 1.3.3
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.14.2
|
||||
range-parser: 1.2.1
|
||||
router: 2.2.0
|
||||
send: 1.2.1
|
||||
serve-static: 2.2.1
|
||||
statuses: 2.0.2
|
||||
type-is: 2.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
@@ -6236,6 +6513,8 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fastq@1.20.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@@ -6269,6 +6548,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
finalhandler@2.1.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
find-up@5.0.0:
|
||||
dependencies:
|
||||
locate-path: 6.0.0
|
||||
@@ -6301,6 +6591,8 @@ snapshots:
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -6408,6 +6700,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hono@4.12.8: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@@ -6427,6 +6721,10 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.7.2:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
@@ -6453,6 +6751,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
@@ -6530,6 +6830,8 @@ snapshots:
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
is-regex@1.2.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -6584,6 +6886,8 @@ snapshots:
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jose@6.2.2: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -6600,6 +6904,10 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema-typed@8.0.2: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@1.0.2:
|
||||
@@ -6705,8 +7013,12 @@ snapshots:
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
merge-descriptors@1.0.3: {}
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
methods@1.1.2: {}
|
||||
@@ -6718,10 +7030,16 @@ snapshots:
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime-types@3.0.2:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
mime@1.6.0: {}
|
||||
|
||||
minimatch@10.2.4:
|
||||
@@ -6761,6 +7079,8 @@ snapshots:
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
next@15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 15.5.14
|
||||
@@ -6854,6 +7174,10 @@ snapshots:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -6891,6 +7215,8 @@ snapshots:
|
||||
|
||||
path-to-regexp@0.1.12: {}
|
||||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
@@ -6940,6 +7266,8 @@ snapshots:
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkce-challenge@5.0.1: {}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
@@ -7032,6 +7360,13 @@ snapshots:
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
raw-body@3.0.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
unpipe: 1.0.0
|
||||
|
||||
react-dom@19.2.4(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -7067,6 +7402,8 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-in-the-middle@8.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -7128,6 +7465,16 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.59.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
router@2.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
depd: 2.0.0
|
||||
is-promise: 4.0.0
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 8.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
@@ -7181,6 +7528,22 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
send@1.2.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.1
|
||||
mime-types: 3.0.2
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@1.16.3:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
@@ -7190,6 +7553,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@2.2.1:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -7490,6 +7862,12 @@ snapshots:
|
||||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
type-is@2.0.1:
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
media-typer: 1.1.0
|
||||
mime-types: 3.0.2
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -7625,6 +8003,8 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@8.20.0: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
@@ -7646,3 +8026,9 @@ snapshots:
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
Reference in New Issue
Block a user