diff options
| author | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
| commit | adc8bd185b54935e7a31aae04da3175b7989927a (patch) | |
| tree | 031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/api/src/routes | |
| parent | 52af4ca33b1f83b8f863a6267d447944f55e729d (diff) | |
| download | dispatch-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.ts | 27 | ||||
| -rw-r--r-- | packages/api/src/routes/models.ts | 69 | ||||
| -rw-r--r-- | packages/api/src/routes/skills.ts | 43 |
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]); +}); |
