agentcraft 0.0.1

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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +7 -0
  2. package/README.md +95 -0
  3. package/bin/agentcraft.js +12 -0
  4. package/commands/agentcraft.md +46 -0
  5. package/hooks/hooks.json +13 -0
  6. package/hooks/play-sound.sh +69 -0
  7. package/opencode.js +87 -0
  8. package/package.json +19 -0
  9. package/screenshot.jpg +0 -0
  10. package/social-share-og.jpg +0 -0
  11. package/social-share-tw.jpg +0 -0
  12. package/social-share.png +0 -0
  13. package/web/README.md +36 -0
  14. package/web/app/api/agents/[name]/route.ts +44 -0
  15. package/web/app/api/agents/route.ts +62 -0
  16. package/web/app/api/assignments/route.ts +53 -0
  17. package/web/app/api/audio/[...path]/route.ts +35 -0
  18. package/web/app/api/health/route.ts +5 -0
  19. package/web/app/api/preview/route.ts +22 -0
  20. package/web/app/api/skills/route.ts +83 -0
  21. package/web/app/api/sounds/route.ts +106 -0
  22. package/web/app/api/ui-sounds/route.ts +44 -0
  23. package/web/app/favicon.ico +0 -0
  24. package/web/app/globals.css +99 -0
  25. package/web/app/icon.svg +13 -0
  26. package/web/app/layout.tsx +19 -0
  27. package/web/app/opengraph-image.tsx +96 -0
  28. package/web/app/page.tsx +268 -0
  29. package/web/bun.lock +292 -0
  30. package/web/components/agent-form.tsx +190 -0
  31. package/web/components/agent-roster-panel.tsx +502 -0
  32. package/web/components/assignment-log-panel.tsx +109 -0
  33. package/web/components/hook-slot.tsx +149 -0
  34. package/web/components/hud-header.tsx +135 -0
  35. package/web/components/sound-browser-panel.tsx +206 -0
  36. package/web/components/sound-unit.tsx +203 -0
  37. package/web/components/ui-sounds-modal.tsx +308 -0
  38. package/web/lib/types.ts +87 -0
  39. package/web/lib/ui-audio.ts +126 -0
  40. package/web/lib/utils.ts +98 -0
  41. package/web/next.config.ts +8 -0
  42. package/web/package.json +37 -0
  43. package/web/postcss.config.mjs +1 -0
  44. package/web/public/file.svg +1 -0
  45. package/web/public/fonts/starcraft-normal.ttf +0 -0
  46. package/web/public/globe.svg +1 -0
  47. package/web/public/next.svg +1 -0
  48. package/web/public/vercel.svg +1 -0
  49. package/web/public/window.svg +1 -0
  50. package/web/tsconfig.json +34 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "agentcraft",
