From 4cb87d41cb18a4f50ba3ff85f777ceca74e8b520 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 29 Jun 2026 19:01:19 -0700 Subject: [PATCH] ref(dotagents): Split agent runtime concerns Move target definitions, MCP and hook writers, and subagent runtime handling into separate modules while keeping the agents barrel as a compatibility layer. Update local agent instructions to describe the new source layout. Co-Authored-By: Codex --- AGENTS.md | 4 +- packages/dotagents/src/agents/index.ts | 30 +++--- packages/dotagents/src/cli/commands/doctor.ts | 2 +- packages/dotagents/src/cli/commands/init.ts | 2 +- .../src/cli/commands/install.test.ts | 2 +- .../src/cli/commands/install/agent-runtime.ts | 12 +-- .../src/cli/commands/install/gitignore.ts | 2 +- .../src/cli/commands/install/subagents.ts | 4 +- .../dotagents/src/cli/commands/sync.test.ts | 2 +- packages/dotagents/src/cli/commands/sync.ts | 12 +-- .../src/cli/ensure-user-scope.test.ts | 2 +- .../dotagents/src/cli/ensure-user-scope.ts | 2 +- packages/dotagents/src/config/loader.ts | 2 +- .../format.test.ts} | 94 +----------------- .../helpers.ts => subagents/format.ts} | 93 ------------------ .../identity.ts} | 0 .../store.test.ts} | 4 +- .../subagent-store.ts => subagents/store.ts} | 6 +- packages/dotagents/src/subagents/types.ts | 49 ++++++++++ .../writer.test.ts} | 4 +- .../writer.ts} | 6 +- .../{agents => targets}/definitions/claude.ts | 3 +- .../{agents => targets}/definitions/codex.ts | 3 +- .../{agents => targets}/definitions/cursor.ts | 3 +- .../src/targets/definitions/helpers.test.ts | 94 ++++++++++++++++++ .../src/targets/definitions/helpers.ts | 97 +++++++++++++++++++ .../definitions/opencode.ts | 3 +- .../{agents => targets}/definitions/vscode.ts | 0 .../src/{agents => targets}/errors.ts | 0 .../{agents => targets}/hook-writer.test.ts | 0 .../src/{agents => targets}/hook-writer.ts | 0 .../{agents => targets}/mcp-writer.test.ts | 0 .../src/{agents => targets}/mcp-writer.ts | 0 .../src/{agents => targets}/paths.test.ts | 0 .../src/{agents => targets}/paths.ts | 0 .../src/{agents => targets}/registry.test.ts | 0 .../src/{agents => targets}/registry.ts | 0 .../src/{agents => targets}/types.ts | 51 +--------- skills/dotagents-qa/SOURCES.md | 10 +- specs/SPEC.md | 22 +++-- 40 files changed, 323 insertions(+), 297 deletions(-) rename packages/dotagents/src/{agents/definitions/helpers.test.ts => subagents/format.test.ts} (56%) rename packages/dotagents/src/{agents/definitions/helpers.ts => subagents/format.ts} (55%) rename packages/dotagents/src/{agents/subagent-identity.ts => subagents/identity.ts} (100%) rename packages/dotagents/src/{agents/subagent-store.test.ts => subagents/store.test.ts} (99%) rename packages/dotagents/src/{agents/subagent-store.ts => subagents/store.ts} (99%) create mode 100644 packages/dotagents/src/subagents/types.ts rename packages/dotagents/src/{agents/subagent-writer.test.ts => subagents/writer.test.ts} (99%) rename packages/dotagents/src/{agents/subagent-writer.ts => subagents/writer.ts} (98%) rename packages/dotagents/src/{agents => targets}/definitions/claude.ts (88%) rename packages/dotagents/src/{agents => targets}/definitions/codex.ts (91%) rename packages/dotagents/src/{agents => targets}/definitions/cursor.ts (94%) create mode 100644 packages/dotagents/src/targets/definitions/helpers.test.ts create mode 100644 packages/dotagents/src/targets/definitions/helpers.ts rename packages/dotagents/src/{agents => targets}/definitions/opencode.ts (91%) rename packages/dotagents/src/{agents => targets}/definitions/vscode.ts (100%) rename packages/dotagents/src/{agents => targets}/errors.ts (100%) rename packages/dotagents/src/{agents => targets}/hook-writer.test.ts (100%) rename packages/dotagents/src/{agents => targets}/hook-writer.ts (100%) rename packages/dotagents/src/{agents => targets}/mcp-writer.test.ts (100%) rename packages/dotagents/src/{agents => targets}/mcp-writer.ts (100%) rename packages/dotagents/src/{agents => targets}/paths.test.ts (100%) rename packages/dotagents/src/{agents => targets}/paths.ts (100%) rename packages/dotagents/src/{agents => targets}/registry.test.ts (100%) rename packages/dotagents/src/{agents => targets}/registry.ts (100%) rename packages/dotagents/src/{agents => targets}/types.ts (67%) diff --git a/AGENTS.md b/AGENTS.md index 9fe8388..3098f31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,9 @@ packages/ │ ├── index.ts # Library entry point (re-exports lib symbols with @deprecated) │ ├── scope.ts # Project/user scope resolution │ ├── cli/ # CLI entry point + commands (init, install, add, remove, sync, list, mcp, doctor, trust) -│ ├── agents/ # Agent definitions, MCP/hook config writers +│ ├── targets/ # Target agent definitions plus MCP/hook config writers +│ ├── subagents/ # Subagent identity, store, and runtime writer +│ ├── agents/ # Compatibility re-export barrel for older internal imports │ ├── config/ # agents.toml schema, loader, writer │ ├── lockfile/ # agents.lock schema, loader, writer │ ├── symlinks/ # Symlink creation/management diff --git a/packages/dotagents/src/agents/index.ts b/packages/dotagents/src/agents/index.ts index 72d00fc..0ca909b 100644 --- a/packages/dotagents/src/agents/index.ts +++ b/packages/dotagents/src/agents/index.ts @@ -1,39 +1,41 @@ -export { getAgent, allAgentIds } from "./registry.js"; -export { writeMcpConfigs, verifyMcpConfigs, toMcpDeclarations, projectMcpResolver } from "./mcp-writer.js"; -export type { McpTargetResolver, McpResolvedTarget } from "./mcp-writer.js"; -export { writeHookConfigs, verifyHookConfigs, toHookDeclarations, projectHookResolver } from "./hook-writer.js"; -export type { HookTargetResolver, HookResolvedTarget } from "./hook-writer.js"; +// Compatibility barrel for older imports. Runtime ownership now lives in +// targets/ for agent targets and subagents/ for subagent handling. +export { getAgent, allAgentIds } from "../targets/registry.js"; +export { writeMcpConfigs, verifyMcpConfigs, toMcpDeclarations, projectMcpResolver } from "../targets/mcp-writer.js"; +export type { McpTargetResolver, McpResolvedTarget } from "../targets/mcp-writer.js"; +export { writeHookConfigs, verifyHookConfigs, toHookDeclarations, projectHookResolver } from "../targets/hook-writer.js"; +export type { HookTargetResolver, HookResolvedTarget } from "../targets/hook-writer.js"; export { writeSubagentConfigs, pruneSubagentConfigs, verifySubagentConfigs, projectSubagentResolver, userSubagentResolver, -} from "./subagent-writer.js"; +} from "../subagents/writer.js"; export { resolveSubagent, writeInstalledSubagents, loadInstalledSubagents, pruneInstalledSubagents, lockEntryForSubagent, -} from "./subagent-store.js"; +} from "../subagents/store.js"; export type { SubagentTargetResolver, SubagentResolvedTarget, SubagentWriteWarning, SubagentWriteResult, SubagentVerifyIssue, -} from "./subagent-writer.js"; +} from "../subagents/writer.js"; export type { SubagentResolveOptions, ResolvedSubagent, ResolvedSubagentType, InstalledSubagentLoadIssue, -} from "./subagent-store.js"; +} from "../subagents/store.js"; export type { LockedSubagent } from "../lockfile/schema.js"; -export { UnsupportedFeature } from "./errors.js"; -export { getUserMcpTarget, userMcpResolver } from "./paths.js"; -export type { UserMcpTarget } from "./paths.js"; +export { UnsupportedFeature } from "../targets/errors.js"; +export { getUserMcpTarget, userMcpResolver } from "../targets/paths.js"; +export type { UserMcpTarget } from "../targets/paths.js"; export type { AgentDefinition, McpDeclaration, @@ -42,6 +44,8 @@ export type { HookDeclaration, HookConfigSpec, HookSerializer, +} from "../targets/types.js"; +export type { SubagentDeclaration, NativeSubagentConfig, NativeSubagentContent, @@ -49,4 +53,4 @@ export type { SubagentConfigSpec, SubagentIdentityStrategy, SubagentSerializer, -} from "./types.js"; +} from "../subagents/types.js"; diff --git a/packages/dotagents/src/cli/commands/doctor.ts b/packages/dotagents/src/cli/commands/doctor.ts index af6a9b1..0b00479 100644 --- a/packages/dotagents/src/cli/commands/doctor.ts +++ b/packages/dotagents/src/cli/commands/doctor.ts @@ -10,7 +10,7 @@ import { isWildcardDep } from "../../config/schema.js"; import { loadLockfile } from "../../lockfile/loader.js"; import { writeLockfile } from "../../lockfile/writer.js"; import { verifySymlinks } from "../../symlinks/manager.js"; -import { getAgent } from "../../agents/registry.js"; +import { getAgent } from "../../targets/registry.js"; import { resolveScope, resolveDefaultScope, ScopeError, type ScopeRoot } from "../../scope.js"; import { exec } from "@sentry/dotagents-lib"; import { isInPlaceSkill } from "../../utils/fs.js"; diff --git a/packages/dotagents/src/cli/commands/init.ts b/packages/dotagents/src/cli/commands/init.ts index f8e730c..86ff0c8 100644 --- a/packages/dotagents/src/cli/commands/init.ts +++ b/packages/dotagents/src/cli/commands/init.ts @@ -6,7 +6,7 @@ import { generateDefaultConfig } from "../../config/writer.js"; import { writeAgentsGitignore, ensureRootGitignoreEntries } from "../../gitignore/writer.js"; import { ensureSkillsSymlink } from "../../symlinks/manager.js"; import { loadConfig } from "../../config/loader.js"; -import { getAgent, allAgentIds, allAgents } from "../../agents/registry.js"; +import { getAgent, allAgentIds, allAgents } from "../../targets/registry.js"; import { parseArgs } from "node:util"; import * as clack from "@clack/prompts"; import { resolveScope, isInsideGitRepo, findGitDir, type ScopeRoot } from "../../scope.js"; diff --git a/packages/dotagents/src/cli/commands/install.test.ts b/packages/dotagents/src/cli/commands/install.test.ts index c526588..a73552d 100644 --- a/packages/dotagents/src/cli/commands/install.test.ts +++ b/packages/dotagents/src/cli/commands/install.test.ts @@ -10,7 +10,7 @@ import { loadLockfile } from "../../lockfile/loader.js"; import { writeLockfile } from "../../lockfile/writer.js"; import type { Lockfile } from "../../lockfile/schema.js"; import { resolveScope } from "../../scope.js"; -import { DOTAGENTS_SUBAGENT_MARKER } from "../../agents/definitions/helpers.js"; +import { DOTAGENTS_SUBAGENT_MARKER } from "../../subagents/format.js"; const SKILL_MD = (name: string) => `--- name: ${name} diff --git a/packages/dotagents/src/cli/commands/install/agent-runtime.ts b/packages/dotagents/src/cli/commands/install/agent-runtime.ts index 55d85f6..9506661 100644 --- a/packages/dotagents/src/cli/commands/install/agent-runtime.ts +++ b/packages/dotagents/src/cli/commands/install/agent-runtime.ts @@ -1,18 +1,18 @@ import { join } from "node:path"; import type { AgentsConfig } from "../../../config/schema.js"; import type { ScopeRoot } from "../../../scope.js"; -import { getAgent } from "../../../agents/registry.js"; +import { getAgent } from "../../../targets/registry.js"; import { ensureSkillsSymlink } from "../../../symlinks/manager.js"; -import { projectMcpResolver, toMcpDeclarations, writeMcpConfigs } from "../../../agents/mcp-writer.js"; -import { projectHookResolver, toHookDeclarations, writeHookConfigs } from "../../../agents/hook-writer.js"; -import { userMcpResolver } from "../../../agents/paths.js"; +import { projectMcpResolver, toMcpDeclarations, writeMcpConfigs } from "../../../targets/mcp-writer.js"; +import { projectHookResolver, toHookDeclarations, writeHookConfigs } from "../../../targets/hook-writer.js"; +import { userMcpResolver } from "../../../targets/paths.js"; import { pruneSubagentConfigs, projectSubagentResolver, userSubagentResolver, writeSubagentConfigs, -} from "../../../agents/subagent-writer.js"; -import type { SubagentDeclaration } from "../../../agents/types.js"; +} from "../../../subagents/writer.js"; +import type { SubagentDeclaration } from "../../../subagents/types.js"; /** Writes agent skill symlinks after canonical install artifacts are ready. */ export async function writeSkillSymlinks( diff --git a/packages/dotagents/src/cli/commands/install/gitignore.ts b/packages/dotagents/src/cli/commands/install/gitignore.ts index 23026d7..9cf8bc4 100644 --- a/packages/dotagents/src/cli/commands/install/gitignore.ts +++ b/packages/dotagents/src/cli/commands/install/gitignore.ts @@ -4,7 +4,7 @@ import type { Lockfile } from "../../../lockfile/schema.js"; import type { ScopeRoot } from "../../../scope.js"; import { checkRootGitignoreEntries, writeAgentsGitignore } from "../../../gitignore/writer.js"; import { isInPlaceSkill } from "../../../utils/fs.js"; -import type { SubagentDeclaration } from "../../../agents/types.js"; +import type { SubagentDeclaration } from "../../../subagents/types.js"; export interface InstallGitignoreArtifacts { installedSkillNames: string[]; diff --git a/packages/dotagents/src/cli/commands/install/subagents.ts b/packages/dotagents/src/cli/commands/install/subagents.ts index 69f99cd..43a3841 100644 --- a/packages/dotagents/src/cli/commands/install/subagents.ts +++ b/packages/dotagents/src/cli/commands/install/subagents.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import type { AgentsConfig, SubagentConfig } from "../../../config/schema.js"; import type { Lockfile } from "../../../lockfile/schema.js"; import type { ScopeRoot } from "../../../scope.js"; -import type { SubagentDeclaration } from "../../../agents/types.js"; +import type { SubagentDeclaration } from "../../../subagents/types.js"; import { InstalledSubagentWriteError, lockEntryForSubagent, @@ -10,7 +10,7 @@ import { pruneInstalledSubagents, resolveSubagent, writeInstalledSubagents, -} from "../../../agents/subagent-store.js"; +} from "../../../subagents/store.js"; import { GitError, TrustError } from "@sentry/dotagents-lib"; import { getCacheStateDir } from "../../cache.js"; import { InstallError } from "./errors.js"; diff --git a/packages/dotagents/src/cli/commands/sync.test.ts b/packages/dotagents/src/cli/commands/sync.test.ts index 318ab3a..54da37d 100644 --- a/packages/dotagents/src/cli/commands/sync.test.ts +++ b/packages/dotagents/src/cli/commands/sync.test.ts @@ -10,7 +10,7 @@ import { writeLockfile } from "../../lockfile/writer.js"; import { loadLockfile } from "../../lockfile/loader.js"; import { loadConfig } from "../../config/loader.js"; import { resolveScope } from "../../scope.js"; -import { DOTAGENTS_SUBAGENT_MARKER } from "../../agents/definitions/helpers.js"; +import { DOTAGENTS_SUBAGENT_MARKER } from "../../subagents/format.js"; import { exec } from "@sentry/dotagents-lib"; const SKILL_MD = (name: string) => `--- diff --git a/packages/dotagents/src/cli/commands/sync.ts b/packages/dotagents/src/cli/commands/sync.ts index 7228845..21c8ce1 100644 --- a/packages/dotagents/src/cli/commands/sync.ts +++ b/packages/dotagents/src/cli/commands/sync.ts @@ -10,12 +10,12 @@ import { writeLockfile } from "../../lockfile/writer.js"; import { addSkillToConfig } from "../../config/writer.js"; import { writeAgentsGitignore, checkRootGitignoreEntries } from "../../gitignore/writer.js"; import { ensureSkillsSymlink, verifySymlinks } from "../../symlinks/manager.js"; -import { getAgent } from "../../agents/registry.js"; -import { verifyMcpConfigs, writeMcpConfigs, toMcpDeclarations, projectMcpResolver } from "../../agents/mcp-writer.js"; -import { verifyHookConfigs, writeHookConfigs, toHookDeclarations, projectHookResolver } from "../../agents/hook-writer.js"; -import { pruneSubagentConfigs, verifySubagentConfigs, writeSubagentConfigs, projectSubagentResolver, userSubagentResolver } from "../../agents/subagent-writer.js"; -import { loadInstalledSubagents, pruneInstalledSubagents } from "../../agents/subagent-store.js"; -import { userMcpResolver } from "../../agents/paths.js"; +import { getAgent } from "../../targets/registry.js"; +import { verifyMcpConfigs, writeMcpConfigs, toMcpDeclarations, projectMcpResolver } from "../../targets/mcp-writer.js"; +import { verifyHookConfigs, writeHookConfigs, toHookDeclarations, projectHookResolver } from "../../targets/hook-writer.js"; +import { pruneSubagentConfigs, verifySubagentConfigs, writeSubagentConfigs, projectSubagentResolver, userSubagentResolver } from "../../subagents/writer.js"; +import { loadInstalledSubagents, pruneInstalledSubagents } from "../../subagents/store.js"; +import { userMcpResolver } from "../../targets/paths.js"; import { resolveScope, resolveDefaultScope, ScopeError, type ScopeRoot } from "../../scope.js"; import { ensureUserScopeBootstrapped } from "../ensure-user-scope.js"; import { isInPlaceSkill, managedSkillPath } from "../../utils/fs.js"; diff --git a/packages/dotagents/src/cli/ensure-user-scope.test.ts b/packages/dotagents/src/cli/ensure-user-scope.test.ts index ef03e7a..fc0ffcb 100644 --- a/packages/dotagents/src/cli/ensure-user-scope.test.ts +++ b/packages/dotagents/src/cli/ensure-user-scope.test.ts @@ -4,7 +4,7 @@ import { readFile, rm, mkdtemp } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { ensureUserScopeBootstrapped } from "./ensure-user-scope.js"; -import { allAgentIds } from "../agents/registry.js"; +import { allAgentIds } from "../targets/registry.js"; import type { ScopeRoot } from "../scope.js"; describe("ensureUserScopeBootstrapped", () => { diff --git a/packages/dotagents/src/cli/ensure-user-scope.ts b/packages/dotagents/src/cli/ensure-user-scope.ts index 04399fc..e5eddf2 100644 --- a/packages/dotagents/src/cli/ensure-user-scope.ts +++ b/packages/dotagents/src/cli/ensure-user-scope.ts @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { mkdir, writeFile } from "node:fs/promises"; import chalk from "chalk"; import { generateDefaultConfig } from "../config/writer.js"; -import { allAgentIds } from "../agents/registry.js"; +import { allAgentIds } from "../targets/registry.js"; import type { ScopeRoot } from "../scope.js"; /** diff --git a/packages/dotagents/src/config/loader.ts b/packages/dotagents/src/config/loader.ts index 3b92ed4..8385721 100644 --- a/packages/dotagents/src/config/loader.ts +++ b/packages/dotagents/src/config/loader.ts @@ -1,7 +1,7 @@ import { readFile } from "node:fs/promises"; import { parse as parseTOML } from "smol-toml"; import { agentsConfigSchema, isWildcardDep, type AgentsConfig } from "./schema.js"; -import { allAgentIds } from "../agents/registry.js"; +import { allAgentIds } from "../targets/registry.js"; import { applyDefaultRepositorySource, parseSource } from "@sentry/dotagents-lib"; export class ConfigError extends Error { diff --git a/packages/dotagents/src/agents/definitions/helpers.test.ts b/packages/dotagents/src/subagents/format.test.ts similarity index 56% rename from packages/dotagents/src/agents/definitions/helpers.test.ts rename to packages/dotagents/src/subagents/format.test.ts index 1fe4efa..acf1903 100644 --- a/packages/dotagents/src/agents/definitions/helpers.test.ts +++ b/packages/dotagents/src/subagents/format.test.ts @@ -1,104 +1,12 @@ import { describe, it, expect } from "vitest"; import { parseMarkdownFrontmatterContent } from "@sentry/dotagents-lib"; import { - interpolateEnvRefs, - interpolateHeaders, - extractCodexHeaders, markManagedMarkdownSubagent, hasDotagentsMarkdownSubagentMarker, hasDotagentsTomlSubagentMarker, serializeMarkdownSubagent, DOTAGENTS_SUBAGENT_MARKER, -} from "./helpers.js"; - -const cursorTpl = (k: string) => `\${env:${k}}`; - -describe("interpolateEnvRefs", () => { - - it("passes through strings with no refs", () => { - expect(interpolateEnvRefs("Bearer tok", cursorTpl)).toBe("Bearer tok"); - }); - - it("replaces a single ref", () => { - expect(interpolateEnvRefs("${API_KEY}", cursorTpl)).toBe("${env:API_KEY}"); - }); - - it("replaces ref in a mixed string", () => { - expect(interpolateEnvRefs("Bearer ${TOKEN}", cursorTpl)).toBe("Bearer ${env:TOKEN}"); - }); - - it("replaces multiple refs", () => { - expect(interpolateEnvRefs("${A}:${B}", cursorTpl)).toBe("${env:A}:${env:B}"); - }); - - it("ignores non-matching patterns", () => { - // No braces, empty braces, leading digit, hyphen in name - for (const s of ["$NOBRACES", "${}", "${123}", "${foo-bar}"]) { - expect(interpolateEnvRefs(s, cursorTpl)).toBe(s); - } - }); - - it("replaces underscore-prefixed and numeric-suffixed vars", () => { - expect(interpolateEnvRefs("${_FOO_2}", cursorTpl)).toBe("${env:_FOO_2}"); - }); -}); - -describe("interpolateHeaders", () => { - it("transforms all header values", () => { - const headers = { "X-Key": "${KEY}", "Authorization": "Bearer ${TOKEN}" }; - const result = interpolateHeaders(headers, (k) => `{env:${k}}`); - expect(result).toEqual({ "X-Key": "{env:KEY}", "Authorization": "Bearer {env:TOKEN}" }); - }); - - it("returns undefined for undefined input", () => { - const noHeaders: Record | undefined = undefined; - expect(interpolateHeaders(noHeaders, (k) => k)).toBeUndefined(); - }); -}); - -describe("extractCodexHeaders", () => { - it("maps pure env ref to envHttpHeaders", () => { - const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ - "X-Api-Key": "${GOOGLE_API_KEY}", - }); - expect(httpHeaders).toBeUndefined(); - expect(envHttpHeaders).toEqual({ GOOGLE_API_KEY: "X-Api-Key" }); - }); - - it("keeps static values in httpHeaders", () => { - const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ - Authorization: "Bearer tok", - }); - expect(httpHeaders).toEqual({ Authorization: "Bearer tok" }); - expect(envHttpHeaders).toBeUndefined(); - }); - - it("keeps mixed values in httpHeaders as literal fallback", () => { - const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ - Authorization: "Bearer ${TOKEN}", - }); - expect(httpHeaders).toEqual({ Authorization: "Bearer ${TOKEN}" }); - expect(envHttpHeaders).toBeUndefined(); - }); - - it("splits mixed and pure refs correctly", () => { - const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ - "X-Api-Key": "${API_KEY}", - Authorization: "Bearer tok", - "X-Mixed": "prefix ${VAR} suffix", - }); - expect(envHttpHeaders).toEqual({ API_KEY: "X-Api-Key" }); - expect(httpHeaders).toEqual({ - Authorization: "Bearer tok", - "X-Mixed": "prefix ${VAR} suffix", - }); - }); - - it("returns empty object for undefined input", () => { - const noHeaders: Record | undefined = undefined; - expect(extractCodexHeaders(noHeaders)).toEqual({}); - }); -}); +} from "./format.js"; describe("serializeMarkdownSubagent", () => { it("serializes simple frontmatter fields", () => { diff --git a/packages/dotagents/src/agents/definitions/helpers.ts b/packages/dotagents/src/subagents/format.ts similarity index 55% rename from packages/dotagents/src/agents/definitions/helpers.ts rename to packages/dotagents/src/subagents/format.ts index 14f3b5c..1686877 100644 --- a/packages/dotagents/src/agents/definitions/helpers.ts +++ b/packages/dotagents/src/subagents/format.ts @@ -1,5 +1,4 @@ import { stringify as tomlStringify } from "smol-toml"; -import type { McpDeclaration, HookDeclaration } from "../types.js"; export const DOTAGENTS_SUBAGENT_MARKER = "Generated by dotagents. Edit agents.toml instead."; const DOTAGENTS_SUBAGENT_MARKER_RE = escapeRegExp(DOTAGENTS_SUBAGENT_MARKER); @@ -10,98 +9,6 @@ const MANAGED_TOML_SUBAGENT_RE = new RegExp( `^\\uFEFF?# ${DOTAGENTS_SUBAGENT_MARKER_RE}\\r?\\n`, ); -export function envRecord( - env: string[] | undefined, - template: (key: string) => string, -): Record | undefined { - if (!env || env.length === 0) {return undefined;} - const rec: Record = {}; - for (const key of env) {rec[key] = template(key);} - return rec; -} - -const ENV_REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g; - -export function interpolateEnvRefs( - value: string, - template: (varName: string) => string, -): string { - return value.replace(ENV_REF_RE, (_match, varName: string) => template(varName)); -} - -export function interpolateHeaders( - headers: Record | undefined, - template: (varName: string) => string, -): Record | undefined { - if (!headers) {return undefined;} - const result: Record = {}; - for (const [key, value] of Object.entries(headers)) { - result[key] = interpolateEnvRefs(value, template); - } - return result; -} - -/** - * Split headers for Codex's model: pure `${VAR}` refs go to `envHttpHeaders` - * (mapping env var name → header name), everything else stays in `httpHeaders`. - * Mixed values like `"Bearer ${TOKEN}"` can't be represented in Codex's - * env_http_headers format and fall through as literal strings in httpHeaders. - */ -export function extractCodexHeaders( - headers: Record | undefined, -): { httpHeaders?: Record; envHttpHeaders?: Record } { - if (!headers) {return {};} - let httpHeaders: Record | undefined; - let envHttpHeaders: Record | undefined; - const pureRefRe = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/; - for (const [key, value] of Object.entries(headers)) { - const match = pureRefRe.exec(value); - if (match) { - (envHttpHeaders ??= {})[match[1]!] = key; - } else { - (httpHeaders ??= {})[key] = value; - } - } - return { httpHeaders, envHttpHeaders }; -} - -export function httpServer( - s: McpDeclaration, - type?: string, - template?: (varName: string) => string, -): [string, unknown] { - const tpl = template ?? ((k: string) => `\${${k}}`); - return [ - s.name, - { - ...(type && { type }), - url: interpolateEnvRefs(s.url!, tpl), - ...(s.headers && { headers: interpolateHeaders(s.headers, tpl) }), - }, - ]; -} - -/** - * Serialize hooks into Claude Code / VS Code settings.json format. - * - * Output shape: - * { - * "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] }], - * "Stop": [{ "hooks": [{ "type": "command", "command": "..." }] }] - * } - */ -export function serializeClaudeHooks(hooks: HookDeclaration[]): Record { - const result: Record = {}; - for (const h of hooks) { - const entry = { - ...(h.matcher && { matcher: h.matcher }), - hooks: [{ type: "command", command: h.command }], - }; - (result[h.event] ??= []).push(entry); - } - return result; -} - export function serializeMarkdownSubagent( fields: Record, instructions: string, diff --git a/packages/dotagents/src/agents/subagent-identity.ts b/packages/dotagents/src/subagents/identity.ts similarity index 100% rename from packages/dotagents/src/agents/subagent-identity.ts rename to packages/dotagents/src/subagents/identity.ts diff --git a/packages/dotagents/src/agents/subagent-store.test.ts b/packages/dotagents/src/subagents/store.test.ts similarity index 99% rename from packages/dotagents/src/agents/subagent-store.test.ts rename to packages/dotagents/src/subagents/store.test.ts index 1d8044c..af56687 100644 --- a/packages/dotagents/src/agents/subagent-store.test.ts +++ b/packages/dotagents/src/subagents/store.test.ts @@ -9,8 +9,8 @@ import { pruneInstalledSubagents, resolveSubagent, writeInstalledSubagents, -} from "./subagent-store.js"; -import { DOTAGENTS_SUBAGENT_MARKER, markManagedMarkdownSubagent } from "./definitions/helpers.js"; +} from "./store.js"; +import { DOTAGENTS_SUBAGENT_MARKER, markManagedMarkdownSubagent } from "./format.js"; import type { SubagentConfig } from "../config/schema.js"; const SUBAGENT_MD = (name: string) => `--- diff --git a/packages/dotagents/src/agents/subagent-store.ts b/packages/dotagents/src/subagents/store.ts similarity index 99% rename from packages/dotagents/src/agents/subagent-store.ts rename to packages/dotagents/src/subagents/store.ts index 87e7ab1..7307f98 100644 --- a/packages/dotagents/src/agents/subagent-store.ts +++ b/packages/dotagents/src/subagents/store.ts @@ -14,9 +14,9 @@ import { type RepositorySource, type TrustPolicy, } from "@sentry/dotagents-lib"; -import { hasDotagentsMarkdownSubagentMarker, serializeMarkdownSubagent } from "./definitions/helpers.js"; -import { getAgent } from "./registry.js"; -import { subagentIdentityFromMarkdownMeta } from "./subagent-identity.js"; +import { getAgent } from "../targets/registry.js"; +import { hasDotagentsMarkdownSubagentMarker, serializeMarkdownSubagent } from "./format.js"; +import { subagentIdentityFromMarkdownMeta } from "./identity.js"; import { SUBAGENT_NAME_PATTERN, type SubagentConfig } from "../config/schema.js"; import type { LockedSubagent } from "../lockfile/schema.js"; import type { diff --git a/packages/dotagents/src/subagents/types.ts b/packages/dotagents/src/subagents/types.ts new file mode 100644 index 0000000..9bc39f2 --- /dev/null +++ b/packages/dotagents/src/subagents/types.ts @@ -0,0 +1,49 @@ +import type { SubagentConfig } from "../config/schema.js"; + +/** + * Universal subagent declaration loaded from an installed subagent Markdown file. + */ +export interface SubagentDeclaration { + name: string; + description: string; + instructions: string; + targets?: SubagentConfig["targets"]; + native?: NativeSubagentContent; +} + +export type NativeSubagentTarget = "claude" | "cursor" | "codex" | "opencode"; + +/** Raw source content in the runtime's native subagent format. */ +export type NativeSubagentConfig = string; + +export type NativeSubagentContent = Partial>; + +export type SubagentIdentityStrategy = + | "frontmatter-name" + | "frontmatter-name-or-filename" + | "filename" + | "toml-name"; + +/** + * Describes where an agent stores custom subagent definitions. + */ +export interface SubagentConfigSpec { + /** Project-scope directory, relative to project root */ + projectDir: string; + /** User-scope directory, absolute path */ + userDir: string; + /** Generated file extension, including the leading dot */ + fileExtension: ".md" | ".toml"; + /** Runtime-specific rule for identifying a subagent artifact */ + identity: SubagentIdentityStrategy; + /** Transforms a universal subagent declaration into an agent-specific file */ + serialize: SubagentSerializer; +} + +/** + * Transforms a universal SubagentDeclaration into a runtime-specific file. + */ +export type SubagentSerializer = (subagent: SubagentDeclaration) => { + fileName: string; + content: string; +}; diff --git a/packages/dotagents/src/agents/subagent-writer.test.ts b/packages/dotagents/src/subagents/writer.test.ts similarity index 99% rename from packages/dotagents/src/agents/subagent-writer.test.ts rename to packages/dotagents/src/subagents/writer.test.ts index b9b55cf..ab124bb 100644 --- a/packages/dotagents/src/agents/subagent-writer.test.ts +++ b/packages/dotagents/src/subagents/writer.test.ts @@ -9,8 +9,8 @@ import { pruneSubagentConfigs, verifySubagentConfigs, writeSubagentConfigs, -} from "./subagent-writer.js"; -import { DOTAGENTS_SUBAGENT_MARKER } from "./definitions/helpers.js"; +} from "./writer.js"; +import { DOTAGENTS_SUBAGENT_MARKER } from "./format.js"; import type { SubagentDeclaration } from "./types.js"; const SUBAGENT: SubagentDeclaration = { diff --git a/packages/dotagents/src/agents/subagent-writer.ts b/packages/dotagents/src/subagents/writer.ts similarity index 98% rename from packages/dotagents/src/agents/subagent-writer.ts rename to packages/dotagents/src/subagents/writer.ts index 3f16e91..c69ff8d 100644 --- a/packages/dotagents/src/agents/subagent-writer.ts +++ b/packages/dotagents/src/subagents/writer.ts @@ -1,9 +1,9 @@ import { existsSync } from "node:fs"; import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { getAgent } from "./registry.js"; -import { hasDotagentsMarkdownSubagentMarker, hasDotagentsTomlSubagentMarker } from "./definitions/helpers.js"; -import { generatedSubagentIdentity, readSubagentFileIdentity } from "./subagent-identity.js"; +import { getAgent } from "../targets/registry.js"; +import { hasDotagentsMarkdownSubagentMarker, hasDotagentsTomlSubagentMarker } from "./format.js"; +import { generatedSubagentIdentity, readSubagentFileIdentity } from "./identity.js"; import type { SubagentConfigSpec, SubagentDeclaration } from "./types.js"; export interface SubagentResolvedTarget { diff --git a/packages/dotagents/src/agents/definitions/claude.ts b/packages/dotagents/src/targets/definitions/claude.ts similarity index 88% rename from packages/dotagents/src/agents/definitions/claude.ts rename to packages/dotagents/src/targets/definitions/claude.ts index d9e5a13..ceda04d 100644 --- a/packages/dotagents/src/agents/definitions/claude.ts +++ b/packages/dotagents/src/targets/definitions/claude.ts @@ -1,7 +1,8 @@ import { join } from "node:path"; import { homedir } from "node:os"; import type { AgentDefinition } from "../types.js"; -import { envRecord, httpServer, markManagedMarkdownSubagent, serializeClaudeHooks, serializeMarkdownSubagent } from "./helpers.js"; +import { envRecord, httpServer, serializeClaudeHooks } from "./helpers.js"; +import { markManagedMarkdownSubagent, serializeMarkdownSubagent } from "../../subagents/format.js"; const claude: AgentDefinition = { id: "claude", diff --git a/packages/dotagents/src/agents/definitions/codex.ts b/packages/dotagents/src/targets/definitions/codex.ts similarity index 91% rename from packages/dotagents/src/agents/definitions/codex.ts rename to packages/dotagents/src/targets/definitions/codex.ts index e02bc0a..7a553c7 100644 --- a/packages/dotagents/src/agents/definitions/codex.ts +++ b/packages/dotagents/src/targets/definitions/codex.ts @@ -3,7 +3,8 @@ import { homedir } from "node:os"; import type { AgentDefinition } from "../types.js"; import { UnsupportedFeature } from "../errors.js"; import claude from "./claude.js"; -import { envRecord, extractCodexHeaders, markManagedTomlSubagent, serializeCodexSubagent } from "./helpers.js"; +import { envRecord, extractCodexHeaders } from "./helpers.js"; +import { markManagedTomlSubagent, serializeCodexSubagent } from "../../subagents/format.js"; const codex: AgentDefinition = { ...claude, diff --git a/packages/dotagents/src/agents/definitions/cursor.ts b/packages/dotagents/src/targets/definitions/cursor.ts similarity index 94% rename from packages/dotagents/src/agents/definitions/cursor.ts rename to packages/dotagents/src/targets/definitions/cursor.ts index a3eaf90..2548f1d 100644 --- a/packages/dotagents/src/agents/definitions/cursor.ts +++ b/packages/dotagents/src/targets/definitions/cursor.ts @@ -3,7 +3,8 @@ import { homedir } from "node:os"; import type { AgentDefinition, HookDeclaration } from "../types.js"; import type { HookEvent } from "../../config/schema.js"; import claude from "./claude.js"; -import { httpServer, markManagedMarkdownSubagent, serializeMarkdownSubagent } from "./helpers.js"; +import { httpServer } from "./helpers.js"; +import { markManagedMarkdownSubagent, serializeMarkdownSubagent } from "../../subagents/format.js"; /** * Maps universal hook events to Cursor event names. diff --git a/packages/dotagents/src/targets/definitions/helpers.test.ts b/packages/dotagents/src/targets/definitions/helpers.test.ts new file mode 100644 index 0000000..fef2aef --- /dev/null +++ b/packages/dotagents/src/targets/definitions/helpers.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { + extractCodexHeaders, + interpolateEnvRefs, + interpolateHeaders, +} from "./helpers.js"; + +const cursorTpl = (key: string) => `\${env:${key}}`; + +describe("interpolateEnvRefs", () => { + it("passes through strings with no refs", () => { + expect(interpolateEnvRefs("Bearer tok", cursorTpl)).toBe("Bearer tok"); + }); + + it("replaces a single ref", () => { + expect(interpolateEnvRefs("${API_KEY}", cursorTpl)).toBe("${env:API_KEY}"); + }); + + it("replaces ref in a mixed string", () => { + expect(interpolateEnvRefs("Bearer ${TOKEN}", cursorTpl)).toBe("Bearer ${env:TOKEN}"); + }); + + it("replaces multiple refs", () => { + expect(interpolateEnvRefs("${A}:${B}", cursorTpl)).toBe("${env:A}:${env:B}"); + }); + + it("ignores non-matching patterns", () => { + // No braces, empty braces, leading digit, hyphen in name + for (const s of ["$NOBRACES", "${}", "${123}", "${foo-bar}"]) { + expect(interpolateEnvRefs(s, cursorTpl)).toBe(s); + } + }); + + it("replaces underscore-prefixed and numeric-suffixed vars", () => { + expect(interpolateEnvRefs("${_FOO_2}", cursorTpl)).toBe("${env:_FOO_2}"); + }); +}); + +describe("interpolateHeaders", () => { + it("transforms all header values", () => { + const headers = { "X-Key": "${KEY}", "Authorization": "Bearer ${TOKEN}" }; + const result = interpolateHeaders(headers, (key) => `{env:${key}}`); + expect(result).toEqual({ "X-Key": "{env:KEY}", Authorization: "Bearer {env:TOKEN}" }); + }); + + it("returns undefined for undefined input", () => { + const noHeaders: Record | undefined = undefined; + expect(interpolateHeaders(noHeaders, (key) => key)).toBeUndefined(); + }); +}); + +describe("extractCodexHeaders", () => { + it("maps pure env ref to envHttpHeaders", () => { + const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ + "X-Api-Key": "${GOOGLE_API_KEY}", + }); + expect(httpHeaders).toBeUndefined(); + expect(envHttpHeaders).toEqual({ GOOGLE_API_KEY: "X-Api-Key" }); + }); + + it("keeps static values in httpHeaders", () => { + const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ + Authorization: "Bearer tok", + }); + expect(httpHeaders).toEqual({ Authorization: "Bearer tok" }); + expect(envHttpHeaders).toBeUndefined(); + }); + + it("keeps mixed values in httpHeaders as literal fallback", () => { + const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ + Authorization: "Bearer ${TOKEN}", + }); + expect(httpHeaders).toEqual({ Authorization: "Bearer ${TOKEN}" }); + expect(envHttpHeaders).toBeUndefined(); + }); + + it("splits mixed and pure refs correctly", () => { + const { httpHeaders, envHttpHeaders } = extractCodexHeaders({ + "X-Api-Key": "${API_KEY}", + Authorization: "Bearer tok", + "X-Mixed": "prefix ${VAR} suffix", + }); + expect(envHttpHeaders).toEqual({ API_KEY: "X-Api-Key" }); + expect(httpHeaders).toEqual({ + Authorization: "Bearer tok", + "X-Mixed": "prefix ${VAR} suffix", + }); + }); + + it("returns empty object for undefined input", () => { + const noHeaders: Record | undefined = undefined; + expect(extractCodexHeaders(noHeaders)).toEqual({}); + }); +}); diff --git a/packages/dotagents/src/targets/definitions/helpers.ts b/packages/dotagents/src/targets/definitions/helpers.ts new file mode 100644 index 0000000..93f5314 --- /dev/null +++ b/packages/dotagents/src/targets/definitions/helpers.ts @@ -0,0 +1,97 @@ +import type { HookDeclaration, McpDeclaration } from "../types.js"; + +/** Build an agent-specific environment map from declared variable names. */ +export function envRecord( + env: string[] | undefined, + template: (key: string) => string, +): Record | undefined { + if (!env || env.length === 0) {return undefined;} + const rec: Record = {}; + for (const key of env) {rec[key] = template(key);} + return rec; +} + +const ENV_REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g; + +/** Rewrite `${VAR}` references into the target agent's environment syntax. */ +export function interpolateEnvRefs( + value: string, + template: (varName: string) => string, +): string { + return value.replace(ENV_REF_RE, (_match, varName: string) => template(varName)); +} + +/** Rewrite all header values with the target agent's environment syntax. */ +export function interpolateHeaders( + headers: Record | undefined, + template: (varName: string) => string, +): Record | undefined { + if (!headers) {return undefined;} + const result: Record = {}; + for (const [key, value] of Object.entries(headers)) { + result[key] = interpolateEnvRefs(value, template); + } + return result; +} + +/** + * Split headers for Codex's model: pure `${VAR}` refs go to `envHttpHeaders` + * (mapping env var name to header name), everything else stays in `httpHeaders`. + * Mixed values like `"Bearer ${TOKEN}"` can't be represented in Codex's + * env_http_headers format and fall through as literal strings in httpHeaders. + */ +export function extractCodexHeaders( + headers: Record | undefined, +): { httpHeaders?: Record; envHttpHeaders?: Record } { + if (!headers) {return {};} + let httpHeaders: Record | undefined; + let envHttpHeaders: Record | undefined; + const pureRefRe = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/; + for (const [key, value] of Object.entries(headers)) { + const match = pureRefRe.exec(value); + if (match) { + (envHttpHeaders ??= {})[match[1]!] = key; + } else { + (httpHeaders ??= {})[key] = value; + } + } + return { httpHeaders, envHttpHeaders }; +} + +/** Serialize an HTTP MCP server into the target's JSON-compatible shape. */ +export function httpServer( + s: McpDeclaration, + type?: string, + template?: (varName: string) => string, +): [string, unknown] { + const tpl = template ?? ((k: string) => `\${${k}}`); + return [ + s.name, + { + ...(type && { type }), + url: interpolateEnvRefs(s.url!, tpl), + ...(s.headers && { headers: interpolateHeaders(s.headers, tpl) }), + }, + ]; +} + +/** + * Serialize hooks into Claude Code / VS Code settings.json format. + * + * Output shape: + * { + * "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] }], + * "Stop": [{ "hooks": [{ "type": "command", "command": "..." }] }] + * } + */ +export function serializeClaudeHooks(hooks: HookDeclaration[]): Record { + const result: Record = {}; + for (const h of hooks) { + const entry = { + ...(h.matcher && { matcher: h.matcher }), + hooks: [{ type: "command", command: h.command }], + }; + (result[h.event] ??= []).push(entry); + } + return result; +} diff --git a/packages/dotagents/src/agents/definitions/opencode.ts b/packages/dotagents/src/targets/definitions/opencode.ts similarity index 91% rename from packages/dotagents/src/agents/definitions/opencode.ts rename to packages/dotagents/src/targets/definitions/opencode.ts index 7e8b231..0e60bf3 100644 --- a/packages/dotagents/src/agents/definitions/opencode.ts +++ b/packages/dotagents/src/targets/definitions/opencode.ts @@ -2,7 +2,8 @@ import { join } from "node:path"; import { homedir } from "node:os"; import type { AgentDefinition } from "../types.js"; import { UnsupportedFeature } from "../errors.js"; -import { envRecord, httpServer, markManagedMarkdownSubagent, serializeMarkdownSubagent } from "./helpers.js"; +import { envRecord, httpServer } from "./helpers.js"; +import { markManagedMarkdownSubagent, serializeMarkdownSubagent } from "../../subagents/format.js"; const opencode: AgentDefinition = { id: "opencode", diff --git a/packages/dotagents/src/agents/definitions/vscode.ts b/packages/dotagents/src/targets/definitions/vscode.ts similarity index 100% rename from packages/dotagents/src/agents/definitions/vscode.ts rename to packages/dotagents/src/targets/definitions/vscode.ts diff --git a/packages/dotagents/src/agents/errors.ts b/packages/dotagents/src/targets/errors.ts similarity index 100% rename from packages/dotagents/src/agents/errors.ts rename to packages/dotagents/src/targets/errors.ts diff --git a/packages/dotagents/src/agents/hook-writer.test.ts b/packages/dotagents/src/targets/hook-writer.test.ts similarity index 100% rename from packages/dotagents/src/agents/hook-writer.test.ts rename to packages/dotagents/src/targets/hook-writer.test.ts diff --git a/packages/dotagents/src/agents/hook-writer.ts b/packages/dotagents/src/targets/hook-writer.ts similarity index 100% rename from packages/dotagents/src/agents/hook-writer.ts rename to packages/dotagents/src/targets/hook-writer.ts diff --git a/packages/dotagents/src/agents/mcp-writer.test.ts b/packages/dotagents/src/targets/mcp-writer.test.ts similarity index 100% rename from packages/dotagents/src/agents/mcp-writer.test.ts rename to packages/dotagents/src/targets/mcp-writer.test.ts diff --git a/packages/dotagents/src/agents/mcp-writer.ts b/packages/dotagents/src/targets/mcp-writer.ts similarity index 100% rename from packages/dotagents/src/agents/mcp-writer.ts rename to packages/dotagents/src/targets/mcp-writer.ts diff --git a/packages/dotagents/src/agents/paths.test.ts b/packages/dotagents/src/targets/paths.test.ts similarity index 100% rename from packages/dotagents/src/agents/paths.test.ts rename to packages/dotagents/src/targets/paths.test.ts diff --git a/packages/dotagents/src/agents/paths.ts b/packages/dotagents/src/targets/paths.ts similarity index 100% rename from packages/dotagents/src/agents/paths.ts rename to packages/dotagents/src/targets/paths.ts diff --git a/packages/dotagents/src/agents/registry.test.ts b/packages/dotagents/src/targets/registry.test.ts similarity index 100% rename from packages/dotagents/src/agents/registry.test.ts rename to packages/dotagents/src/targets/registry.test.ts diff --git a/packages/dotagents/src/agents/registry.ts b/packages/dotagents/src/targets/registry.ts similarity index 100% rename from packages/dotagents/src/agents/registry.ts rename to packages/dotagents/src/targets/registry.ts diff --git a/packages/dotagents/src/agents/types.ts b/packages/dotagents/src/targets/types.ts similarity index 67% rename from packages/dotagents/src/agents/types.ts rename to packages/dotagents/src/targets/types.ts index 9518aec..bef9c70 100644 --- a/packages/dotagents/src/agents/types.ts +++ b/packages/dotagents/src/targets/types.ts @@ -1,4 +1,5 @@ -import type { HookEvent, SubagentConfig } from "../config/schema.js"; +import type { HookEvent } from "../config/schema.js"; +import type { SubagentConfigSpec } from "../subagents/types.js"; /** * Universal MCP server declaration from agents.toml [[mcp]] sections. @@ -73,54 +74,6 @@ export interface HookConfigSpec { */ export type HookSerializer = (hooks: HookDeclaration[]) => unknown; -/** - * Universal subagent declaration loaded from an installed subagent Markdown file. - */ -export interface SubagentDeclaration { - name: string; - description: string; - instructions: string; - targets?: SubagentConfig["targets"]; - native?: NativeSubagentContent; -} - -export type NativeSubagentTarget = "claude" | "cursor" | "codex" | "opencode"; - -/** Raw source content in the runtime's native subagent format. */ -export type NativeSubagentConfig = string; - -export type NativeSubagentContent = Partial>; - -export type SubagentIdentityStrategy = - | "frontmatter-name" - | "frontmatter-name-or-filename" - | "filename" - | "toml-name"; - -/** - * Describes where an agent stores custom subagent definitions. - */ -export interface SubagentConfigSpec { - /** Project-scope directory, relative to project root */ - projectDir: string; - /** User-scope directory, absolute path */ - userDir: string; - /** Generated file extension, including the leading dot */ - fileExtension: ".md" | ".toml"; - /** Runtime-specific rule for identifying a subagent artifact */ - identity: SubagentIdentityStrategy; - /** Transforms a universal subagent declaration into an agent-specific file */ - serialize: SubagentSerializer; -} - -/** - * Transforms a universal SubagentDeclaration into a runtime-specific file. - */ -export type SubagentSerializer = (subagent: SubagentDeclaration) => { - fileName: string; - content: string; -}; - /** * Definition of an agent tool that dotagents manages. */ diff --git a/skills/dotagents-qa/SOURCES.md b/skills/dotagents-qa/SOURCES.md index b1011d4..d862603 100644 --- a/skills/dotagents-qa/SOURCES.md +++ b/skills/dotagents-qa/SOURCES.md @@ -7,11 +7,11 @@ | `AGENTS.md` | pnpm and validation conventions | | `package.json` | `pnpm build` and `pnpm check` scripts | | `specs/SPEC.md` | canonical `.agents/skills/`, agent symlink, MCP, hook, install, sync, list, and doctor behavior | -| `packages/dotagents/src/agents/definitions/claude.ts` | Claude project skills and generated config paths | -| `packages/dotagents/src/agents/definitions/cursor.ts` | Cursor shares `.claude/skills` and writes Cursor-specific MCP/hook config | -| `packages/dotagents/src/agents/definitions/vscode.ts` | VS Code reads `.agents/skills` natively and writes `.vscode/mcp.json` plus shared Claude-style hooks | -| `packages/dotagents/src/agents/definitions/codex.ts` | Codex reads `.agents/skills` natively and writes `.codex/config.toml` | -| `packages/dotagents/src/agents/definitions/opencode.ts` | OpenCode reads `.agents/skills` natively and writes `opencode.json` | +| `packages/dotagents/src/targets/definitions/claude.ts` | Claude project skills and generated config paths | +| `packages/dotagents/src/targets/definitions/cursor.ts` | Cursor shares `.claude/skills` and writes Cursor-specific MCP/hook config | +| `packages/dotagents/src/targets/definitions/vscode.ts` | VS Code reads `.agents/skills` natively and writes `.vscode/mcp.json` plus shared Claude-style hooks | +| `packages/dotagents/src/targets/definitions/codex.ts` | Codex reads `.agents/skills` natively and writes `.codex/config.toml` | +| `packages/dotagents/src/targets/definitions/opencode.ts` | OpenCode reads `.agents/skills` natively and writes `opencode.json` | | `packages/dotagents/src/cli/cache.ts` | `DOTAGENTS_STATE_DIR` cache isolation | | `packages/dotagents/src/cli/update-notifier.ts` | `HOME` isolation for update-check cache | | `skills/dotagents/SKILL.md` | sibling skill layout | diff --git a/specs/SPEC.md b/specs/SPEC.md index 9771504..f44ff7b 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -774,14 +774,22 @@ dotagents/ mcp.ts trust.ts doctor.ts + targets/ + types.ts # Target agent interfaces and MCP/hook declarations + registry.ts # Target registry (claude, cursor, codex, vscode, opencode) + definitions/ # Per-target definitions + mcp-writer.ts # MCP config file generation per target + hook-writer.ts # Hook config file generation per target + paths.ts # Target config path resolution + errors.ts # Target-specific error types + subagents/ + types.ts # Subagent declaration and runtime projection types + identity.ts # Runtime subagent identity parsing + store.ts # Canonical installed subagent loading/writing + writer.ts # Runtime subagent config generation + format.ts # Subagent serialization and managed markers agents/ - types.ts # McpDeclaration, AgentDefinition interfaces - registry.ts # Agent registry (claude, cursor, codex, vscode, opencode) - definitions/ # Per-agent definitions - mcp-writer.ts # MCP config file generation per agent - hook-writer.ts # Hook config file generation per agent - paths.ts # Agent config path resolution - errors.ts # Agent-specific error types + index.ts # Compatibility re-export barrel config/ # agents.toml schema, loader, writer lockfile/ # agents.lock schema, loader, writer symlinks/ # Symlink create/verify/repair