CATEGORY_ORDER hardcoded Video/Audio/Cleanup in the frontend, so adding a category meant a code change. The order now lives in categories.yaml at the root of the items store and reaches the client via GET /tasks/categories — the platform no longer knows any category by name. The endpoint is declared before /:name, which would otherwise match "categories". Categories used by a task but absent from the file still work: they sort alphabetically after the listed ones, and Other stays last. Menus consume grouped tasks rather than grouping them per row. Groups are built from the tasks and the file only ranks them, so a category listed with no matching tasks cannot produce an empty submenu — locked in by tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import { createRouter } from '../../create-router';
|
|
import { listTasks, getTaskByDirName, createTask, deleteTask, readCategoryOrder } from './task-files';
|
|
|
|
type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' };
|
|
|
|
export const tasksRouter = createRouter();
|
|
|
|
tasksRouter.get('/', async (ctx) => {
|
|
const summaries = await listTasks();
|
|
|
|
const tasks = summaries.map((row) => ({
|
|
dirName: row.dirName,
|
|
name: row.name,
|
|
description: row.description,
|
|
category: row.category,
|
|
triggers: (row.trigger as TriggerConfig[]) ?? [],
|
|
mode: row.mode ?? 'agentic',
|
|
}));
|
|
|
|
return ctx.json(tasks);
|
|
});
|
|
|
|
// Must be declared before '/:name', which would otherwise match "categories".
|
|
tasksRouter.get('/categories', async (ctx) => {
|
|
return ctx.json(await readCategoryOrder());
|
|
});
|
|
|
|
tasksRouter.get('/:name', async (ctx) => {
|
|
const name = ctx.req.param('name');
|
|
|
|
const task = await getTaskByDirName(name);
|
|
if (!task) return ctx.text('Not found', 404);
|
|
|
|
return ctx.json({
|
|
dirName: task.dirName,
|
|
name: task.name,
|
|
description: task.description,
|
|
category: task.category,
|
|
mode: task.mode,
|
|
inline: task.inline,
|
|
language: task.language,
|
|
body: task.body,
|
|
implementation: task.implementation,
|
|
inputs: task.inputs,
|
|
args: task.args,
|
|
trigger: task.trigger,
|
|
config: task.config,
|
|
version: task.version,
|
|
filePath: task.filePath,
|
|
});
|
|
});
|
|
|
|
tasksRouter.post('/', async (ctx) => {
|
|
const body = await ctx.req.json<{ name: string; description?: string; mode?: string; language?: string }>();
|
|
|
|
if (!body.name?.trim()) return ctx.text('Name is required', 400);
|
|
|
|
try {
|
|
const task = await createTask({
|
|
name: body.name,
|
|
description: body.description ?? null,
|
|
mode: body.mode ?? 'agentic',
|
|
language: body.language ?? null,
|
|
});
|
|
return ctx.json(task);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : 'Failed to create task';
|
|
return ctx.text(message, message === 'Task already exists' ? 409 : 400);
|
|
}
|
|
});
|
|
|
|
tasksRouter.delete('/:name', async (ctx) => {
|
|
const name = ctx.req.param('name');
|
|
|
|
const task = await getTaskByDirName(name);
|
|
if (!task) return ctx.text('Not found', 404);
|
|
|
|
await deleteTask(task.dirName);
|
|
return ctx.json({ ok: true });
|
|
});
|