3
+ "version": "0.0.8",
4
+ "description": "Assign sounds to AI coding agent lifecycle events. Works with Claude Code and OpenCode.",
5
+ "author": { "name": "rohenaz" },
6
+ "keywords": ["sounds", "hooks", "audio", "productivity", "opencode", "claude-code"]
7
+ }
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # AgentCraft
2
+
3
+ Assign sounds to your AI coding agent's lifecycle events. Configure everything through an SC2-inspired dashboard — drag sounds onto hook slots, pick a UI theme, and hear your agents come alive.
4
+
5
+ Works with **Claude Code** and **OpenCode**.
6
+
7
+ ![AgentCraft dashboard](screenshot.jpg)
8
+
9
+ ## How It Works
10
+
11
+ **1. Run the skill**
12
+
13
+ From any Claude Code session:
14
+
15
+ ```
16
+ /agentcraft
17
+ ```
18
+
19
+ **2. Sounds download automatically**
20
+
21
+ On first launch, AgentCraft clones the [agentcraft-sounds](https://github.com/rohenaz/agentcraft-sounds) library to `~/.agentcraft/sounds/`. No manual setup required.
22
+
23
+ **3. The dashboard opens at `http://localhost:4040`**
24
+
25
+ A local web UI starts in the background. It stays running between sessions so subsequent `/agentcraft` calls open instantly.
26
+
27
+ **4. Assign sounds to hook points and skill calls**
28
+
29
+ - **AGENTS tab** — Expand **GLOBAL OVERRIDE** or any agent row. Click a hook slot (SESSION START, STOP, TOOL FAILURE, etc.) to enter select mode, then click any sound in the browser to assign it. Or drag a sound card directly onto a slot.
30
+ - **SKILLS tab** — Assign sounds to individual skill invocations. Each skill has an **ON INVOKE** and **ON COMPLETE** slot.
31
+ - Hit **ESTABLISH UPLINK** to save.
32
+
33
+ From then on, Claude Code plays your sounds automatically as it works — no dashboard needed.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ claude plugin install agentcraft@rohenaz
39
+ ```
40
+
41
+ ## Prerequisites
42
+
43
+ - [Bun](https://bun.sh) — runs the web UI
44
+ - [jq](https://jqlang.github.io/jq/) — JSON parsing in the hook script
45
+ - [git](https://git-scm.com) — downloads the sound library on first run
46
+ - macOS: `afplay` (built in) · Linux: `paplay` or `aplay`
47
+
48
+ ## Hooks
49
+
50
+ | Event | When it fires |
51
+ |-------|--------------|
52
+ | `SessionStart` | New session begins |
53
+ | `SessionEnd` | Session ends |
54
+ | `Stop` | Claude finishes a response |
55
+ | `SubagentStop` | A subagent completes |
56
+ | `Notification` | Claude sends a notification |
57
+ | `PreCompact` | Context is about to be compacted |
58
+ | `PreToolUse (Skill)` | A skill is invoked |
59
+ | `PostToolUse (Skill)` | A skill completes |
60
+
61
+ Sounds can be set globally (fires for all agents) or per-agent (overrides global for that agent).
62
+
63
+ ## UI Sound Themes
64
+
65
+ The **UI SFX** dropdown in the header controls ambient sounds that play as you use the dashboard. Click **⚙** to customize individual slots per theme.
66
+
67
+ | Theme | Style |
68
+ |-------|-------|
69
+ | SC2 | StarCraft II — crisp, digital |
70
+ | WC3 | Warcraft III — warm, fantasy |
71
+ | FF7 | Final Fantasy VII — retro RPG |
72
+ | FF9 | Final Fantasy IX — soft, minimal |
73
+ | OFF | No UI sounds |
74
+
75
+ ## Sound Library
76
+
77
+ Sounds live at `~/.agentcraft/sounds/`. Any `.mp3`, `.wav`, `.ogg`, or `.m4a` file you drop there appears in the browser automatically. To update the library:
78
+
79
+ ```bash
80
+ git -C ~/.agentcraft/sounds pull
81
+ ```
82
+
83
+ ## Storage
84
+
85
+ | Path | Contents |
86
+ |------|----------|
87
+ | `~/.agentcraft/assignments.json` | Your sound assignments |
88
+ | `~/.agentcraft/sounds/` | Sound library |
89
+
90
+ ## Managing the Plugin
91
+
92
+ ```bash
93
+ claude plugin update agentcraft@rohenaz # Update to latest
94
+ claude plugin uninstall agentcraft@rohenaz # Remove
95
+ ```
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // AgentCraft CLI — https://github.com/rohenaz/agentcraft
5
+ // Full implementation coming in a future release.
6
+ // Install the Claude Code plugin: claude plugin install agentcraft@rohenaz
7
+
8
+ const [,, cmd, ...args] = process.argv;
9
+
10
+ console.log('AgentCraft CLI v0.0.1');
11
+ console.log('Full CLI coming soon. Install the Claude Code plugin:');
12
+ console.log(' claude plugin install agentcraft@rohenaz');
@@ -0,0 +1,46 @@
1
+ ---
2
+ allowed-tools: Bash(lsof:*), Bash(open:*), Bash(bun:*), Bash(curl:*), Bash(git:*), Bash(ls:*), Bash(mkdir:*)
3
+ description: Open the AgentCraft sound assignment dashboard. Launches web UI on port 4040.
4
+ argument-hint: "[--stop] Stop the AgentCraft server"
5
+ ---
6
+
7
+ Open the AgentCraft sound assignment dashboard.
8
+
9
+ If user passed --stop argument, kill the server instead:
10
+ ```bash
11
+ kill $(lsof -ti:4040) 2>/dev/null
12
+ echo "AgentCraft server stopped."
13
+ ```
14
+
15
+ Otherwise, ensure the sound library is present. Check if ~/.agentcraft/sounds exists and has files:
16
+ ```bash
17
+ ls ~/.agentcraft/sounds 2>/dev/null | head -1
18
+ ```
19
+
20
+ If that returned nothing (empty or missing), clone the sound library:
21
+ ```bash
22
+ git clone https://github.com/rohenaz/agentcraft-sounds ~/.agentcraft/sounds
23
+ ```
24
+
25
+ Check if server is already running:
26
+ ```bash
27
+ lsof -ti:4040
28
+ ```
29
+
30
+ If NOT running, start it:
31
+ ```bash
32
+ cd "$CLAUDE_PLUGIN_ROOT/web" && bun install --silent && bun dev --port 4040 &
33
+ ```
34
+
35
+ Poll until server responds (max 15 attempts, 1s apart):
36
+ ```bash
37
+ for i in {1..15}; do
38
+ curl -s http://localhost:4040/api/health | grep -q '"ok"' && break
39
+ sleep 1
40
+ done
41
+ ```
42
+
43
+ Then open in browser:
44
+ ```bash
45
+ open http://localhost:4040
46
+ ```
@@ -0,0 +1,13 @@
1
+ {
2
+ "description": "AgentCraft: play sounds on Claude Code lifecycle events",
3
+ "hooks": {
4
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 5 }] }],
5
+ "SessionEnd": [{ "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 5 }] }],
6
+ "Stop": [{ "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 3 }] }],
7
+ "SubagentStop": [{ "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 3 }] }],
8
+ "Notification": [{ "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 3 }] }],
9
+ "PreCompact": [{ "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 3 }] }],
10
+ "PreToolUse": [{ "matcher": "Skill", "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 3 }] }],
11
+ "PostToolUse": [{ "matcher": "Skill", "hooks": [{ "type": "command", "command": "bash $CLAUDE_PLUGIN_ROOT/hooks/play-sound.sh", "timeout": 3 }] }]
12
+ }
13
+ }
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ # AgentCraft hook - plays assigned sound for this event/agent/skill
3
+ CONFIG="$HOME/.agentcraft/assignments.json"
4
+ LIBRARY="$HOME/.agentcraft/sounds"
5
+
6
+ # Read stdin with 2s timeout. 'timeout' isn't available by default on macOS,
7
+ # so use perl's alarm() which is always present.
8
+ INPUT=$(perl -e '$SIG{ALRM}=sub{exit 0}; alarm 2; local $/; $d=<STDIN>; print $d if $d' 2>/dev/null)
9
+ [ -z "$INPUT" ] && INPUT='{}'
10
+ EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
11
+ AGENT=$(echo "$INPUT" | jq -r '.agent_type // empty')
12
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
13
+
14
+ [ -z "$EVENT" ] && exit 0
15
+ [ ! -f "$CONFIG" ] && exit 0
16
+
17
+ ENABLED=$(jq -r '.settings.enabled // true' "$CONFIG")
18
+ [ "$ENABLED" = "false" ] && exit 0
19
+
20
+ # Deduplicate: prevent the same event from firing within 3 seconds.
21
+ # Claude Code fires SessionStart twice on resume (process init + session restore).
22
+ LOCKFILE="/tmp/agentcraft-${EVENT}.lock"
23
+ NOW=$(date +%s)
24
+ if [ -f "$LOCKFILE" ]; then
25
+ LAST=$(cat "$LOCKFILE" 2>/dev/null || echo 0)
26
+ if [ $((NOW - LAST)) -lt 3 ]; then
27
+ exit 0
28
+ fi
29
+ fi
30
+ echo "$NOW" > "$LOCKFILE"
31
+
32
+ SOUND=""
33
+
34
+ # Skill-specific lookup: PreToolUse/PostToolUse when tool_name=Skill
35
+ # tool_input.skill is the qualified name, e.g. "plugin-dev:hook-development" or "ask-gemini"
36
+ if [ "$TOOL_NAME" = "Skill" ]; then
37
+ SKILL_KEY=$(echo "$INPUT" | jq -r '.tool_input.skill // empty')
38
+ if [ -n "$SKILL_KEY" ]; then
39
+ SKILL_ENABLED=$(jq -r --arg s "$SKILL_KEY" '.skills[$s].enabled // true' "$CONFIG")
40
+ if [ "$SKILL_ENABLED" = "true" ]; then
41
+ SOUND=$(jq -r --arg s "$SKILL_KEY" --arg e "$EVENT" '.skills[$s].hooks[$e] // empty' "$CONFIG")
42
+ fi
43
+ fi
44
+ fi
45
+
46
+ # Agent-specific lookup
47
+ if [ -z "$SOUND" ] && [ -n "$AGENT" ]; then
48
+ AGENT_ENABLED=$(jq -r --arg a "$AGENT" '.agents[$a].enabled // true' "$CONFIG")
49
+ if [ "$AGENT_ENABLED" = "true" ]; then
50
+ SOUND=$(jq -r --arg a "$AGENT" --arg e "$EVENT" '.agents[$a].hooks[$e] // empty' "$CONFIG")
51
+ fi
52
+ fi
53
+
54
+ # Global fallback
55
+ [ -z "$SOUND" ] && SOUND=$(jq -r --arg e "$EVENT" '.global[$e] // empty' "$CONFIG")
56
+ [ -z "$SOUND" ] && exit 0
57
+
58
+ FULL="$LIBRARY/$SOUND"
59
+ [ ! -f "$FULL" ] && exit 0
60
+
61
+ if [[ "$OSTYPE" == "darwin"* ]]; then
62
+ afplay "$FULL" &
63
+ elif command -v paplay &>/dev/null; then
64
+ paplay "$FULL" &
65
+ elif command -v aplay &>/dev/null; then
66
+ aplay "$FULL" &
67
+ fi
68
+
69
+ exit 0
package/opencode.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * AgentCraft — OpenCode plugin
3
+ *
4
+ * Plays sounds on OpenCode lifecycle events using the same assignments.json
5
+ * config as the Claude Code plugin. Shared config lives at:
6
+ * ~/.claude/sounds/assignments.json
7
+ *
8
+ * To install for OpenCode, symlink or copy this file to:
9
+ * ~/.config/opencode/plugins/agentcraft.js (global)
10
+ * .opencode/plugins/agentcraft.js (project)
11
+ */
12
+
13
+ import { readFileSync, existsSync } from 'fs';
14
+ import { execSync } from 'child_process';
15
+ import { join } from 'path';
16
+ import { homedir } from 'os';
17
+
18
+ const CONFIG_PATH = join(homedir(), '.claude', 'sounds', 'assignments.json');
19
+ const LIBRARY_PATH = join(homedir(), 'code', 'claude-sounds');
20
+
21
+ function getAssignments() {
22
+ try {
23
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function play(soundPath) {
30
+ if (!soundPath) return;
31
+ const full = join(LIBRARY_PATH, soundPath);
32
+ if (!existsSync(full)) return;
33
+ try {
34
+ if (process.platform === 'darwin') execSync(`afplay "${full}" &`, { stdio: 'ignore' });
35
+ else if (process.platform === 'linux') execSync(`paplay "${full}" &`, { stdio: 'ignore' });
36
+ } catch { /* non-blocking, ignore errors */ }
37
+ }
38
+
39
+ function playEvent(a, event) {
40
+ if (!a || a.settings?.enabled === false) return;
41
+ play(a.global?.[event]);
42
+ }
43
+
44
+ export const AgentCraft = async () => {
45
+ return {
46
+ // Session start → SessionStart
47
+ 'session.created': async () => {
48
+ playEvent(getAssignments(), 'SessionStart');
49
+ },
50
+
51
+ // Agent idle/done → Stop
52
+ 'session.idle': async () => {
53
+ playEvent(getAssignments(), 'Stop');
54
+ },
55
+
56
+ // Context compacted → PreCompact
57
+ 'session.compacted': async () => {
58
+ playEvent(getAssignments(), 'PreCompact');
59
+ },
60
+
61
+ // Tool about to run → PreToolUse (skills only)
62
+ 'tool.execute.before': async (input) => {
63
+ const a = getAssignments();
64
+ if (!a || a.settings?.enabled === false) return;
65
+
66
+ if (input.tool === 'skill') {
67
+ const key = input.args?.skill;
68
+ if (!key) return;
69
+ const cfg = a.skills?.[key];
70
+ if (cfg && cfg.enabled !== false) play(cfg.hooks?.PreToolUse);
71
+ }
72
+ },
73
+
74
+ // Tool finished → PostToolUse (skills only)
75
+ 'tool.execute.after': async (input) => {
76
+ const a = getAssignments();
77
+ if (!a || a.settings?.enabled === false) return;
78
+
79
+ if (input.tool === 'skill') {
80
+ const key = input.args?.skill;
81
+ if (!key) return;
82
+ const cfg = a.skills?.[key];
83
+ if (cfg && cfg.enabled !== false) play(cfg.hooks?.PostToolUse);
84
+ }
85
+ },
86
+ };
87
+ };
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "agentcraft",
3
+ "version": "0.0.1",
4
+ "description": "Assign sounds to AI coding agent lifecycle events. CLI for managing sound packs.",
5
+ "license": "MIT",
6
+ "author": "rohenaz",
7
+ "bin": {
8
+ "agentcraft": "./bin/agentcraft.js"
9
+ },
10
+ "keywords": ["claude-code", "opencode", "sounds", "hooks", "audio", "agents", "ai"],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/rohenaz/agentcraft"
14
+ },
15
+ "homepage": "https://github.com/rohenaz/agentcraft#readme",
16
+ "engines": {
17
+ "node": ">=18"
18
+ }
19
+ }
package/screenshot.jpg ADDED
Binary file
Binary file
Binary file
Binary file
package/web/README.md ADDED
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,44 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { writeFile, unlink } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const AGENTS_DIR = join(homedir(), '.claude', 'agents');
7
+
8
+ function buildAgentContent(data: { name: string; description: string; model: string; tools: string; color: string; prompt: string }): string {
9
+ const lines = ['---', `name: ${data.name}`, `description: ${data.description}`, `model: ${data.model}`];
10
+ if (data.tools) lines.push(`tools: ${data.tools}`);
11
+ if (data.color) lines.push(`color: ${data.color}`);
12
+ lines.push('---', '', data.prompt || '');
13
+ return lines.join('\n');
14
+ }
15
+
16
+ export async function PUT(
17
+ req: NextRequest,
18
+ { params }: { params: Promise<{ name: string }> }
19
+ ) {
20
+ try {
21
+ const { name } = await params;
22
+ const data = await req.json();
23
+ const filename = `${name}.md`;
24
+ const content = buildAgentContent(data);
25
+ await writeFile(join(AGENTS_DIR, filename), content, 'utf-8');
26
+ return NextResponse.json({ ok: true });
27
+ } catch {
28
+ return NextResponse.json({ error: 'Failed to update agent' }, { status: 500 });
29
+ }
30
+ }
31
+
32
+ export async function DELETE(
33
+ req: NextRequest,
34
+ { params }: { params: Promise<{ name: string }> }
35
+ ) {
36
+ try {
37
+ const { name } = await params;
38
+ const filename = `${name}.md`;
39
+ await unlink(join(AGENTS_DIR, filename));
40
+ return NextResponse.json({ ok: true });
41
+ } catch {
42
+ return NextResponse.json({ error: 'Failed to delete agent' }, { status: 500 });
43
+ }
44
+ }
@@ -0,0 +1,62 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { readdir, readFile, writeFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import type { AgentInfo } from '@/lib/types';
6
+
7
+ const AGENTS_DIR = join(homedir(), '.claude', 'agents');
8
+
9
+ function buildAgentContent(data: { name: string; description: string; model: string; tools: string; color: string; prompt: string }): string {
10
+ const lines = ['---', `name: ${data.name}`, `description: ${data.description}`, `model: ${data.model}`];
11
+ if (data.tools) lines.push(`tools: ${data.tools}`);
12
+ if (data.color) lines.push(`color: ${data.color}`);
13
+ lines.push('---', '', data.prompt || '');
14
+ return lines.join('\n');
15
+ }
16
+
17
+ export async function GET() {
18
+ try {
19
+ const files = await readdir(AGENTS_DIR);
20
+ const agents: AgentInfo[] = [];
21
+
22
+ for (const file of files) {
23
+ if (!file.endsWith('.md')) continue;
24
+ const content = await readFile(join(AGENTS_DIR, file), 'utf-8');
25
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
26
+ const descMatch = content.match(/^description:\s*(.+)$/m);
27
+ const modelMatch = content.match(/^model:\s*(.+)$/m);
28
+ const toolsMatch = content.match(/^tools:\s*(.+)$/m);
29
+ const colorMatch = content.match(/^color:\s*(.+)$/m);
30
+ const promptMatch = content.match(/^---\s*\n(?:.*\n)*?---\s*\n([\s\S]*)$/);
31
+ const name = nameMatch ? nameMatch[1].trim() : file.replace('.md', '');
32
+ agents.push({
33
+ name,
34
+ filename: file,
35
+ description: descMatch ? descMatch[1].trim() : '',
36
+ model: modelMatch ? modelMatch[1].trim() : 'sonnet',
37
+ tools: toolsMatch ? toolsMatch[1].trim() : '',
38
+ color: colorMatch ? colorMatch[1].trim() : '',
39
+ prompt: promptMatch ? promptMatch[1].trim() : '',
40
+ });
41
+ }
42
+
43
+ return NextResponse.json(agents);
44
+ } catch {
45
+ return NextResponse.json([]);
46
+ }
47
+ }
48
+
49
+ export async function POST(req: NextRequest) {
50
+ try {
51
+ const data = await req.json();
52
+ if (!data.name || typeof data.name !== 'string') {
53
+ return NextResponse.json({ error: 'name required' }, { status: 400 });
54
+ }
55
+ const filename = `${data.name.toLowerCase().replace(/\s+/g, '-')}.md`;
56
+ const content = buildAgentContent(data);
57
+ await writeFile(join(AGENTS_DIR, filename), content, 'utf-8');
58
+ return NextResponse.json({ ok: true, filename });
59
+ } catch {
60
+ return NextResponse.json({ error: 'Failed to create agent' }, { status: 500 });
61
+ }
62
+ }
@@ -0,0 +1,53 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { readFile, writeFile, mkdir } from 'fs/promises';
3
+ import { join, dirname } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const ASSIGNMENTS_PATH = join(homedir(), '.agentcraft', 'assignments.json');
7
+
8
+ export async function GET() {
9
+ try {
10
+ const data = await readFile(ASSIGNMENTS_PATH, 'utf-8');
11
+ const parsed = JSON.parse(data);
12
+ // Ensure required keys exist for older assignment files
13
+ if (!parsed.skills) parsed.skills = {};
14
+ if (!parsed.settings) parsed.settings = {};
15
+ if (!parsed.settings.uiTheme) parsed.settings.uiTheme = 'sc2';
16
+ return NextResponse.json(parsed);
17
+ } catch {
18
+ return NextResponse.json({
19
+ global: {},
20
+ agents: {},
21
+ skills: {},
22
+ settings: {
23
+ masterVolume: 1.0,
24
+ enabled: true,
25
+ theme: 'terran',
26
+ uiTheme: 'sc2',
27
+ uiSounds: {
28
+ sc2: {
29
+ hover: 'ui/sc-bigbox/set2-move.mp3',
30
+ click: 'ui/sc2/click.mp3',
31
+ error: 'ui/sc2/error.mp3',
32
+ },
33
+ wc3: {
34
+ hover: 'ui/wc3/hover.mp3',
35
+ click: 'ui/wc3/click.mp3',
36
+ error: 'ui/wc3/error.mp3',
37
+ },
38
+ },
39
+ },
40
+ });
41
+ }
42
+ }
43
+
44
+ export async function POST(req: NextRequest) {
45
+ try {
46
+ const body = await req.json();
47
+ await mkdir(dirname(ASSIGNMENTS_PATH), { recursive: true });
48
+ await writeFile(ASSIGNMENTS_PATH, JSON.stringify(body, null, 2), 'utf-8');
49
+ return NextResponse.json({ ok: true });
50
+ } catch {
51
+ return NextResponse.json({ error: 'Failed to write assignments' }, { status: 500 });
52
+ }
53
+ }
@@ -0,0 +1,35 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { readFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const SOUND_LIBRARY = join(homedir(), '.agentcraft', 'sounds');
7
+
8
+ export async function GET(
9
+ req: NextRequest,
10
+ { params }: { params: Promise<{ path: string[] }> }
11
+ ) {
12
+ try {
13
+ const { path: pathParts } = await params;
14
+ const relativePath = pathParts.join('/');
15
+
16
+ if (relativePath.includes('..')) {
17
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
18
+ }
19
+
20
+ const fullPath = join(SOUND_LIBRARY, relativePath);
21
+ const data = await readFile(fullPath);
22
+
23
+ const ext = relativePath.split('.').pop()?.toLowerCase();
24
+ const contentType = ext === 'mp3' ? 'audio/mpeg' : ext === 'wav' ? 'audio/wav' : 'audio/mpeg';
25
+
26
+ return new NextResponse(data, {
27
+ headers: {
28
+ 'Content-Type': contentType,
29
+ 'Cache-Control': 'public, max-age=86400',
30
+ },
31
+ });
32
+ } catch {
33
+ return NextResponse.json({ error: 'Audio file not found' }, { status: 404 });
34
+ }
35
+ }
@@ -0,0 +1,5 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({ status: 'ok', version: '1.0.0' });
5
+ }
@@ -0,0 +1,22 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { spawn } from 'child_process';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const SOUND_LIBRARY = join(homedir(), '.agentcraft', 'sounds');
7
+
8
+ export async function POST(req: NextRequest) {
9
+ try {
10
+ const { path } = await req.json();
11
+ if (!path || typeof path !== 'string' || path.includes('..')) {
12
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
13
+ }
14
+
15
+ const fullPath = join(SOUND_LIBRARY, path);
16
+ spawn('afplay', [fullPath], { detached: true, stdio: 'ignore' }).unref();
17
+
18
+ return NextResponse.json({ ok: true });
19
+ } catch {
20
+ return NextResponse.json({ error: 'Playback failed' }, { status: 500 });
21
+ }
22
+ }