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,126 @@
1
+ 'use client';
2
+
3
+ type SoundName = 'click' | 'hover' | 'error' | 'pageChange' | 'toggle' | 'confirm';
4
+
5
+ let audioCtx: AudioContext | null = null;
6
+ const bufferCache = new Map<string, AudioBuffer | null>();
7
+ let currentTheme = 'off';
8
+ let lastHoveredEl: Element | null = null;
9
+ let cleanupListeners: (() => void) | null = null;
10
+
11
+ // Custom slot paths: e.g. { click: 'ui/sc-bigbox/set1-select.mp3', hover: 'ui/sc-bigbox/set3-move.mp3' }
12
+ let customSlots: Partial<Record<SoundName, string>> = {};
13
+
14
+ function getCtx(): AudioContext {
15
+ if (!audioCtx) audioCtx = new AudioContext();
16
+ return audioCtx;
17
+ }
18
+
19
+ async function loadBuffer(url: string): Promise<AudioBuffer | null> {
20
+ if (bufferCache.has(url)) return bufferCache.get(url) ?? null;
21
+ bufferCache.set(url, null); // placeholder so concurrent calls don't double-fetch
22
+ try {
23
+ const res = await fetch(url + '?v=' + Date.now()); // cache-bust
24
+ if (!res.ok) return null;
25
+ const buf = await getCtx().decodeAudioData(await res.arrayBuffer());
26
+ bufferCache.set(url, buf);
27
+ return buf;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function urlFor(name: SoundName): string | null {
34
+ if (currentTheme === 'off') return null;
35
+ const custom = customSlots[name];
36
+ if (!custom && (name === 'pageChange' || name === 'toggle' || name === 'confirm')) return null; // no defaults — must be explicitly set
37
+ const rel = custom ?? `ui/${currentTheme}/${name}.mp3`;
38
+ return `/api/audio/${rel}`;
39
+ }
40
+
41
+ export async function setUITheme(
42
+ theme: string,
43
+ uiSounds?: Partial<Record<SoundName, string>>
44
+ ): Promise<void> {
45
+ currentTheme = theme;
46
+ customSlots = uiSounds ?? {};
47
+ if (theme === 'off') return;
48
+
49
+ const names: SoundName[] = ['click', 'hover', 'error', 'pageChange', 'toggle', 'confirm'];
50
+ await Promise.all(
51
+ names.map((name) => {
52
+ const url = urlFor(name);
53
+ if (!url) return;
54
+ bufferCache.delete(url); // force reload
55
+ return loadBuffer(url);
56
+ })
57
+ );
58
+ }
59
+
60
+ export function playUISound(name: SoundName, volume = 0.3): void {
61
+ if (currentTheme === 'off') return;
62
+ const url = urlFor(name);
63
+ if (!url) return;
64
+ const buf = bufferCache.get(url);
65
+ if (!buf) return;
66
+
67
+ const ctx = getCtx();
68
+ if (ctx.state === 'suspended') ctx.resume();
69
+ const src = ctx.createBufferSource();
70
+ const gain = ctx.createGain();
71
+ gain.gain.value = volume;
72
+ src.buffer = buf;
73
+ src.connect(gain);
74
+ gain.connect(ctx.destination);
75
+ src.start();
76
+ }
77
+
78
+ /** Preview any sound by path immediately in the browser (bypasses theme) */
79
+ export async function previewUISound(relativePath: string, volume = 0.5): Promise<void> {
80
+ const url = `/api/audio/${relativePath}`;
81
+ bufferCache.delete(url);
82
+ const buf = await loadBuffer(url);
83
+ if (!buf) return;
84
+ const ctx = getCtx();
85
+ if (ctx.state === 'suspended') ctx.resume();
86
+ const src = ctx.createBufferSource();
87
+ const gain = ctx.createGain();
88
+ gain.gain.value = volume;
89
+ src.buffer = buf;
90
+ src.connect(gain);
91
+ gain.connect(ctx.destination);
92
+ src.start();
93
+ }
94
+
95
+ export function initGlobalUIListeners(): () => void {
96
+ if (cleanupListeners) cleanupListeners();
97
+
98
+ function handleClick(e: MouseEvent) {
99
+ const target = e.target as Element;
100
+ if (target.closest('[data-no-ui-sound]')) return;
101
+ const el = target.closest('button, [role="button"], [data-sf-hover]');
102
+ if (el && !el.hasAttribute('data-no-ui-sound')) {
103
+ playUISound('click');
104
+ }
105
+ }
106
+
107
+ function handleMouseover(e: MouseEvent) {
108
+ const el = (e.target as Element).closest('[data-sf-hover]');
109
+ if (el && el !== lastHoveredEl) {
110
+ lastHoveredEl = el;
111
+ playUISound('hover', 0.2);
112
+ }
113
+ if (!el) lastHoveredEl = null;
114
+ }
115
+
116
+ document.addEventListener('click', handleClick, { passive: true });
117
+ document.addEventListener('mouseover', handleMouseover, { passive: true });
118
+
119
+ cleanupListeners = () => {
120
+ document.removeEventListener('click', handleClick);
121
+ document.removeEventListener('mouseover', handleMouseover);
122
+ cleanupListeners = null;
123
+ };
124
+
125
+ return cleanupListeners;
126
+ }
@@ -0,0 +1,98 @@
1
+ import type { SoundAsset } from './types';
2
+
3
+ export function formatSoundName(filename: string): string {
4
+ return filename
5
+ .replace(/\.(mp3|wav|ogg|m4a)$/i, '')
6
+ .replace(/[-_]/g, ' ')
7
+ .replace(/\b\w/g, (c) => c.toUpperCase());
8
+ }
9
+
10
+ export function groupSoundsByCategory(sounds: SoundAsset[]): Record<string, Record<string, SoundAsset[]>> {
11
+ const grouped: Record<string, Record<string, SoundAsset[]>> = {};
12
+ for (const sound of sounds) {
13
+ if (!grouped[sound.category]) grouped[sound.category] = {};
14
+ if (!grouped[sound.category][sound.subcategory]) grouped[sound.category][sound.subcategory] = [];
15
+ grouped[sound.category][sound.subcategory].push(sound);
16
+ }
17
+ return grouped;
18
+ }
19
+
20
+ export function getGroupLabel(group: string): string {
21
+ const labels: Record<string, string> = {
22
+ 'sc2': 'SC2',
23
+ 'wc3': 'Warcraft',
24
+ 'aoe': 'AoE',
25
+ 'cnc': 'C&C',
26
+ 'homeworld': 'Homeworld',
27
+ 'classic-os': 'Classic OS',
28
+ 'apps': 'Apps',
29
+ 'phones': 'Phones',
30
+ 'devices': 'Devices',
31
+ 'ui': 'UI SFX',
32
+ };
33
+ return labels[group] ?? group.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
34
+ }
35
+
36
+ export function getSubTabLabel(category: string): string {
37
+ const labels: Record<string, string> = {
38
+ 'sc2/terran': 'Terran',
39
+ 'sc2/protoss': 'Protoss',
40
+ 'sc2/zerg': 'Zerg',
41
+ 'sc2/alerts': 'Alerts',
42
+ 'wc3/orc': 'Orc',
43
+ 'wc3/human': 'Human',
44
+ 'wc3/nightelf': 'Night Elf',
45
+ 'wc3/undead': 'Undead',
46
+ 'cnc/gdi': 'GDI',
47
+ 'cnc/nod': 'NOD',
48
+ 'cnc/allied': 'Allied',
49
+ 'cnc/soviet': 'Soviet',
50
+ 'cnc/eva': 'EVA',
51
+ };
52
+ const segment = category.split('/').pop() ?? category;
53
+ return labels[category] ?? segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
54
+ }
55
+
56
+ export function getCategoryLabel(category: string): string {
57
+ const labels: Record<string, string> = {
58
+ 'sc2/terran': 'SC2 Terran',
59
+ 'sc2/protoss': 'SC2 Protoss',
60
+ 'sc2/zerg': 'SC2 Zerg',
61
+ 'sc2/alerts': 'SC2 Alerts',
62
+ 'wc3/orc': 'WC3 Orc',
63
+ 'wc3/human': 'WC3 Human',
64
+ 'wc3/nightelf': 'WC3 Night Elf',
65
+ 'wc3/undead': 'WC3 Undead',
66
+ 'classic-os': 'Classic OS',
67
+ 'apps': 'Apps',
68
+ 'phones': 'Phones',
69
+ 'devices': 'Devices',
70
+ 'homeworld': 'Homeworld',
71
+ 'aoe': 'Age of Empires',
72
+ 'cnc': 'C&C',
73
+ 'cnc/gdi': 'C&C GDI',
74
+ 'cnc/nod': 'C&C NOD',
75
+ 'cnc/allied': 'C&C Allied',
76
+ 'cnc/soviet': 'C&C Soviet',
77
+ 'cnc/eva': 'C&C EVA',
78
+ };
79
+ // Auto-format unknown categories
80
+ return labels[category] ?? (category.split('/').pop() ?? category)
81
+ .replace(/-/g, ' ')
82
+ .replace(/\b\w/g, (c) => c.toUpperCase());
83
+ }
84
+
85
+ export function getEventLabel(event: string): string {
86
+ const labels: Record<string, string> = {
87
+ 'SessionStart': 'SESSION START',
88
+ 'SessionEnd': 'SESSION END',
89
+ 'Stop': 'STOP',
90
+ 'SubagentStop': 'SUBAGENT STOP',
91
+ 'PreToolUse': 'PRE TOOL USE',
92
+ 'PostToolUse': 'POST TOOL USE',
93
+ 'PostToolUseFailure': 'TOOL FAILURE',
94
+ 'Notification': 'NOTIFICATION',
95
+ 'PreCompact': 'PRE COMPACT',
96
+ };
97
+ return labels[event] ?? event;
98
+ }
@@ -0,0 +1,8 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ reactCompiler: true,
6
+ };
7
+
8
+ export default nextConfig;
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "web",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --port 4040",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "@dnd-kit/core": "^6.3.1",
12
+ "@dnd-kit/sortable": "^10.0.0",
13
+ "@dnd-kit/utilities": "^3.2.2",
14
+ "audio-decode": "^2.2.3",
15
+ "lucide-react": "^0.575.0",
16
+ "next": "16.1.6",
17
+ "react": "19.2.4",
18
+ "react-dom": "19.2.4"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/postcss": "^4",
22
+ "@types/node": "^25.3.0",
23
+ "@types/react": "^19",
24
+ "@types/react-dom": "^19",
25
+ "babel-plugin-react-compiler": "1.0.0",
26
+ "tailwindcss": "^4",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "ignoreScripts": [
30
+ "sharp",
31
+ "unrs-resolver"
32
+ ],
33
+ "trustedDependencies": [
34
+ "sharp",
35
+ "unrs-resolver"
36
+ ]
37
+ }
@@ -0,0 +1 @@
1
+ export default { plugins: { "@tailwindcss/postcss": {} } }
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }