@jiggai/kitchen 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/channels.html +2 -2
- package/.next/server/app/channels.rsc +1 -1
- package/.next/server/app/channels.segments/_full.segment.rsc +1 -1
- package/.next/server/app/channels.segments/_head.segment.rsc +1 -1
- package/.next/server/app/channels.segments/_index.segment.rsc +1 -1
- package/.next/server/app/channels.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/channels.segments/channels/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/channels.segments/channels.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.html +1 -1
- package/.next/server/app/cron-jobs.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/_full.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/_index.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/cron-jobs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/cron-jobs.segment.rsc +1 -1
- package/.next/server/app/goals/new.html +2 -2
- package/.next/server/app/goals/new.rsc +1 -1
- package/.next/server/app/goals/new.segments/_full.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/_head.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/_index.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/goals/new/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/goals/new.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/goals.segment.rsc +1 -1
- package/.next/server/app/goals.html +1 -1
- package/.next/server/app/goals.rsc +1 -1
- package/.next/server/app/goals.segments/_full.segment.rsc +1 -1
- package/.next/server/app/goals.segments/_head.segment.rsc +1 -1
- package/.next/server/app/goals.segments/_index.segment.rsc +1 -1
- package/.next/server/app/goals.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/goals.segments/goals/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/goals.segments/goals.segment.rsc +1 -1
- package/.next/server/app/settings.html +1 -1
- package/.next/server/app/settings.rsc +1 -1
- package/.next/server/app/settings.segments/_full.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +2 -2
- package/package.json +1 -2
- package/src/app/HomeClient.tsx +0 -207
- package/src/app/agents/[agentId]/agent-editor-tabs.tsx +0 -298
- package/src/app/agents/[agentId]/agent-editor.tsx +0 -468
- package/src/app/agents/[agentId]/page.tsx +0 -32
- package/src/app/api/__tests__/agents-add-route.test.ts +0 -143
- package/src/app/api/__tests__/agents-file-route.test.ts +0 -117
- package/src/app/api/__tests__/agents-files-route.test.ts +0 -61
- package/src/app/api/__tests__/agents-id-route.test.ts +0 -104
- package/src/app/api/__tests__/agents-identity-route.test.ts +0 -92
- package/src/app/api/__tests__/agents-route.test.ts +0 -54
- package/src/app/api/__tests__/agents-skills-install-route.test.ts +0 -131
- package/src/app/api/__tests__/agents-skills-route.test.ts +0 -64
- package/src/app/api/__tests__/agents-update-route.test.ts +0 -95
- package/src/app/api/__tests__/channels-bindings-route.test.ts +0 -143
- package/src/app/api/__tests__/cron-delete-route.test.ts +0 -93
- package/src/app/api/__tests__/cron-job-route.test.ts +0 -78
- package/src/app/api/__tests__/cron-jobs-route.test.ts +0 -116
- package/src/app/api/__tests__/cron-recipe-installed-route.test.ts +0 -114
- package/src/app/api/__tests__/gateway-restart-route.test.ts +0 -36
- package/src/app/api/__tests__/goals-promote-route.test.ts +0 -200
- package/src/app/api/__tests__/goals-route.test.ts +0 -184
- package/src/app/api/__tests__/ids-check-route.test.ts +0 -188
- package/src/app/api/__tests__/marketplace-recipes-route.test.ts +0 -123
- package/src/app/api/__tests__/recipes-clone-route.test.ts +0 -221
- package/src/app/api/__tests__/recipes-delete-route.test.ts +0 -248
- package/src/app/api/__tests__/recipes-id-route.test.ts +0 -166
- package/src/app/api/__tests__/recipes-route.test.ts +0 -57
- package/src/app/api/__tests__/recipes-team-agents-route.test.ts +0 -135
- package/src/app/api/__tests__/scaffold-route.test.ts +0 -173
- package/src/app/api/__tests__/settings-cron-installation-route.test.ts +0 -82
- package/src/app/api/__tests__/skills-available-route.test.ts +0 -47
- package/src/app/api/__tests__/swarms-start-route.test.ts +0 -79
- package/src/app/api/__tests__/swarms-status-route.test.ts +0 -42
- package/src/app/api/__tests__/teams-file-route.test.ts +0 -129
- package/src/app/api/__tests__/teams-files-route.test.ts +0 -57
- package/src/app/api/__tests__/teams-meta-route.test.ts +0 -113
- package/src/app/api/__tests__/teams-orchestrator-install-route.test.ts +0 -66
- package/src/app/api/__tests__/teams-orchestrator-route.test.ts +0 -59
- package/src/app/api/__tests__/teams-remove-team-route.test.ts +0 -122
- package/src/app/api/__tests__/teams-skills-install-route.test.ts +0 -78
- package/src/app/api/__tests__/teams-skills-route.test.ts +0 -73
- package/src/app/api/__tests__/teams-workflow-runs-route.test.ts +0 -85
- package/src/app/api/__tests__/teams-workflows-route.test.ts +0 -110
- package/src/app/api/__tests__/tickets-move-route.test.ts +0 -60
- package/src/app/api/agents/[id]/route.ts +0 -83
- package/src/app/api/agents/add/route.ts +0 -114
- package/src/app/api/agents/file/route.ts +0 -45
- package/src/app/api/agents/files/route.ts +0 -23
- package/src/app/api/agents/identity/route.ts +0 -41
- package/src/app/api/agents/route.ts +0 -22
- package/src/app/api/agents/skills/install/route.ts +0 -34
- package/src/app/api/agents/skills/route.ts +0 -39
- package/src/app/api/agents/update/route.ts +0 -52
- package/src/app/api/channels/bindings/route.ts +0 -63
- package/src/app/api/cron/__tests__/helpers.test.ts +0 -164
- package/src/app/api/cron/delete/route.ts +0 -23
- package/src/app/api/cron/helpers.ts +0 -172
- package/src/app/api/cron/job/route.ts +0 -22
- package/src/app/api/cron/jobs/route.ts +0 -52
- package/src/app/api/cron/recipe-installed/route.ts +0 -65
- package/src/app/api/gateway/restart/route.ts +0 -20
- package/src/app/api/goals/[id]/promote/route.ts +0 -119
- package/src/app/api/goals/[id]/route.ts +0 -54
- package/src/app/api/goals/route.ts +0 -44
- package/src/app/api/ids/check/route.ts +0 -113
- package/src/app/api/marketplace/recipes/[slug]/route.ts +0 -16
- package/src/app/api/marketplace/recipes/route.ts +0 -22
- package/src/app/api/recipes/[id]/route.ts +0 -62
- package/src/app/api/recipes/clone/route.ts +0 -106
- package/src/app/api/recipes/custom-team/route.ts +0 -193
- package/src/app/api/recipes/delete/helpers.ts +0 -65
- package/src/app/api/recipes/delete/route.ts +0 -73
- package/src/app/api/recipes/route.ts +0 -21
- package/src/app/api/recipes/team-agents/__tests__/helpers.test.ts +0 -156
- package/src/app/api/recipes/team-agents/helpers.ts +0 -151
- package/src/app/api/recipes/team-agents/route.ts +0 -80
- package/src/app/api/scaffold/__tests__/helpers.test.ts +0 -186
- package/src/app/api/scaffold/helpers.ts +0 -214
- package/src/app/api/scaffold/route.ts +0 -95
- package/src/app/api/settings/cron-installation/route.ts +0 -58
- package/src/app/api/skills/available/route.ts +0 -23
- package/src/app/api/swarms/start/route.ts +0 -100
- package/src/app/api/swarms/status/route.ts +0 -31
- package/src/app/api/teams/[teamId]/tickets/assign/route.ts +0 -105
- package/src/app/api/teams/[teamId]/tickets/assignees/route.ts +0 -27
- package/src/app/api/teams/[teamId]/tickets/delete/route.ts +0 -55
- package/src/app/api/teams/[teamId]/tickets/move/route.ts +0 -70
- package/src/app/api/teams/[teamId]/tickets/move-to-goals/route.ts +0 -56
- package/src/app/api/teams/file/route.ts +0 -46
- package/src/app/api/teams/files/route.ts +0 -63
- package/src/app/api/teams/memory/route.ts +0 -250
- package/src/app/api/teams/meta/route.ts +0 -43
- package/src/app/api/teams/orchestrator/install/route.ts +0 -129
- package/src/app/api/teams/orchestrator/route.ts +0 -216
- package/src/app/api/teams/remove-team/route.ts +0 -37
- package/src/app/api/teams/skills/install/route.ts +0 -18
- package/src/app/api/teams/skills/route.ts +0 -25
- package/src/app/api/teams/workflow-runs/route.ts +0 -534
- package/src/app/api/teams/workflow-templates/route.ts +0 -71
- package/src/app/api/teams/workflows/route.ts +0 -55
- package/src/app/api/tickets/assign/route.ts +0 -94
- package/src/app/api/tickets/assignees/route.ts +0 -24
- package/src/app/api/tickets/move/route.ts +0 -69
- package/src/app/channels/channels-client.tsx +0 -271
- package/src/app/channels/page.tsx +0 -5
- package/src/app/cron-jobs/cron-jobs-client.tsx +0 -243
- package/src/app/cron-jobs/page.tsx +0 -34
- package/src/app/favicon.ico +0 -0
- package/src/app/global-error.tsx +0 -50
- package/src/app/globals.css +0 -153
- package/src/app/goals/[id]/goal-editor.tsx +0 -162
- package/src/app/goals/[id]/page.tsx +0 -6
- package/src/app/goals/goals-client.tsx +0 -201
- package/src/app/goals/new/page.tsx +0 -81
- package/src/app/goals/page.tsx +0 -10
- package/src/app/layout.tsx +0 -53
- package/src/app/manifest.ts +0 -15
- package/src/app/not-found.tsx +0 -8
- package/src/app/page.tsx +0 -33
- package/src/app/recipes/CreateAgentModal.tsx +0 -156
- package/src/app/recipes/CreateCustomTeamModal.tsx +0 -375
- package/src/app/recipes/CreateModalShell.tsx +0 -55
- package/src/app/recipes/CreateTeamModal.tsx +0 -91
- package/src/app/recipes/[id]/RecipeEditor/RecipeEditorCreateModal.tsx +0 -72
- package/src/app/recipes/[id]/RecipeEditor/RecipeEditorPanel.tsx +0 -216
- package/src/app/recipes/[id]/RecipeEditor/index.tsx +0 -271
- package/src/app/recipes/[id]/RecipeEditor/recipe-editor-utils.ts +0 -46
- package/src/app/recipes/[id]/RecipeEditor/types.ts +0 -52
- package/src/app/recipes/[id]/page.tsx +0 -37
- package/src/app/recipes/page.tsx +0 -101
- package/src/app/recipes/recipes-client.tsx +0 -620
- package/src/app/settings/page.tsx +0 -26
- package/src/app/settings/settings-client.tsx +0 -91
- package/src/app/teams/[teamId]/CloneTeamModal.tsx +0 -116
- package/src/app/teams/[teamId]/OrchestratorPanel.tsx +0 -255
- package/src/app/teams/[teamId]/OrchestratorSetupModal.tsx +0 -184
- package/src/app/teams/[teamId]/PublishChangesModal.tsx +0 -43
- package/src/app/teams/[teamId]/page.tsx +0 -49
- package/src/app/teams/[teamId]/team-editor/TeamAgentsTab.tsx +0 -145
- package/src/app/teams/[teamId]/team-editor/TeamCronTab.tsx +0 -72
- package/src/app/teams/[teamId]/team-editor/TeamFilesTab.tsx +0 -74
- package/src/app/teams/[teamId]/team-editor/TeamMemoryTab.tsx +0 -349
- package/src/app/teams/[teamId]/team-editor/TeamRecipeTab.tsx +0 -151
- package/src/app/teams/[teamId]/team-editor/TeamSkillsTab.tsx +0 -68
- package/src/app/teams/[teamId]/team-editor/index.tsx +0 -558
- package/src/app/teams/[teamId]/team-editor/team-editor-data.ts +0 -255
- package/src/app/teams/[teamId]/team-editor/team-editor-utils.ts +0 -78
- package/src/app/teams/[teamId]/team-editor/types.ts +0 -34
- package/src/app/teams/[teamId]/tickets/[ticket]/page.tsx +0 -35
- package/src/app/teams/[teamId]/tickets/page.tsx +0 -15
- package/src/app/teams/[teamId]/workflows/[workflowId]/WorkflowCanvas.tsx +0 -111
- package/src/app/teams/[teamId]/workflows/[workflowId]/page.tsx +0 -27
- package/src/app/teams/[teamId]/workflows/[workflowId]/workflows-editor-client.tsx +0 -1608
- package/src/app/teams/[teamId]/workflows/page.tsx +0 -40
- package/src/app/teams/[teamId]/workflows/workflows-client.tsx +0 -494
- package/src/app/tickets/TicketDetailClient.tsx +0 -147
- package/src/app/tickets/TicketsBoardClient.tsx +0 -200
- package/src/app/tickets/[ticket]/TicketAssignControl.tsx +0 -112
- package/src/app/tickets/[ticket]/page.tsx +0 -36
- package/src/app/tickets/page.tsx +0 -10
- package/src/components/AppShell.tsx +0 -286
- package/src/components/ConfirmationModal.tsx +0 -81
- package/src/components/DeleteEntityModal.tsx +0 -41
- package/src/components/ErrorBoundary.tsx +0 -70
- package/src/components/FileListWithOptionalToggle.tsx +0 -86
- package/src/components/GoalFormFields.tsx +0 -163
- package/src/components/ScaffoldOverlay.tsx +0 -78
- package/src/components/ThemeToggle.tsx +0 -53
- package/src/components/ToastProvider.tsx +0 -163
- package/src/components/__tests__/ConfirmationModal.test.tsx +0 -109
- package/src/components/__tests__/ErrorBoundary.test.tsx +0 -39
- package/src/components/__tests__/FileListWithOptionalToggle.test.tsx +0 -109
- package/src/components/__tests__/GoalFormFields.test.tsx +0 -117
- package/src/components/delete-modals.tsx +0 -59
- package/src/components/icons.tsx +0 -48
- package/src/lib/__tests__/agent-workspace.test.ts +0 -44
- package/src/lib/__tests__/agents.test.ts +0 -36
- package/src/lib/__tests__/api-route-helpers.test.ts +0 -188
- package/src/lib/__tests__/cron.test.ts +0 -45
- package/src/lib/__tests__/editor-utils.test.ts +0 -38
- package/src/lib/__tests__/errors.test.ts +0 -15
- package/src/lib/__tests__/exec.test.ts +0 -13
- package/src/lib/__tests__/fetch-json.test.ts +0 -118
- package/src/lib/__tests__/gateway.test.ts +0 -234
- package/src/lib/__tests__/goal-promote.test.ts +0 -39
- package/src/lib/__tests__/goals-client.test.ts +0 -26
- package/src/lib/__tests__/goals.test.ts +0 -275
- package/src/lib/__tests__/json.test.ts +0 -15
- package/src/lib/__tests__/kitchen-api.test.ts +0 -32
- package/src/lib/__tests__/marketplace.test.ts +0 -116
- package/src/lib/__tests__/openclaw.test.ts +0 -129
- package/src/lib/__tests__/paths.test.ts +0 -136
- package/src/lib/__tests__/poll.test.ts +0 -26
- package/src/lib/__tests__/recipe-clone.test.ts +0 -85
- package/src/lib/__tests__/recipe-team-agents.test.ts +0 -70
- package/src/lib/__tests__/recipes.test.ts +0 -199
- package/src/lib/__tests__/scaffold-client.test.ts +0 -106
- package/src/lib/__tests__/scaffold.test.ts +0 -64
- package/src/lib/__tests__/slugify.test.ts +0 -23
- package/src/lib/__tests__/tickets.test.ts +0 -158
- package/src/lib/__tests__/type-guards.test.ts +0 -18
- package/src/lib/__tests__/use-slugified-id.test.tsx +0 -120
- package/src/lib/agent-workspace.ts +0 -14
- package/src/lib/agents.ts +0 -17
- package/src/lib/api-route-helpers.ts +0 -157
- package/src/lib/cron.ts +0 -40
- package/src/lib/editor-utils.ts +0 -18
- package/src/lib/errors.ts +0 -7
- package/src/lib/exec.ts +0 -4
- package/src/lib/fetch-json.ts +0 -29
- package/src/lib/gateway.ts +0 -100
- package/src/lib/goal-promote.ts +0 -27
- package/src/lib/goals-client.ts +0 -69
- package/src/lib/goals.ts +0 -171
- package/src/lib/json.ts +0 -10
- package/src/lib/kitchen-api.ts +0 -19
- package/src/lib/marketplace.ts +0 -46
- package/src/lib/openclaw.ts +0 -59
- package/src/lib/paths.ts +0 -69
- package/src/lib/poll.ts +0 -18
- package/src/lib/recipe-clone.ts +0 -42
- package/src/lib/recipe-team-agents.ts +0 -30
- package/src/lib/recipes.ts +0 -95
- package/src/lib/scaffold-client.ts +0 -31
- package/src/lib/scaffold.ts +0 -37
- package/src/lib/slugify.ts +0 -25
- package/src/lib/swarms.ts +0 -25
- package/src/lib/tickets.ts +0 -192
- package/src/lib/type-guards.ts +0 -3
- package/src/lib/use-slugified-id.ts +0 -35
- package/src/lib/workflows/README.md +0 -11
- package/src/lib/workflows/__tests__/storage.test.ts +0 -129
- package/src/lib/workflows/__tests__/validate.test.ts +0 -92
- package/src/lib/workflows/api-handlers.ts +0 -35
- package/src/lib/workflows/readdir.ts +0 -23
- package/src/lib/workflows/runs-storage.ts +0 -59
- package/src/lib/workflows/runs-types.ts +0 -42
- package/src/lib/workflows/storage.ts +0 -70
- package/src/lib/workflows/templates/index.ts +0 -1
- package/src/lib/workflows/templates/marketing-cadence-v1.ts +0 -142
- package/src/lib/workflows/types.ts +0 -48
- package/src/lib/workflows/validate.ts +0 -92
- package/src/proxy.ts +0 -28
- /package/.next/static/{z86RoqzzXXrWnpi229zP6 → Jrrrm9HH5bKkSrQhe1j93}/_buildManifest.js +0 -0
- /package/.next/static/{z86RoqzzXXrWnpi229zP6 → Jrrrm9HH5bKkSrQhe1j93}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{z86RoqzzXXrWnpi229zP6 → Jrrrm9HH5bKkSrQhe1j93}/_ssgManifest.js +0 -0
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { NextResponse } from "next/server";
|
|
4
|
-
import { getWorkspaceRecipesDir } from "@/lib/paths";
|
|
5
|
-
|
|
6
|
-
function isValidId(id: string) {
|
|
7
|
-
// Keep consistent with common recipe/team id expectations.
|
|
8
|
-
return /^[a-z0-9][a-z0-9_-]{1,62}$/.test(id);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function yamlEscape(s: string) {
|
|
12
|
-
// Minimal YAML escaping for simple strings.
|
|
13
|
-
const trimmed = s.replace(/\r\n/g, "\n").trimEnd();
|
|
14
|
-
if (!trimmed) return "\"\"";
|
|
15
|
-
// If it's a safe bare string, keep it readable.
|
|
16
|
-
if (/^[A-Za-z0-9 _.,:;()\[\]{}\-+/@]+$/.test(trimmed) && !/[:#\n]/.test(trimmed)) {
|
|
17
|
-
return trimmed;
|
|
18
|
-
}
|
|
19
|
-
return JSON.stringify(trimmed);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function POST(req: Request) {
|
|
23
|
-
const body = (await req.json()) as {
|
|
24
|
-
dryRun?: boolean;
|
|
25
|
-
recipeId?: string;
|
|
26
|
-
teamId?: string;
|
|
27
|
-
name?: string;
|
|
28
|
-
description?: string;
|
|
29
|
-
roles?: Array<{ roleId?: string; displayName?: string }>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const dryRun = !!body.dryRun;
|
|
33
|
-
const recipeId = String(body.recipeId ?? body.teamId ?? "").trim();
|
|
34
|
-
const teamId = String(body.teamId ?? recipeId).trim();
|
|
35
|
-
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
36
|
-
const description = typeof body.description === "string" ? body.description.trim() : "";
|
|
37
|
-
const roles = Array.isArray(body.roles) ? body.roles : [];
|
|
38
|
-
|
|
39
|
-
if (!recipeId) return NextResponse.json({ ok: false, error: "recipeId is required" }, { status: 400 });
|
|
40
|
-
if (!teamId) return NextResponse.json({ ok: false, error: "teamId is required" }, { status: 400 });
|
|
41
|
-
if (!isValidId(recipeId)) {
|
|
42
|
-
return NextResponse.json(
|
|
43
|
-
{ ok: false, error: "Invalid recipeId. Use lowercase letters/numbers with - or _ (2-63 chars)." },
|
|
44
|
-
{ status: 400 },
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
if (!isValidId(teamId)) {
|
|
48
|
-
return NextResponse.json(
|
|
49
|
-
{ ok: false, error: "Invalid teamId. Use lowercase letters/numbers with - or _ (2-63 chars)." },
|
|
50
|
-
{ status: 400 },
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
if (!teamId.endsWith("-team")) {
|
|
54
|
-
return NextResponse.json(
|
|
55
|
-
{ ok: false, error: "teamId must end with -team" },
|
|
56
|
-
{ status: 400 },
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (roles.length < 1) {
|
|
61
|
-
return NextResponse.json({ ok: false, error: "Select at least one role/agent" }, { status: 400 });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const normalizedRoles = roles
|
|
65
|
-
.map((r) => ({
|
|
66
|
-
roleId: String(r.roleId ?? "").trim(),
|
|
67
|
-
displayName: typeof r.displayName === "string" ? r.displayName.trim() : "",
|
|
68
|
-
}))
|
|
69
|
-
.filter((r) => r.roleId);
|
|
70
|
-
|
|
71
|
-
if (normalizedRoles.length < 1) {
|
|
72
|
-
return NextResponse.json({ ok: false, error: "Each selected agent must have a roleId" }, { status: 400 });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const roleSet = new Set<string>();
|
|
76
|
-
for (const r of normalizedRoles) {
|
|
77
|
-
if (!isValidId(r.roleId)) {
|
|
78
|
-
return NextResponse.json(
|
|
79
|
-
{ ok: false, error: `Invalid roleId: ${r.roleId}. Use lowercase letters/numbers with - or _.` },
|
|
80
|
-
{ status: 400 },
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (roleSet.has(r.roleId)) {
|
|
84
|
-
return NextResponse.json({ ok: false, error: `Duplicate roleId: ${r.roleId}` }, { status: 400 });
|
|
85
|
-
}
|
|
86
|
-
roleSet.add(r.roleId);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const dir = await getWorkspaceRecipesDir();
|
|
90
|
-
const filePath = path.join(dir, `${recipeId}.md`);
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
await fs.access(filePath);
|
|
94
|
-
return NextResponse.json({ ok: false, error: `Recipe already exists: ${recipeId}` }, { status: 409 });
|
|
95
|
-
} catch {
|
|
96
|
-
// ok
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const lines: string[] = [];
|
|
100
|
-
lines.push("---");
|
|
101
|
-
lines.push(`id: ${recipeId}`);
|
|
102
|
-
lines.push(`name: ${yamlEscape(name || recipeId)}`);
|
|
103
|
-
lines.push(`version: 0.1.0`);
|
|
104
|
-
if (description) lines.push(`description: ${yamlEscape(description)}`);
|
|
105
|
-
lines.push("kind: team");
|
|
106
|
-
lines.push("requiredSkills: []");
|
|
107
|
-
lines.push("team:");
|
|
108
|
-
lines.push(` teamId: ${teamId}`);
|
|
109
|
-
lines.push("agents:");
|
|
110
|
-
|
|
111
|
-
for (const r of normalizedRoles) {
|
|
112
|
-
lines.push(` - role: ${r.roleId}`);
|
|
113
|
-
if (r.displayName) lines.push(` name: ${yamlEscape(r.displayName)}`);
|
|
114
|
-
lines.push(" tools:");
|
|
115
|
-
lines.push(" profile: coding");
|
|
116
|
-
lines.push(" allow:");
|
|
117
|
-
lines.push(" - group:fs");
|
|
118
|
-
lines.push(" - group:web");
|
|
119
|
-
lines.push(" - group:runtime");
|
|
120
|
-
lines.push(" deny:");
|
|
121
|
-
lines.push(" - exec");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
lines.push("templates:");
|
|
125
|
-
|
|
126
|
-
for (const r of normalizedRoles) {
|
|
127
|
-
const roleId = r.roleId;
|
|
128
|
-
|
|
129
|
-
lines.push(` ${roleId}.soul: |`);
|
|
130
|
-
lines.push(` # SOUL.md`);
|
|
131
|
-
lines.push(` `);
|
|
132
|
-
lines.push(` Role: ${roleId}`);
|
|
133
|
-
lines.push(` `);
|
|
134
|
-
|
|
135
|
-
lines.push(` ${roleId}.agents: |`);
|
|
136
|
-
lines.push(` # AGENTS.md`);
|
|
137
|
-
lines.push(` `);
|
|
138
|
-
lines.push(` You are the ${roleId} role in team ${teamId}.`);
|
|
139
|
-
lines.push(` `);
|
|
140
|
-
|
|
141
|
-
lines.push(` ${roleId}.tools: |`);
|
|
142
|
-
lines.push(` # TOOLS.md`);
|
|
143
|
-
lines.push(` `);
|
|
144
|
-
lines.push(` - (empty)`);
|
|
145
|
-
lines.push(` `);
|
|
146
|
-
|
|
147
|
-
lines.push(` ${roleId}.status: |`);
|
|
148
|
-
lines.push(` # STATUS.md`);
|
|
149
|
-
lines.push(` `);
|
|
150
|
-
lines.push(` - (empty)`);
|
|
151
|
-
lines.push(` `);
|
|
152
|
-
|
|
153
|
-
lines.push(` ${roleId}.notes: |`);
|
|
154
|
-
lines.push(` # NOTES.md`);
|
|
155
|
-
lines.push(` `);
|
|
156
|
-
lines.push(` - (empty)`);
|
|
157
|
-
lines.push("");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
lines.push("files:");
|
|
161
|
-
lines.push(" - path: SOUL.md");
|
|
162
|
-
lines.push(" template: soul");
|
|
163
|
-
lines.push(" mode: createOnly");
|
|
164
|
-
lines.push(" - path: AGENTS.md");
|
|
165
|
-
lines.push(" template: agents");
|
|
166
|
-
lines.push(" mode: createOnly");
|
|
167
|
-
lines.push(" - path: TOOLS.md");
|
|
168
|
-
lines.push(" template: tools");
|
|
169
|
-
lines.push(" mode: createOnly");
|
|
170
|
-
lines.push(" - path: STATUS.md");
|
|
171
|
-
lines.push(" template: status");
|
|
172
|
-
lines.push(" mode: createOnly");
|
|
173
|
-
lines.push(" - path: NOTES.md");
|
|
174
|
-
lines.push(" template: notes");
|
|
175
|
-
lines.push(" mode: createOnly");
|
|
176
|
-
|
|
177
|
-
lines.push("---");
|
|
178
|
-
lines.push("");
|
|
179
|
-
lines.push("# Custom team recipe");
|
|
180
|
-
lines.push("");
|
|
181
|
-
lines.push("Generated by ClawKitchen Custom Team Builder.");
|
|
182
|
-
lines.push("");
|
|
183
|
-
|
|
184
|
-
const md = lines.join("\n");
|
|
185
|
-
|
|
186
|
-
if (dryRun) {
|
|
187
|
-
return NextResponse.json({ ok: true, dryRun: true, recipeId, teamId, filePath, md });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
await fs.writeFile(filePath, md, "utf8");
|
|
191
|
-
|
|
192
|
-
return NextResponse.json({ ok: true, recipeId, teamId, filePath });
|
|
193
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { runOpenClaw } from "@/lib/openclaw";
|
|
4
|
-
|
|
5
|
-
export async function getAttachedTeams(workspaceRoot: string, recipeId: string): Promise<string[]> {
|
|
6
|
-
const attachedTeams: string[] = [];
|
|
7
|
-
const teamsRoot = path.resolve(workspaceRoot, "..");
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
const entries = await fs.readdir(teamsRoot, { withFileTypes: true });
|
|
11
|
-
const workspaceDirs = entries.filter((e) => e.isDirectory() && e.name.startsWith("workspace-"));
|
|
12
|
-
|
|
13
|
-
for (const dirent of workspaceDirs) {
|
|
14
|
-
const metaPath = path.join(teamsRoot, dirent.name, "team.json");
|
|
15
|
-
try {
|
|
16
|
-
const raw = await fs.readFile(metaPath, "utf8");
|
|
17
|
-
const meta = JSON.parse(raw) as { recipeId?: unknown; teamId?: unknown };
|
|
18
|
-
if (String(meta.recipeId ?? "").trim() === recipeId) {
|
|
19
|
-
attachedTeams.push(
|
|
20
|
-
String(meta.teamId ?? dirent.name.replace(/^workspace-/, "")).trim() || dirent.name
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
} catch {
|
|
24
|
-
// ignore
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
} catch {
|
|
28
|
-
// ignore
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return attachedTeams;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function getAttachedAgents(
|
|
35
|
-
workspaceRoot: string,
|
|
36
|
-
recipeId: string
|
|
37
|
-
): Promise<{ attachedAgents: string[]; hasSameIdAgent: boolean }> {
|
|
38
|
-
const attachedAgents: string[] = [];
|
|
39
|
-
let hasSameIdAgent = false;
|
|
40
|
-
|
|
41
|
-
const agentsRes = await runOpenClaw(["agents", "list", "--json"]);
|
|
42
|
-
if (!agentsRes.ok) return { attachedAgents, hasSameIdAgent };
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const agents = JSON.parse(agentsRes.stdout) as Array<{ id?: unknown }>;
|
|
46
|
-
hasSameIdAgent = agents.some((a) => String(a.id ?? "").trim() === recipeId);
|
|
47
|
-
|
|
48
|
-
for (const a of agents) {
|
|
49
|
-
const agentId = String(a.id ?? "").trim();
|
|
50
|
-
if (!agentId) continue;
|
|
51
|
-
const metaPath = path.join(workspaceRoot, "agents", agentId, "agent.json");
|
|
52
|
-
try {
|
|
53
|
-
const raw = await fs.readFile(metaPath, "utf8");
|
|
54
|
-
const meta = JSON.parse(raw) as { recipeId?: unknown };
|
|
55
|
-
if (String(meta.recipeId ?? "").trim() === recipeId) attachedAgents.push(agentId);
|
|
56
|
-
} catch {
|
|
57
|
-
// ignore
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// ignore
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { attachedAgents, hasSameIdAgent };
|
|
65
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { runOpenClaw } from "@/lib/openclaw";
|
|
4
|
-
import { findRecipeById, resolveRecipePath } from "@/lib/recipes";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import { getAttachedTeams, getAttachedAgents } from "./helpers";
|
|
7
|
-
|
|
8
|
-
export async function POST(req: Request) {
|
|
9
|
-
const body = (await req.json()) as { id?: string };
|
|
10
|
-
const id = String(body.id ?? "").trim();
|
|
11
|
-
if (!id) return NextResponse.json({ ok: false, error: "id is required" }, { status: 400 });
|
|
12
|
-
|
|
13
|
-
const item = await findRecipeById(id);
|
|
14
|
-
if (!item) return NextResponse.json({ ok: false, error: `Recipe not found: ${id}` }, { status: 404 });
|
|
15
|
-
if (item.source === "builtin") {
|
|
16
|
-
return NextResponse.json({ ok: false, error: `Recipe ${id} is builtin and cannot be deleted` }, { status: 403 });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const workspaceRoot = (await runOpenClaw(["config", "get", "agents.defaults.workspace"]))?.stdout?.trim();
|
|
20
|
-
if (!workspaceRoot) {
|
|
21
|
-
return NextResponse.json({ ok: false, error: "agents.defaults.workspace not set" }, { status: 500 });
|
|
22
|
-
}
|
|
23
|
-
const allowedDir = path.resolve(workspaceRoot, "recipes") + path.sep;
|
|
24
|
-
|
|
25
|
-
const filePath = await resolveRecipePath(item);
|
|
26
|
-
const resolved = path.resolve(filePath);
|
|
27
|
-
if (!resolved.startsWith(allowedDir)) {
|
|
28
|
-
return NextResponse.json({ ok: false, error: `Refusing to delete non-workspace recipe path: ${resolved}` }, { status: 403 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const kind = (item.kind ?? "team") as "team" | "agent";
|
|
32
|
-
|
|
33
|
-
if (kind === "team") {
|
|
34
|
-
const attachedTeams = await getAttachedTeams(workspaceRoot, id);
|
|
35
|
-
if (attachedTeams.length) {
|
|
36
|
-
return NextResponse.json(
|
|
37
|
-
{
|
|
38
|
-
ok: false,
|
|
39
|
-
error: `Team ${id} is in use by installed team(s): ${attachedTeams.join(", ")}. Remove the team(s) first, then delete the recipe. If no team is shown, you still have a .openclaw/workspace-${id} folder. Please remove the folder to delete this recipe.`,
|
|
40
|
-
details: { attachedTeams },
|
|
41
|
-
},
|
|
42
|
-
{ status: 409 }
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (kind === "agent") {
|
|
48
|
-
const { attachedAgents, hasSameIdAgent } = await getAttachedAgents(workspaceRoot, id);
|
|
49
|
-
if (hasSameIdAgent) {
|
|
50
|
-
return NextResponse.json(
|
|
51
|
-
{
|
|
52
|
-
ok: false,
|
|
53
|
-
error: `Agent recipe ${id} cannot be deleted because an active agent exists with the same id: ${id}. Delete the agent first, then delete the recipe.`,
|
|
54
|
-
details: { agentId: id },
|
|
55
|
-
},
|
|
56
|
-
{ status: 409 }
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
if (attachedAgents.length) {
|
|
60
|
-
return NextResponse.json(
|
|
61
|
-
{
|
|
62
|
-
ok: false,
|
|
63
|
-
error: `Agent recipe ${id} is in use by active agent(s): ${attachedAgents.join(", ")}. Delete the agent(s) first, then delete the recipe.`,
|
|
64
|
-
details: { attachedAgents },
|
|
65
|
-
},
|
|
66
|
-
{ status: 409 }
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
await fs.rm(resolved, { force: true });
|
|
72
|
-
return NextResponse.json({ ok: true, deleted: resolved });
|
|
73
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { runOpenClaw } from "@/lib/openclaw";
|
|
3
|
-
|
|
4
|
-
export async function GET() {
|
|
5
|
-
const { stdout, stderr } = await runOpenClaw(["recipes", "list"]);
|
|
6
|
-
if (stderr.trim()) {
|
|
7
|
-
// non-fatal warnings go to stderr sometimes; still try to parse stdout.
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
let data: unknown;
|
|
11
|
-
try {
|
|
12
|
-
data = JSON.parse(stdout);
|
|
13
|
-
} catch {
|
|
14
|
-
return NextResponse.json({ error: "Failed to parse openclaw recipes list output", stderr, stdout }, { status: 500 });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// NOTE: We intentionally return the full list here. openclaw can return both builtin + workspace
|
|
18
|
-
// entries for the same (kind,id). The UI can decide which to prefer depending on context.
|
|
19
|
-
const list = Array.isArray(data) ? data : [];
|
|
20
|
-
return NextResponse.json({ recipes: list, stderr });
|
|
21
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
parseRecipeFrontmatter,
|
|
4
|
-
buildNextMarkdown,
|
|
5
|
-
handleRemove,
|
|
6
|
-
handleAdd,
|
|
7
|
-
handleAddLike,
|
|
8
|
-
} from "../helpers";
|
|
9
|
-
|
|
10
|
-
vi.mock("@/lib/openclaw", () => ({
|
|
11
|
-
runOpenClaw: vi.fn(),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
import { runOpenClaw } from "@/lib/openclaw";
|
|
15
|
-
|
|
16
|
-
describe("recipes team-agents helpers", () => {
|
|
17
|
-
describe("parseRecipeFrontmatter", () => {
|
|
18
|
-
it("parses valid YAML with agents and templates", () => {
|
|
19
|
-
const yaml = `
|
|
20
|
-
kind: team
|
|
21
|
-
agents:
|
|
22
|
-
- role: lead
|
|
23
|
-
name: Lead
|
|
24
|
-
templates:
|
|
25
|
-
lead.prompt: "# Prompt"
|
|
26
|
-
`;
|
|
27
|
-
const { fm, agents, templates } = parseRecipeFrontmatter(yaml);
|
|
28
|
-
expect(fm.kind).toBe("team");
|
|
29
|
-
expect(agents).toHaveLength(1);
|
|
30
|
-
expect(agents[0]).toEqual({ role: "lead", name: "Lead" });
|
|
31
|
-
expect(templates).toEqual({ "lead.prompt": "# Prompt" });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("handles empty YAML", () => {
|
|
35
|
-
const { fm, agents, templates } = parseRecipeFrontmatter("");
|
|
36
|
-
expect(fm).toEqual({});
|
|
37
|
-
expect(agents).toEqual([]);
|
|
38
|
-
expect(templates).toEqual({});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("handles non-array agents", () => {
|
|
42
|
-
const { agents } = parseRecipeFrontmatter("agents: not-an-array");
|
|
43
|
-
expect(agents).toEqual([]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("handles non-object templates", () => {
|
|
47
|
-
const { templates } = parseRecipeFrontmatter("templates: []");
|
|
48
|
-
expect(templates).toEqual({});
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("buildNextMarkdown", () => {
|
|
53
|
-
it("builds markdown with updated agents and templates", () => {
|
|
54
|
-
const fm = { kind: "team" };
|
|
55
|
-
const nextAgents = [{ role: "lead", name: "Lead" }];
|
|
56
|
-
const nextTemplates = { "lead.prompt": "content" };
|
|
57
|
-
const rest = "# Body";
|
|
58
|
-
const result = buildNextMarkdown(fm, nextAgents, nextTemplates, rest);
|
|
59
|
-
expect(result).toContain("---");
|
|
60
|
-
expect(result).toContain("kind: team");
|
|
61
|
-
expect(result).toContain("role: lead");
|
|
62
|
-
expect(result).toContain("# Body");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("omits templates when empty", () => {
|
|
66
|
-
const fm = { kind: "team" };
|
|
67
|
-
const nextAgents: Array<Record<string, unknown>> = [];
|
|
68
|
-
const nextTemplates: Record<string, unknown> = {};
|
|
69
|
-
const result = buildNextMarkdown(fm, nextAgents, nextTemplates, "body");
|
|
70
|
-
expect(result).not.toContain("templates:");
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("handleRemove", () => {
|
|
75
|
-
it("removes agent and role-prefixed templates", () => {
|
|
76
|
-
const agents = [
|
|
77
|
-
{ role: "lead", name: "Lead" },
|
|
78
|
-
{ role: "qa", name: "QA" },
|
|
79
|
-
];
|
|
80
|
-
const templates = {
|
|
81
|
-
"lead.prompt": "x",
|
|
82
|
-
"qa.prompt": "y",
|
|
83
|
-
"other.stuff": "z",
|
|
84
|
-
};
|
|
85
|
-
const result = handleRemove(agents, templates, "lead");
|
|
86
|
-
expect(result.nextAgents).toHaveLength(1);
|
|
87
|
-
expect(result.nextAgents[0].role).toBe("qa");
|
|
88
|
-
expect(result.nextTemplates).toEqual({ "qa.prompt": "y", "other.stuff": "z" });
|
|
89
|
-
expect(result.addedRole).toBeNull();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("returns addedRole null", () => {
|
|
93
|
-
const result = handleRemove([{ role: "x" }], {}, "x");
|
|
94
|
-
expect(result.addedRole).toBeNull();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("handleAdd", () => {
|
|
99
|
-
it("adds new agent when role not present", () => {
|
|
100
|
-
const agents: Array<Record<string, unknown>> = [];
|
|
101
|
-
const templates = {};
|
|
102
|
-
const result = handleAdd(agents, templates, "qa", "QA Lead");
|
|
103
|
-
expect(result.nextAgents).toHaveLength(1);
|
|
104
|
-
expect(result.nextAgents[0]).toEqual({ role: "qa", name: "QA Lead" });
|
|
105
|
-
expect(result.addedRole).toBe("qa");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("updates existing agent when role present", () => {
|
|
109
|
-
const agents = [{ role: "qa", name: "Old" }];
|
|
110
|
-
const templates = {};
|
|
111
|
-
const result = handleAdd(agents, templates, "qa", "New Name");
|
|
112
|
-
expect(result.nextAgents).toHaveLength(1);
|
|
113
|
-
expect(result.nextAgents[0]).toEqual({ role: "qa", name: "New Name" });
|
|
114
|
-
expect(result.addedRole).toBe("qa");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("copies templates without mutating", () => {
|
|
118
|
-
const agents: Array<Record<string, unknown>> = [];
|
|
119
|
-
const templates = { x: "y" };
|
|
120
|
-
const result = handleAdd(agents, templates, "qa", "");
|
|
121
|
-
expect(result.nextTemplates).toEqual({ x: "y" });
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("handleAddLike", () => {
|
|
126
|
-
beforeEach(() => {
|
|
127
|
-
vi.mocked(runOpenClaw).mockResolvedValue({
|
|
128
|
-
ok: true,
|
|
129
|
-
stdout: JSON.stringify([{ id: "team1-lead" }]),
|
|
130
|
-
stderr: "",
|
|
131
|
-
exitCode: 0,
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("returns 400 when baseRole not found", async () => {
|
|
136
|
-
const agents: Array<Record<string, unknown>> = [];
|
|
137
|
-
const result = await handleAddLike(agents, {}, "lead", "New", "team1");
|
|
138
|
-
expect(result).toBeInstanceOf(Response);
|
|
139
|
-
const res = result as Response;
|
|
140
|
-
expect(res.status).toBe(400);
|
|
141
|
-
const json = await res.json();
|
|
142
|
-
expect(json.error).toContain("baseRole not found");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("adds clone with suffixed role when base exists", async () => {
|
|
146
|
-
const agents = [{ role: "lead", name: "Lead" }];
|
|
147
|
-
const templates = { "lead.prompt": "content" };
|
|
148
|
-
const result = await handleAddLike(agents, templates, "lead", "Lead 2", "team1");
|
|
149
|
-
expect(result).not.toBeInstanceOf(Response);
|
|
150
|
-
const op = result as { nextAgents: Array<Record<string, unknown>>; nextTemplates: Record<string, unknown>; addedRole: string | null };
|
|
151
|
-
expect(op.nextAgents).toHaveLength(2);
|
|
152
|
-
expect(op.nextAgents[1].role).toMatch(/^lead(-\d+)?$/);
|
|
153
|
-
expect(op.addedRole).toBeTruthy();
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import YAML from "yaml";
|
|
2
|
-
import { NextResponse } from "next/server";
|
|
3
|
-
import { runOpenClaw } from "@/lib/openclaw";
|
|
4
|
-
import { splitRecipeFrontmatter, normalizeRole } from "@/lib/recipe-team-agents";
|
|
5
|
-
|
|
6
|
-
export { splitRecipeFrontmatter as splitFrontmatter, normalizeRole };
|
|
7
|
-
|
|
8
|
-
export function parseRecipeFrontmatter(yamlText: string) {
|
|
9
|
-
const fm = (YAML.parse(yamlText) ?? {}) as Record<string, unknown>;
|
|
10
|
-
const agentsRaw = fm.agents;
|
|
11
|
-
const agents: Array<Record<string, unknown>> = Array.isArray(agentsRaw)
|
|
12
|
-
? (agentsRaw as Array<Record<string, unknown>>)
|
|
13
|
-
: [];
|
|
14
|
-
const templatesRaw = fm.templates;
|
|
15
|
-
const templates: Record<string, unknown> =
|
|
16
|
-
templatesRaw && typeof templatesRaw === "object" && !Array.isArray(templatesRaw)
|
|
17
|
-
? (templatesRaw as Record<string, unknown>)
|
|
18
|
-
: {};
|
|
19
|
-
return { fm, agents, templates };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function buildNextMarkdown(
|
|
23
|
-
fm: Record<string, unknown>,
|
|
24
|
-
nextAgents: Array<Record<string, unknown>>,
|
|
25
|
-
nextTemplates: Record<string, unknown>,
|
|
26
|
-
rest: string
|
|
27
|
-
) {
|
|
28
|
-
const nextFm = {
|
|
29
|
-
...fm,
|
|
30
|
-
agents: nextAgents,
|
|
31
|
-
...(Object.keys(nextTemplates).length ? { templates: nextTemplates } : {}),
|
|
32
|
-
};
|
|
33
|
-
const nextYaml = YAML.stringify(nextFm).trimEnd();
|
|
34
|
-
return `---\n${nextYaml}\n---\n${rest}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type OpResult = {
|
|
38
|
-
nextAgents: Array<Record<string, unknown>>;
|
|
39
|
-
nextTemplates: Record<string, unknown>;
|
|
40
|
-
addedRole: string | null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export function handleRemove(
|
|
44
|
-
agents: Array<Record<string, unknown>>,
|
|
45
|
-
templates: Record<string, unknown>,
|
|
46
|
-
role: string
|
|
47
|
-
): OpResult {
|
|
48
|
-
const nextAgents = agents.filter((a) => String(a.role ?? "") !== role);
|
|
49
|
-
const nextTemplates = { ...templates };
|
|
50
|
-
for (const k of Object.keys(nextTemplates)) {
|
|
51
|
-
if (k.startsWith(`${role}.`)) delete nextTemplates[k];
|
|
52
|
-
}
|
|
53
|
-
return { nextAgents, nextTemplates, addedRole: null };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function handleAdd(
|
|
57
|
-
agents: Array<Record<string, unknown>>,
|
|
58
|
-
templates: Record<string, unknown>,
|
|
59
|
-
role: string,
|
|
60
|
-
name: string
|
|
61
|
-
): OpResult {
|
|
62
|
-
const next = {
|
|
63
|
-
...agents.find((a) => String(a.role ?? "") === role),
|
|
64
|
-
role,
|
|
65
|
-
...(name ? { name } : {}),
|
|
66
|
-
};
|
|
67
|
-
const nextAgents = agents.slice();
|
|
68
|
-
const idx = nextAgents.findIndex((a) => String(a.role ?? "") === role);
|
|
69
|
-
if (idx === -1) nextAgents.push(next);
|
|
70
|
-
else nextAgents[idx] = next;
|
|
71
|
-
return { nextAgents, nextTemplates: { ...templates }, addedRole: role };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function maxSuffixFromUsedRoles(usedRoles: Set<string>, baseRole: string): number {
|
|
75
|
-
const escaped = baseRole.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
76
|
-
let n = 1;
|
|
77
|
-
for (const r of usedRoles) {
|
|
78
|
-
if (r === baseRole) n = Math.max(n, 1);
|
|
79
|
-
const m = r.match(new RegExp(`^${escaped}-([0-9]+)$`));
|
|
80
|
-
if (m) {
|
|
81
|
-
const k = Number(m[1]);
|
|
82
|
-
if (Number.isFinite(k)) n = Math.max(n, k);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return n;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function fetchExistingAgentIds(teamId: string): Promise<Set<string>> {
|
|
89
|
-
if (!teamId) return new Set<string>();
|
|
90
|
-
try {
|
|
91
|
-
const res = await runOpenClaw(["agents", "list", "--json"]);
|
|
92
|
-
if (!res.ok) return new Set<string>();
|
|
93
|
-
const items = JSON.parse(res.stdout) as Array<{ id?: unknown }>;
|
|
94
|
-
return new Set(items.map((a) => String(a.id ?? "").trim()).filter(Boolean));
|
|
95
|
-
} catch {
|
|
96
|
-
return new Set<string>();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function pickNextRole(
|
|
101
|
-
baseRole: string,
|
|
102
|
-
usedRoles: Set<string>,
|
|
103
|
-
existingAgentIds: Set<string>,
|
|
104
|
-
teamId: string,
|
|
105
|
-
n: number
|
|
106
|
-
): string {
|
|
107
|
-
const isTaken = (role: string) =>
|
|
108
|
-
usedRoles.has(role) || (teamId ? existingAgentIds.has(`${teamId}-${role}`) : false);
|
|
109
|
-
if (!isTaken(baseRole)) return baseRole;
|
|
110
|
-
let i = Math.max(2, n + 1);
|
|
111
|
-
while (isTaken(`${baseRole}-${i}`)) i++;
|
|
112
|
-
return `${baseRole}-${i}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function handleAddLike(
|
|
116
|
-
agents: Array<Record<string, unknown>>,
|
|
117
|
-
templates: Record<string, unknown>,
|
|
118
|
-
baseRole: string,
|
|
119
|
-
name: string,
|
|
120
|
-
teamId: string
|
|
121
|
-
): Promise<OpResult | NextResponse> {
|
|
122
|
-
const base = agents.find((a) => String(a.role ?? "") === baseRole);
|
|
123
|
-
if (!base) {
|
|
124
|
-
return NextResponse.json({ ok: false, error: `baseRole not found in recipe: ${baseRole}` }, { status: 400 });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const nextAgents = agents.slice();
|
|
128
|
-
const nextTemplates = { ...templates };
|
|
129
|
-
const usedRoles = new Set(nextAgents.map((a) => String(a.role ?? "").trim()).filter(Boolean));
|
|
130
|
-
const n = maxSuffixFromUsedRoles(usedRoles, baseRole);
|
|
131
|
-
const existingAgentIds = await fetchExistingAgentIds(teamId);
|
|
132
|
-
const nextRole = pickNextRole(baseRole, usedRoles, existingAgentIds, teamId, n);
|
|
133
|
-
|
|
134
|
-
const baseName = typeof (base as { name?: unknown }).name === "string" ? String((base as { name?: unknown }).name) : "";
|
|
135
|
-
const autoSuffix = nextRole === baseRole ? "" : String(nextRole.slice(baseRole.length + 1));
|
|
136
|
-
const suffixPart = autoSuffix ? ` ${autoSuffix}` : "";
|
|
137
|
-
const nextName = name || (baseName ? baseName + suffixPart : "");
|
|
138
|
-
|
|
139
|
-
const clone = { ...base, role: nextRole, ...(nextName ? { name: nextName } : {}) };
|
|
140
|
-
nextAgents.push(clone);
|
|
141
|
-
|
|
142
|
-
for (const [k, v] of Object.entries(templates)) {
|
|
143
|
-
if (!k.startsWith(`${baseRole}.`)) continue;
|
|
144
|
-
const suffix = k.slice(baseRole.length);
|
|
145
|
-
const nextKey = `${nextRole}${suffix}`;
|
|
146
|
-
if (nextTemplates[nextKey] === undefined) nextTemplates[nextKey] = v;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const addedRole = String((nextAgents[nextAgents.length - 1] as { role?: unknown } | undefined)?.role ?? "").trim() || null;
|
|
150
|
-
return { nextAgents, nextTemplates, addedRole };
|
|
151
|
-
}
|