diff options
| author | Adam Malczewski <[email protected]> | 2026-06-04 21:21:20 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-04 21:21:20 +0900 |
| commit | 394f1ed37ce860da6fdc385769bf29f9737105cd (patch) | |
| tree | 4b825dc642cb6eb9a060e54bf8d69288fbee4904 /packages/core/src/agents | |
| parent | 81a9cdbadf8c9d940d4fe9a2a0de607dee1f5f1a (diff) | |
| download | dispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.tar.gz dispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.zip | |
chore: genesis — remove all files to rebuild from scratch (arch rewrite)
Diffstat (limited to 'packages/core/src/agents')
| -rw-r--r-- | packages/core/src/agents/index.ts | 11 | ||||
| -rw-r--r-- | packages/core/src/agents/loader.ts | 294 |
2 files changed, 0 insertions, 305 deletions
diff --git a/packages/core/src/agents/index.ts b/packages/core/src/agents/index.ts deleted file mode 100644 index 4931162..0000000 --- a/packages/core/src/agents/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - deleteAgent, - expandAgentToolNames, - GLOBAL_AGENTS_DIR, - getAgentDirPaths, - getAgentDirs, - getProjectAgentsDir, - loadAgent, - loadAgents, - saveAgent, -} from "./loader.js"; diff --git a/packages/core/src/agents/loader.ts b/packages/core/src/agents/loader.ts deleted file mode 100644 index 9971803..0000000 --- a/packages/core/src/agents/loader.ts +++ /dev/null @@ -1,294 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml"; -import { type AgentDefinition, type AgentModelEntry, isReasoningEffort } from "../types/index.js"; - -// ─── Helpers ───────────────────────────────────────────────────── - -/** Sanitize a slug to prevent path traversal */ -function sanitizeSlug(slug: string): string { - // Strip directory components and ensure only safe characters - const base = path.basename(slug); - const clean = base - .replace(/[^a-zA-Z0-9_-]/g, "-") - .replace(/-+/g, "-") - .replace(/^-|-$/g, ""); - if (!clean) throw new Error("Invalid agent slug"); - return clean; -} - -// ─── Constants ─────────────────────────────────────────────────── - -export const GLOBAL_AGENTS_DIR = path.join(os.homedir(), ".config", "dispatch", "agents"); - -export function getProjectAgentsDir(projectDir: string): string { - return path.join(projectDir, ".dispatch", "agents"); -} - -// ─── Public API ────────────────────────────────────────────────── - -/** - * Returns the agent directories that exist or could exist. - * Always includes global. Includes project dir if projectDir is provided. - */ -export function getAgentDirs( - projectDir?: string, -): Array<{ label: string; path: string; scope: string }> { - const dirs: Array<{ label: string; path: string; scope: string }> = [ - { label: "Global (~/.config/dispatch/agents)", path: GLOBAL_AGENTS_DIR, scope: "global" }, - ]; - if (projectDir) { - dirs.push({ - label: `.dispatch/agents (${path.basename(projectDir)})`, - path: getProjectAgentsDir(projectDir), - scope: projectDir, - }); - } - return dirs; -} - -/** - * Return just the absolute filesystem paths of the agent directories. - * Used by the agent's permission gate to grant read-only access to - * these locations by default (so any agent can list/read agent - * definitions without prompting the user). - */ -export function getAgentDirPaths(projectDir?: string): string[] { - const paths = [GLOBAL_AGENTS_DIR]; - if (projectDir) paths.push(getProjectAgentsDir(projectDir)); - return paths; -} - -/** - * Load a single agent definition by slug. Searches the project-scoped - * directory first (if `projectDir` is provided), then falls back to - * the global directory. Returns `null` if no match is found. - * - * Slug matching is exact and case-sensitive; sanitization mirrors - * `saveAgent` to keep loader and writer symmetric. - */ -export function loadAgent(slug: string, projectDir?: string): AgentDefinition | null { - const safeSlug = sanitizeSlug(slug); - const agents = loadAgents(projectDir); - return agents.find((a) => a.slug === safeSlug) ?? null; -} - -/** - * Translate the short permission-group names used by `AgentDefinition.tools` - * (e.g. `"read"`, `"edit"`, `"bash"`) into the concrete tool-implementation - * names registered with the agent runtime (e.g. `"read_file"`, - * `"list_files"`, `"write_file"`, `"run_shell"`). - * - * The mapping mirrors the per-permission tool-creation paths in - * `AgentManager.getOrCreateAgentForTab` so a subagent summoned with a - * given agent definition ends up with the exact same set of registered - * tools as a top-level tab using that definition. Tool names that aren't - * group aliases (`summon`, `retrieve`, `web_search`, `youtube_transcribe`, - * `todo`) are passed through unchanged. - * - * `"todo"` is auto-included so the summoned agent always has its task list - * available, matching the parent-agent path which always registers `todo`. - */ -export function expandAgentToolNames(tools: string[]): string[] { - const expanded = new Set<string>(); - for (const t of tools) { - switch (t) { - case "read": - expanded.add("read_file"); - expanded.add("read_file_slice"); - expanded.add("list_files"); - break; - case "edit": - expanded.add("write_file"); - break; - case "bash": - expanded.add("run_shell"); - break; - default: - // Pass through tool names that aren't permission-group - // aliases (summon, retrieve, web_search, youtube_transcribe, - // send_to_tab, read_tab, todo, and the granular file tools - // themselves if a user hand-wrote them in a TOML). - expanded.add(t); - } - } - // Always include `todo` — every agent should be able to track its work, - // and the parent-agent path adds it unconditionally. - expanded.add("todo"); - return Array.from(expanded); -} - -/** - * Ensure the default global agent exists. Creates it if missing. - */ -function ensureDefaultAgent(): void { - const filePath = path.join(GLOBAL_AGENTS_DIR, "default.toml"); - if (fs.existsSync(filePath)) return; - - const defaultAgent: AgentDefinition = { - name: "Default", - description: "Default agent with all tools enabled", - skills: [], - tools: ["read", "edit", "bash", "summon"], - models: [], - scope: "global", - slug: "default", - }; - saveAgent(defaultAgent); -} - -/** - * Load all agent definitions from global + project directories. - * Auto-generates the default global agent if it doesn't exist. - */ -export function loadAgents(projectDir?: string): AgentDefinition[] { - ensureDefaultAgent(); - - const agents: AgentDefinition[] = []; - - // Global agents - agents.push(...loadAgentsFromDir(GLOBAL_AGENTS_DIR, "global")); - - // Project-scoped agents - if (projectDir) { - agents.push(...loadAgentsFromDir(getProjectAgentsDir(projectDir), projectDir)); - } - - return agents; -} - -/** - * Save (create or update) an agent definition to a TOML file. - * The scope determines which directory: - * - "global" -> ~/.config/dispatch/agents/ - * - any other string -> that directory path + /.dispatch/agents/ - */ -export function saveAgent(agent: AgentDefinition): void { - if (agent.scope !== "global" && agent.scope.includes("..")) { - throw new Error("Invalid agent scope"); - } - const dir = agent.scope === "global" ? GLOBAL_AGENTS_DIR : getProjectAgentsDir(agent.scope); - - fs.mkdirSync(dir, { recursive: true }); - - const tomlContent: Record<string, unknown> = { - name: agent.name, - description: agent.description, - skills: agent.skills, - tools: agent.tools, - }; - - if (agent.cwd) { - tomlContent.cwd = agent.cwd; - } - - if (agent.is_subagent) { - tomlContent.is_subagent = true; - } - - // smol-toml handles [[models]] array-of-tables - if (agent.models.length > 0) { - tomlContent.models = agent.models.map((m) => ({ - key_id: m.key_id, - model_id: m.model_id, - ...(m.effort ? { effort: m.effort } : {}), - })); - } - - const content = stringifyTOML(tomlContent); - const safeSlug = sanitizeSlug(agent.slug); - const filePath = path.join(dir, `${safeSlug}.toml`); - fs.writeFileSync(filePath, content, "utf-8"); -} - -/** - * Delete an agent TOML file. - */ -export function deleteAgent(slug: string, scope: string): boolean { - if (scope !== "global" && scope.includes("..")) { - throw new Error("Invalid agent scope"); - } - const dir = scope === "global" ? GLOBAL_AGENTS_DIR : getProjectAgentsDir(scope); - - const safeSlug = sanitizeSlug(slug); - const filePath = path.join(dir, `${safeSlug}.toml`); - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - return true; - } - return false; -} - -// ─── Internal ──────────────────────────────────────────────────── - -function loadAgentsFromDir(dir: string, scope: string): AgentDefinition[] { - if (!fs.existsSync(dir)) return []; - - const results: AgentDefinition[] = []; - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { - return []; - } - - for (const entry of entries) { - if (!entry.isFile() || !entry.name.endsWith(".toml")) continue; - - const filePath = path.join(dir, entry.name); - const slug = entry.name.slice(0, -5); // remove .toml - - try { - const raw = fs.readFileSync(filePath, "utf-8"); - const parsed = parseTOML(raw); - - const models: AgentModelEntry[] = []; - if (Array.isArray(parsed.models)) { - for (const m of parsed.models) { - if (m && typeof m === "object" && "key_id" in m && "model_id" in m) { - const rawEffort = (m as Record<string, unknown>).effort; - models.push({ - key_id: String((m as Record<string, unknown>).key_id), - model_id: String((m as Record<string, unknown>).model_id), - // Only carry `effort` when it's a recognised level; an - // unset or invalid value falls back to the per-tab / - // default effort at the call site. - ...(isReasoningEffort(rawEffort) ? { effort: rawEffort } : {}), - }); - } - } - } - - const skills: string[] = []; - if (Array.isArray(parsed.skills)) { - for (const s of parsed.skills) { - if (typeof s === "string") skills.push(s); - } - } - - const tools: string[] = []; - if (Array.isArray(parsed.tools)) { - for (const t of parsed.tools) { - if (typeof t === "string") tools.push(t); - } - } - - results.push({ - name: typeof parsed.name === "string" ? parsed.name : slug, - description: typeof parsed.description === "string" ? parsed.description : "", - skills, - tools, - models, - scope, - slug, - ...(typeof parsed.cwd === "string" && parsed.cwd ? { cwd: parsed.cwd } : {}), - ...(parsed.is_subagent === true ? { is_subagent: true } : {}), - }); - } catch { - // Skip unparseable files - } - } - - return results; -} |
