@ryodeushii/ai-product-team-agents 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -132,71 +132,69 @@ Models are defined in `models.yml`. Three tiers:
132
132
  - **executing** — roles that search and run tasks (Haiku)
133
133
  - **creative** — roles that write copy and design UI (Sonnet, swap to Gemini etc.)
134
134
 
135
+ ### Presets
136
+
137
+ Switch all tiers at once with a named preset:
138
+
139
+ ```yaml
140
+ preset: google # gemini 2.5 pro for thinking/creative, flash for executing
141
+ ```
142
+
143
+ | Preset | thinking | executing | creative |
144
+ |---|---|---|---|
145
+ | `balanced` | claude-sonnet-4-6 | claude-haiku-4-5 | claude-sonnet-4-6 |
146
+ | `fast` | claude-haiku-4-5 | claude-haiku-4-5 | claude-haiku-4-5 |
147
+ | `google` | gemini-2.5-pro | gemini-2.5-flash | gemini-2.5-pro |
148
+ | `openai` | gpt-4.1 | gpt-4.1-mini | gpt-4.1 |
149
+
135
150
  ### Per-project overrides
136
151
 
137
- In your project's `.agents.yml`, override any tier or individual role:
152
+ Mix a preset with per-role overrides:
153
+
154
+ ```yaml
155
+ preset: google
156
+ models:
157
+ roles:
158
+ explorer: local/qwen2.5-coder # override one role on top of preset
159
+ ```
160
+
161
+ Or override individual tiers without a preset:
138
162
 
139
163
  ```yaml
140
164
  models:
141
165
  defaults:
142
- thinking: anthropic/claude-sonnet-4-6 # all thinking-tier roles
143
- executing: anthropic/claude-haiku-4-5-20251001 # all executing-tier roles
144
- creative: google/gemini-2.5-pro # all creative-tier roles
166
+ creative: google/gemini-2.5-pro # swap just the creative tier
145
167
  roles:
146
- architect: openai/gpt-4.1 # one specific role
147
- explorer: local/qwen2.5-coder # local model via OpenCode
168
+ architect: openai/gpt-4.1 # one specific role
148
169
  ```
149
170
 
150
171
  Resolution order (highest to lowest priority):
151
172
  1. Project role override
152
173
  2. Project tier default
153
- 3. Framework role default
154
- 4. Framework tier default
155
-
156
- Models are resolved **dynamically** — changes to `models.yml` or `.agents.yml` take effect immediately without re-running setup.
174
+ 3. Preset tier
175
+ 4. Framework role default
176
+ 5. Framework tier default
157
177
 
158
- ### MCP server
178
+ ### Auto-update on startup (Claude Code)
159
179
 
160
- The framework ships an MCP server that exposes a `resolve_model(role, project_root)` tool for dynamic model resolution.
180
+ The setup script writes a `SessionStart` hook into `.claude/settings.json` that re-runs `--update` every time a Claude Code session starts. This means changes to `.agents.yml` (preset, model overrides) take effect automatically the next time you open Claude Code — no manual `--update` needed.
161
181
 
162
- Add to your Claude Code MCP config (`~/.claude/mcp.json` or project-level `.mcp.json`):
182
+ The hook looks like this in `.claude/settings.json`:
163
183
 
164
184
  ```json
165
185
  {
166
- "mcpServers": {
167
- "agents-framework": {
168
- "command": "bunx",
169
- "args": ["--package=@ryodeushii/ai-product-team-agents", "agents-mcp"]
170
- }
186
+ "hooks": {
187
+ "SessionStart": [
188
+ {
189
+ "matcher": "startup",
190
+ "hooks": [{ "type": "command", "command": "bunx --bun @ryodeushii/ai-product-team-agents --update 2>/dev/null; true" }]
191
+ }
192
+ ]
171
193
  }
172
194
  }
173
195
  ```
174
196
 
