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.
- package/.claude-plugin/plugin.json +7 -0
- package/README.md +95 -0
- package/bin/agentcraft.js +12 -0
- package/commands/agentcraft.md +46 -0
- package/hooks/hooks.json +13 -0
- package/hooks/play-sound.sh +69 -0
- package/opencode.js +87 -0
- package/package.json +19 -0
- package/screenshot.jpg +0 -0
- package/social-share-og.jpg +0 -0
- package/social-share-tw.jpg +0 -0
- package/social-share.png +0 -0
- package/web/README.md +36 -0
- package/web/app/api/agents/[name]/route.ts +44 -0
- package/web/app/api/agents/route.ts +62 -0
- package/web/app/api/assignments/route.ts +53 -0
- package/web/app/api/audio/[...path]/route.ts +35 -0
- package/web/app/api/health/route.ts +5 -0
- package/web/app/api/preview/route.ts +22 -0
- package/web/app/api/skills/route.ts +83 -0
- package/web/app/api/sounds/route.ts +106 -0
- package/web/app/api/ui-sounds/route.ts +44 -0
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +99 -0
- package/web/app/icon.svg +13 -0
- package/web/app/layout.tsx +19 -0
- package/web/app/opengraph-image.tsx +96 -0
- package/web/app/page.tsx +268 -0
- package/web/bun.lock +292 -0
- package/web/components/agent-form.tsx +190 -0
- package/web/components/agent-roster-panel.tsx +502 -0
- package/web/components/assignment-log-panel.tsx +109 -0
- package/web/components/hook-slot.tsx +149 -0
- package/web/components/hud-header.tsx +135 -0
- package/web/components/sound-browser-panel.tsx +206 -0
- package/web/components/sound-unit.tsx +203 -0
- package/web/components/ui-sounds-modal.tsx +308 -0
- package/web/lib/types.ts +87 -0
- package/web/lib/ui-audio.ts +126 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.ts +8 -0
- package/web/package.json +37 -0
- package/web/postcss.config.mjs +1 -0
- package/web/public/file.svg +1 -0
- package/web/public/fonts/starcraft-normal.ttf +0 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- 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
|
+

|
|
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
|
+
```
|
package/hooks/hooks.json
ADDED
|
@@ -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
|
package/social-share.png
ADDED
|
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,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
|
+
}
|