summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src/routes
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-20 15:04:26 +0900
committerAdam Malczewski <[email protected]>2026-05-20 15:04:26 +0900
commitadc8bd185b54935e7a31aae04da3175b7989927a (patch)
tree031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/api/src/routes
parent52af4ca33b1f83b8f863a6267d447944f55e729d (diff)
downloaddispatch-adc8bd185b54935e7a31aae04da3175b7989927a.tar.gz
dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.zip
feat: phase 3 — config, skills, model groups, task list, and sidebar UI
- Config system: TOML-based dispatch.toml with hot-reload via chokidar - Model/key resolution: tag-based model selection, key fallback chains - Skills system: directory loader with TOML frontmatter, agent mappings - Task list tool: add/update/list/get operations with WebSocket events - API routes: GET /config, /skills, /skills/:name, /models, /models/resolve - Frontend: sidebar with model status, task list, config viewer, skills browser, permission log - Sliding sidebar animation using CSS transitions (not Svelte transitions)
Diffstat (limited to 'packages/api/src/routes')
-rw-r--r--packages/api/src/routes/config.ts27
-rw-r--r--packages/api/src/routes/models.ts69
-rw-r--r--packages/api/src/routes/skills.ts43
3 files changed, 139 insertions, 0 deletions
diff --git a/packages/api/src/routes/config.ts b/packages/api/src/routes/config.ts
new file mode 100644
index 0000000..2d08167
--- /dev/null
+++ b/packages/api/src/routes/config.ts
@@ -0,0 +1,27 @@
+import { Hono } from "hono";
+import type { DispatchConfig } from "@dispatch/core";
+
+let getConfig: () => DispatchConfig = () => ({ permissions: {} });
+
+export function setConfigGetter(getter: () => DispatchConfig): void {
+ getConfig = getter;
+}
+
+const configRoutes = new Hono();
+
+configRoutes.get("/", (c) => {
+ const config = getConfig();
+
+ // Strip env field values from keys for security
+ const safeConfig: DispatchConfig = {
+ ...config,
+ keys: config.keys?.map((key) => ({
+ ...key,
+ env: "***",
+ })),
+ };
+
+ return c.json({ config: safeConfig });
+});
+
+export { configRoutes };
diff --git a/packages/api/src/routes/models.ts b/packages/api/src/routes/models.ts
new file mode 100644
index 0000000..62f7340
--- /dev/null
+++ b/packages/api/src/routes/models.ts
@@ -0,0 +1,69 @@
+import { Hono } from "hono";
+import type { ModelRegistry, ModelResolver } from "@dispatch/core";
+
+let getRegistry: () => ModelRegistry | null = () => null;
+let getResolver: () => ModelResolver | null = () => null;
+
+export function setModelsGetter(
+ registryGetter: () => ModelRegistry | null,
+ resolverGetter: () => ModelResolver | null,
+): void {
+ getRegistry = registryGetter;
+ getResolver = resolverGetter;
+}
+
+export const modelsRoutes = new Hono();
+
+modelsRoutes.get("/", (c) => {
+ const registry = getRegistry();
+ if (!registry) {
+ return c.json({ models: [], tags: [], keys: [] });
+ }
+
+ const models = registry.getModels();
+ const tags = registry.getAllTags();
+ const keyStates = registry.getKeys();
+
+ const keys = keyStates.map((ks) => ({
+ id: ks.definition.id,
+ provider: ks.definition.provider,
+ status: ks.status,
+ lastError: ks.lastError ?? null,
+ exhaustedAt: ks.exhaustedAt ?? null,
+ }));
+
+ return c.json({ models, tags, keys });
+});
+
+modelsRoutes.get("/resolve", (c) => {
+ const registry = getRegistry();
+ const resolver = getResolver();
+ if (!registry || !resolver) {
+ return c.json({ resolved: null, reason: "no models configured" });
+ }
+
+ const tag = c.req.query("tag");
+ if (!tag) {
+ return c.json({ error: "tag query parameter is required" }, 400);
+ }
+
+ const matchingModels = registry.getModelsByTag(tag);
+ if (matchingModels.length === 0) {
+ return c.json({ resolved: null, reason: "no models match tag" });
+ }
+
+ const resolved = resolver.resolve(tag);
+ if (!resolved) {
+ return c.json({ resolved: null, reason: "all keys exhausted for matching providers" });
+ }
+
+ return c.json({
+ resolved: {
+ model: resolved.model,
+ key: {
+ id: resolved.key.id,
+ provider: resolved.key.provider,
+ },
+ },
+ });
+});
diff --git a/packages/api/src/routes/skills.ts b/packages/api/src/routes/skills.ts
new file mode 100644
index 0000000..245fb4c
--- /dev/null
+++ b/packages/api/src/routes/skills.ts
@@ -0,0 +1,43 @@
+import { Hono } from "hono";
+import type { AgentSkillMapping, SkillDefinition, SkillScope } from "@dispatch/core";
+
+let getSkills: () => { skills: SkillDefinition[]; mappings: AgentSkillMapping[] } = () => ({ skills: [], mappings: [] });
+
+export function setSkillsGetter(getter: () => { skills: SkillDefinition[]; mappings: AgentSkillMapping[] }): void {
+ getSkills = getter;
+}
+
+export const skillsRoutes = new Hono();
+
+skillsRoutes.get("/", (c) => {
+ const { skills, mappings } = getSkills();
+ const skillSummaries = skills.map(({ name, description, tags, scope, directory }) => ({
+ name,
+ description,
+ tags,
+ scope,
+ directory,
+ }));
+ return c.json({ skills: skillSummaries, mappings });
+});
+
+skillsRoutes.get("/:name", (c) => {
+ const { name } = c.req.param();
+ const scopeParam = c.req.query("scope") as SkillScope | undefined;
+ const { skills } = getSkills();
+
+ const matches = skills.filter((s) => s.name === name);
+ if (matches.length === 0) {
+ return c.json({ error: "Skill not found" }, 404);
+ }
+
+ if (scopeParam) {
+ const scoped = matches.find((s) => s.scope === scopeParam);
+ if (!scoped) {
+ return c.json({ error: "Skill not found" }, 404);
+ }
+ return c.json(scoped);
+ }
+
+ return c.json(matches[0]);
+});