175
- Or with npx:
176
-
177
- ```json
178
- {
179
- "mcpServers": {
180
- "agents-framework": {
181
- "command": "npx",
182
- "args": ["-y", "-p", "@ryodeushii/ai-product-team-agents", "agents-mcp"]
183
- }
184
- }
185
- }
186
- ```
187
-
188
- **OpenCode** — add to `opencode.json` (project root or `~/.config/opencode/opencode.json`):
189
-
190
- ```json
191
- {
192
- "mcp": {
193
- "agents-framework": {
194
- "type": "local",
195
- "command": ["npx", "-y", "-p", "@ryodeushii/ai-product-team-agents", "agents-mcp"]
196
- }
197
- }
198
- }
199
- ```
197
+ **OpenCode** uses a local plugin instead. The setup script writes `.opencode/plugins/agents-auto-update.ts`, which hooks into `session.created` and runs `--update` automatically at the start of every OpenCode session.
200
198
 
201
199
  ---
202
200
 
package/models.yml CHANGED
@@ -16,3 +16,28 @@ roles:
16
16
  seo: anthropic/claude-haiku-4-5-20251001
17
17
  designer: anthropic/claude-sonnet-4-6
18
18
  marketing: anthropic/claude-sonnet-4-6
19
+
20
+ presets:
21
+ # Anthropic — balanced quality/cost (framework default)
22
+ balanced:
23
+ thinking: anthropic/claude-sonnet-4-6
24
+ executing: anthropic/claude-haiku-4-5-20251001
25
+ creative: anthropic/claude-sonnet-4-6
26
+
27
+ # Anthropic — all fast/cheap
28
+ fast:
29
+ thinking: anthropic/claude-haiku-4-5-20251001
30
+ executing: anthropic/claude-haiku-4-5-20251001
31
+ creative: anthropic/claude-haiku-4-5-20251001
32
+
33
+ # Google Gemini
34
+ google:
35
+ thinking: google/gemini-2.5-pro
36
+ executing: google/gemini-2.5-flash
37
+ creative: google/gemini-2.5-pro
38
+
39
+ # OpenAI
40
+ openai:
41
+ thinking: openai/gpt-4.1
42
+ executing: openai/gpt-4.1-mini
43
+ creative: openai/gpt-4.1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@ryodeushii/ai-product-team-agents",
4
- "version": "0.0.6",
4
+ "version": "0.0.7",
5
5
  "repository": {
6
6
  "url": "https://github.com/ryodeushii/ai-product-team-agents"
7
7
  },
@@ -16,13 +16,11 @@
16
16
  "src/config/schema.ts",
17
17
  "src/models/resolver.ts",
18
18
  "src/models/types.ts",
19
- "src/mcp/server.ts",
20
19
  "src/setup/index.ts"
21
20
  ],
22
21
  "bin": {
23
22
  "ai-product-team-agents": "./src/setup/index.ts",
24
- "agents-setup": "./src/setup/index.ts",
25
- "agents-mcp": "./src/mcp/server.ts"
23
+ "agents-setup": "./src/setup/index.ts"
26
24
  },
27
25
  "scripts": {
28
26
  "build": "tsc",
@@ -38,7 +36,6 @@
38
36
  "typescript": "^5.9.3"
39
37
  },
40
38
  "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.27.1",
42
39
  "js-yaml": "^4.1.1",
43
40
  "zod": "^4.3.6"
44
41
  }
@@ -14,6 +14,7 @@ export const frameworkConfigSchema = z.object({
14
14
  creative: z.string(),
15
15
  }),
16
16
  roles: z.record(z.string(), z.string()).default({}),
17
+ presets: z.record(z.string(), tierOverrideSchema).default({}),
17
18
  });
18
19
 
19
20
  export const projectConfigSchema = z.object({
@@ -25,6 +26,7 @@ export const projectConfigSchema = z.object({
25
26
  .enum(["supervised", "checkpoint", "autonomous"])
26
27
  .default("checkpoint"),
27
28
  deploy: z.string().optional(),
29
+ preset: z.string().optional(),
28
30
  models: z
29
31
  .object({
30
32
  defaults: tierOverrideSchema.optional(),
@@ -7,6 +7,7 @@ export function resolveModel(
7
7
  role: Role,
8
8
  framework: ModelsConfig,
9
9
  project: ProjectModelsOverride = {},
10
+ preset?: string,
10
11
  ): string {
11
12
  // 1. project role override
12
13
  if (project.roles?.[role] !== undefined) return project.roles[role]!;
@@ -15,10 +16,18 @@ export function resolveModel(
15
16
  const tier = ROLE_TIERS[role];
16
17
  if (project.defaults?.[tier] !== undefined) return project.defaults[tier]!;
17
18
 
18
- // 3. framework role default
19
+ // 3. preset tier
20
+ if (preset !== undefined) {
21
+ const presetConfig = framework.presets[preset];
22
+ if (presetConfig === undefined)
23
+ throw new Error(`Unknown preset "${preset}"`);
24
+ if (presetConfig[tier] !== undefined) return presetConfig[tier]!;
25
+ }
26
+
27
+ // 4. framework role default
19
28
  if (framework.roles[role] !== undefined) return framework.roles[role]!;
20
29
 
21
- // 4. framework tier default
30
+ // 5. framework tier default
22
31
  const result = framework.defaults[tier];
23
32
  if (result === undefined)
24
33
  throw new Error(`No model configured for tier "${tier}"`);
@@ -36,6 +36,7 @@ export const ROLE_TIERS: Record<Role, Tier> = {
36
36
  export interface ModelsConfig {
37
37
  defaults: Record<Tier, string>;
38
38
  roles: Partial<Record<Role, string>>;
39
+ presets: Record<string, Partial<Record<Tier, string>>>;
39
40
  }
40
41
 
41
42
  export interface ProjectModelsOverride {
@@ -2,15 +2,18 @@
2
2
  // src/setup/index.ts
3
3
 
4
4
  import { fileURLToPath } from "node:url";
5
- import {
6
- lstat,
7
- mkdir,
8
- readFile,
9
- rm,
10
- symlink,
11
- writeFile,
12
- } from "fs/promises";
5
+ import { lstat, mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
13
6
  import { join } from "path";
7
+ import { loadFrameworkConfig, loadProjectConfig } from "../config/loader";
8
+ import { resolveModel } from "../models/resolver";
9
+ import { ROLES } from "../models/types";
10
+ import type { Role } from "../models/types";
11
+
12
+ // The SessionStart hook command written into .claude/settings.json.
13
+ // Runs --update on every Claude Code session start so model overrides
14
+ // from .agents.yml are always applied without manual intervention.
15
+ const HOOK_COMMAND =
16
+ "bunx --bun @ryodeushii/ai-product-team-agents --update 2>/dev/null; true";
14
17
 
15
18
  interface SetupOptions {
16
19
  projectName: string;
@@ -40,20 +43,23 @@ export async function setupProject(
40
43
  await writeFile(dest, content, "utf-8");
41
44
  };
42
45
 
43
- const link = async (target: string, linkPath: string) => {
44
- try {
45
- await lstat(linkPath);
46
- if (!overwrite) return;
47
- await rm(linkPath, { recursive: true, force: true });
48
- } catch (err) {
49
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
50
- }
51
- await mkdir(join(linkPath, ".."), { recursive: true });
52
- await symlink(target, linkPath);
53
- };
54
-
55
- // CLAUDE.md — one-liner import
56
- await write(join(projectRoot, "CLAUDE.md"), "@./AGENTS.md\n");
46
+ // CLAUDE.md append import if not already present
47
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
48
+ const claudeImport = "@./AGENTS.md";
49
+ const existingClaude = await readFile(claudeMdPath, "utf-8").catch(
50
+ (err: NodeJS.ErrnoException) =>
51
+ err.code === "ENOENT" ? null : Promise.reject(err),
52
+ );
53
+ if (existingClaude === null) {
54
+ await writeFile(claudeMdPath, `${claudeImport}\n`, "utf-8");
55
+ } else if (!existingClaude.includes(claudeImport)) {
56
+ const sep = existingClaude.endsWith("\n") ? "" : "\n";
57
+ await writeFile(
58
+ claudeMdPath,
59
+ `${existingClaude}${sep}${claudeImport}\n`,
60
+ "utf-8",
61
+ );
62
+ }
57
63
 
58
64
  // AGENTS.md — fill template
59
65
  let agentsTpl: string;
@@ -88,23 +94,161 @@ export async function setupProject(
88
94
  agentsYml = agentsYml.replaceAll("{{project_name}}", projectName);
89
95
  await write(join(projectRoot, ".agents.yml"), agentsYml);
90
96
 
91
- // symlink roles into .claude/agents and .opencode/agents
97
+ // copy role files into .claude/agents and .opencode/agents,
98
+ // injecting resolved model: into frontmatter for each role
92
99
  const rolesPath = join(frameworkRoot, "roles");
93
- await link(rolesPath, join(projectRoot, ".claude/agents"));
94
- await link(rolesPath, join(projectRoot, ".opencode/agents"));
100
+ const [framework, project] = await Promise.all([
101
+ loadFrameworkConfig(frameworkRoot),
102
+ loadProjectConfig(projectRoot),
103
+ ]);
104
+
105
+ const roleFiles = (await readdir(rolesPath)).filter((f) => f.endsWith(".md"));
106
+ for (const dest of [
107
+ join(projectRoot, ".claude/agents"),
108
+ join(projectRoot, ".opencode/agents"),
109
+ ]) {
110
+ await mkdir(dest, { recursive: true });
111
+ for (const file of roleFiles) {
112
+ const destFile = join(dest, file);
113
+ const exists = await lstat(destFile).then(
114
+ () => true,
115
+ () => false,
116
+ );
117
+ if (exists && !overwrite) continue;
118
+
119
+ let content = await readFile(join(rolesPath, file), "utf-8");
120
+ const roleName = file.replace(/\.md$/, "") as Role;
121
+ if (ROLES.includes(roleName)) {
122
+ const model = resolveModel(
123
+ roleName,
124
+ framework,
125
+ project.models ?? {},
126
+ project.preset,
127
+ );
128
+ // inject or replace model: in frontmatter
129
+ if (content.startsWith("---")) {
130
+ content = content.replace(
131
+ /^(---\n)([\s\S]*?)(---)/,
132
+ (_, open, body, close) => {
133
+ const cleaned = body.replace(/^model:.*\n?/m, "");
134
+ return `${open}${cleaned}model: ${model}\n${close}`;
135
+ },
136
+ );
137
+ }
138
+ }
139
+ await writeFile(destFile, content, "utf-8");
140
+ }
141
+ }
142
+
143
+ // .claude/settings.json — add SessionStart hook for auto-update
144
+ await installClaudeHook(join(projectRoot, ".claude/settings.json"));
145
+
146
+ // .opencode/plugins/agents-auto-update.ts — OpenCode session.created hook
147
+ await installOpenCodePlugin(projectRoot, frameworkRoot);
148
+ }
149
+
150
+ async function installOpenCodePlugin(
151
+ projectRoot: string,
152
+ frameworkRoot: string,
153
+ ): Promise<void> {
154
+ const pluginDir = join(projectRoot, ".opencode/plugins");
155
+ const pluginDest = join(pluginDir, "agents-auto-update.ts");
156
+ await mkdir(pluginDir, { recursive: true });
157
+ const src = await readFile(
158
+ join(frameworkRoot, "templates/opencode-plugin.ts"),
159
+ "utf-8",
160
+ );
161
+ await writeFile(pluginDest, src, "utf-8");
162
+ }
163
+
164
+ async function installClaudeHook(settingsPath: string): Promise<void> {
165
+ const raw = await readFile(settingsPath, "utf-8").catch(
166
+ (err: NodeJS.ErrnoException) =>
167
+ err.code === "ENOENT" ? null : Promise.reject(err),
168
+ );
169
+ const settings = raw ? JSON.parse(raw) : {};
170
+
171
+ // idempotent: don't add duplicate hook
172
+ const existing: { matcher: string; hooks: { type: string; command: string }[] }[] =
173
+ settings.hooks?.SessionStart ?? [];
174
+ const alreadyInstalled = existing.some((entry) =>
175
+ entry.hooks?.some((h) => h.command === HOOK_COMMAND),
176
+ );
177
+ if (alreadyInstalled) return;
178
+
179
+ settings.hooks ??= {};
180
+ settings.hooks.SessionStart ??= [];
181
+ settings.hooks.SessionStart.push({
182
+ matcher: "startup",
183
+ hooks: [{ type: "command", command: HOOK_COMMAND }],
184
+ });
185
+
186
+ await mkdir(settingsPath.replace(/\/[^/]+$/, ""), { recursive: true });
187
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
188
+ }
189
+
190
+ async function removeClaudeHook(settingsPath: string): Promise<void> {
191
+ const raw = await readFile(settingsPath, "utf-8").catch(
192
+ (err: NodeJS.ErrnoException) =>
193
+ err.code === "ENOENT" ? null : Promise.reject(err),
194
+ );
195
+ if (!raw) return;
196
+
197
+ const settings = JSON.parse(raw);
198
+ if (!settings.hooks?.SessionStart) return;
199
+
200
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
201
+ (entry: { hooks?: { command: string }[] }) =>
202
+ !entry.hooks?.some((h) => h.command === HOOK_COMMAND),
203
+ );
204
+ if (settings.hooks.SessionStart.length === 0) {
205
+ delete settings.hooks.SessionStart;
206
+ }
207
+ if (Object.keys(settings.hooks).length === 0) {
208
+ delete settings.hooks;
209
+ }
210
+
211
+ if (Object.keys(settings).length === 0) {
212
+ await rm(settingsPath, { force: true });
213
+ } else {
214
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
215
+ }
95
216
  }
96
217
 
97
218
  export async function uninstallProject(projectRoot: string): Promise<void> {
98
- const files = [
219
+ // Remove framework-owned files outright
220
+ for (const f of [
99
221
  join(projectRoot, "AGENTS.md"),
100
- join(projectRoot, "CLAUDE.md"),
101
222
  join(projectRoot, ".agents.yml"),
102
223
  join(projectRoot, ".claude/agents"),
103
224
  join(projectRoot, ".opencode/agents"),
104
- ];
105
- for (const f of files) {
225
+ join(projectRoot, ".opencode/plugins/agents-auto-update.ts"),
226
+ ]) {
106
227
  await rm(f, { recursive: true, force: true });
107
228
  }
229
+
230
+ // CLAUDE.md — remove only the import line, keep the rest
231
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
232
+ const content = await readFile(claudeMdPath, "utf-8").catch(
233
+ (err: NodeJS.ErrnoException) =>
234
+ err.code === "ENOENT" ? null : Promise.reject(err),
235
+ );
236
+ if (content !== null) {
237
+ const updated = content
238
+ .split("\n")
239
+ .filter((l) => l.trim() !== "@./AGENTS.md")
240
+ .join("\n")
241
+ .replace(/\n{3,}/g, "\n\n")
242
+ .trim();
243
+ if (updated.length === 0) {
244
+ await rm(claudeMdPath, { force: true });
245
+ } else {
246
+ await writeFile(claudeMdPath, `${updated}\n`, "utf-8");
247
+ }
248
+ }
249
+
250
+ // .claude/settings.json — remove the SessionStart hook
251
+ await removeClaudeHook(join(projectRoot, ".claude/settings.json"));
108
252
  }
109
253
 
110
254
  // CLI entrypoint
@@ -24,8 +24,15 @@ autonomy: checkpoint # supervised | checkpoint | autonomous
24
24
  # Resolution order (highest → lowest priority):
25
25
  # 1. models.roles.<role> — this project, specific role
26
26
  # 2. models.defaults.<tier> — this project, whole tier
27
- # 3. framework roles default — models.yml roles section
28
- # 4. framework tier default — models.yml defaults section
27
+ # 3. preset.<tier> — named preset from models.yml
28
+ # 4. framework roles default — models.yml roles section
29
+ # 5. framework tier default — models.yml defaults section
30
+ #
31
+ # Presets (switch all tiers at once):
32
+ # preset: balanced # default — anthropic sonnet/haiku
33
+ # preset: fast # all haiku — cheap and quick
34
+ # preset: google # gemini 2.5 pro/flash
35
+ # preset: openai # gpt-4.1/mini
29
36
  #
30
37
  # models:
31
38
  # defaults:
@@ -0,0 +1,16 @@
1
+ // Auto-generated by @ryodeushii/ai-product-team-agents
2
+ // Runs --update on session start so model overrides in .agents.yml
3
+ // are always applied without manual intervention.
4
+ // To remove: bunx @ryodeushii/ai-product-team-agents --uninstall
5
+
6
+ export const AgentsAutoUpdate = async ({
7
+ $,
8
+ }: {
9
+ $: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>;
10
+ }) => {
11
+ return {
12
+ "session.created": async () => {
13
+ await ($ as any)`bunx --bun @ryodeushii/ai-product-team-agents --update`.quiet().nothrow();
14
+ },
15
+ };
16
+ };
package/src/mcp/server.ts DELETED
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env bun
2
- // src/mcp/server.ts
3
-
4
- import { fileURLToPath } from "node:url";
5
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { z } from "zod";
8
- import { loadFrameworkConfig, loadProjectConfig } from "../config/loader";
9
- import { resolveModel } from "../models/resolver";
10
- import type { Role } from "../models/types";
11
- import { ROLES } from "../models/types";
12
-
13
- const FRAMEWORK_ROOT = fileURLToPath(new URL("../../", import.meta.url));
14
-
15
- export async function resolveModelForProject(
16
- role: string,
17
- projectRoot: string,
18
- frameworkRoot = FRAMEWORK_ROOT,
19
- ): Promise<string> {
20
- if (!ROLES.includes(role as Role)) throw new Error(`Unknown role: ${role}`);
21
- const [framework, project] = await Promise.all([
22
- loadFrameworkConfig(frameworkRoot),
23
- loadProjectConfig(projectRoot),
24
- ]);
25
- return resolveModel(role as Role, framework, project.models ?? {});
26
- }
27
-
28
- export function createMcpServer(): McpServer {
29
- const server = new McpServer({
30
- name: "agents-framework",
31
- version: "0.1.0",
32
- });
33
-
34
- server.tool(
35
- "resolve_model",
36
- "Resolve the model for an agent role, applying project overrides",
37
- {
38
- role: z.string().describe("Agent role name"),
39
- project_root: z.string().describe("Absolute path to target project root"),
40
- },
41
- async ({ role, project_root }) => {
42
- try {
43
- const model = await resolveModelForProject(role, project_root);
44
- return { content: [{ type: "text", text: model }] };
45
- } catch (err) {
46
- const message = err instanceof Error ? err.message : String(err);
47
- return { content: [{ type: "text", text: message }], isError: true };
48
- }
49
- },
50
- );
51
-
52
- return server;
53
- }
54
-
55
- // entrypoint when run directly
56
- if (import.meta.main) {
57
- const server = createMcpServer();
58
- const transport = new StdioServerTransport();
59
- await server.connect(transport);
60
- }